Compare commits

40 Commits
Author SHA1 Message Date
jbranchaud 8ee23956d2 Add Connect To Individual Overmind Processes Via tmux as a tmux TIL 2026-08-30 08:19:24 -05:00
jbranchaud 7e4a02e66f Add Output Query Result In Nicely Formatted Table as a SQLite TIL 2026-08-29 21:05:44 -05:00
jbranchaud 965bade9d4 Add Register SQLite Adapter To Serialize Datetimes as a Python TIL 2026-08-28 13:46:28 -05:00
jbranchaud 2a2a962864 Add Set Permission Mode When Starting Session as a Claude Code TIL 2026-08-27 14:25:05 -05:00
jbranchaud a1708116bb Add Add Check Constraint To Existing Column as a SQLite TIL 2026-08-25 20:44:10 -05:00
jbranchaud 85a6dd3644 Add Specify Parameter Name For Click Option as a Python TIL 2026-08-23 18:06:37 -05:00
jbranchaud 453e9edbdf Add Check What Is Inside A Zip File as a Unix TIL 2026-08-22 09:04:49 -05:00
jbranchaud bcf957cabd Add Generate Sample PDFs With ReportLab as a Python TIL 2026-08-21 10:58:37 -05:00
jbranchaud fe019c3338 Change a couple 'we' to 'I' 2026-08-19 22:56:40 -05:00
jbranchaud 5773961c93 Add Remove Pages From A PDF as a Workflow TIL 2026-08-19 22:52:26 -05:00
jbranchaud 8191e9303b Add Escape Curly Braces Within Formatted String as a Python TIL 2026-08-19 22:11:53 -05:00
jbranchaud 74e71cf1af Add Duplicate Current Browser Tab as a Chrome TIL 2026-08-19 12:05:08 -05:00
jbranchaud a16dcc7760 Add Join A List Of Strings as a Python TIL 2026-08-18 21:42:31 -05:00
jbranchaud c495194a0d Add npm run Has Some Typo Aliases as a JavaScript TIL 2026-08-17 22:24:05 -05:00
jbranchaud e3f8446980 Add Initialize A PyTorch Tensor as a Python TIL 2026-08-16 23:11:25 -05:00
jbranchaud d4ebff5939 Add Override Timezone That All Tests Run In as a Python TIL 2026-08-15 16:06:26 -05:00
jbranchaud 565d6a4680 Add Override Output Field Separator as a Ruby TIL 2026-08-14 21:39:41 -05:00
jbranchaud 3c1ec67965 Add Delete Empty Files With Find as a Unix TIL 2026-08-13 11:19:28 -05:00
jbranchaud f83b5003de Add Handle Bad Numerical Amounts With BigDecimal as a Rails TIL 2026-08-12 08:32:38 -05:00
jbranchaud 78c9e92bae Add Where And Which Are Whence as a Zsh TIL 2026-08-12 01:03:25 -05:00
jbranchaud 2bb402ad40 Add Combine StrEnum With Pydantic For Union Type as a Python TIL 2026-08-10 17:08:27 -05:00
jbranchaud 38b822fb5c Add Convert Arbitrary Number To Probability With Sigmoid as a Math TIL 2026-08-09 13:17:59 -05:00
jbranchaud d0496bd8ce Add Sequence Side-Effect With Rolling Back Inserts as a Postgres TIL 2026-08-08 08:54:19 -05:00
jbranchaud 47b83535b8 Add Annotate Return Type On Enum Function as a Python TIL 2026-08-07 22:27:43 -05:00
jbranchaud a83030bb6d Add Filter By Truthy Values With Boolean Function as a JavaScript TIL 2026-08-07 16:44:31 -05:00
jbranchaud eb419752c4 Add Publish A Package To A Test Env As A Dry Run as a Python TIL 2026-08-06 19:28:35 -05:00
jbranchaud b484204c2f Add Remove Blank Values From A Hash as a Rails TIL 2026-08-06 09:25:41 -05:00
jbranchaud f2de2ce3f4 Add List And Count All Posts In TIL Repo as a Git TIL 2026-08-05 18:26:08 -05:00
jbranchaud 4055ed7a7d Add Manage Lightweight Schema Migrations With User Version as a SQLite TIL 2026-08-04 17:15:28 -05:00
jbranchaud 0866968be3 Add Mark A Release With An Annotated Tag as a Git TIL 2026-08-02 20:22:22 -05:00
jbranchaud 2e34d6480b Add Experiment With SQLite Queries In Memory as a Python TIL 2026-08-01 19:10:00 -05:00
jbranchaud 64b464c931 Add Move YouTube Subtitles Out Of The Way as a Workflow TIL 2026-07-31 23:40:54 -05:00
jbranchaud 1a0b02168e Add Another Way To Mark Keyword-Only Dataclass Fields as a Python TIL 2026-07-30 22:10:21 -05:00
jbranchaud 146274230e Add Run Scheduled Action To Commit Regular Updates as a GitHub Actions TIL 2026-07-29 18:21:47 -05:00
jbranchaud 300a22f18b Fix indentation of latest TIL 2026-07-29 01:23:23 -05:00
jbranchaud 1225349978 Add Dedent Common Whitespace From Multiline String as a Python TIL 2026-07-29 01:00:46 -05:00
jbranchaud 0fbf06a2ce Add Open Current Command Line Prompt In Editor as a Zsh TIL 2026-07-28 17:51:39 -05:00
jbranchaud 2c247f2e20 Add Start Jupyter Notebook With Extra Packages as a Python TIL 2026-07-27 18:12:36 -05:00
jbranchaud 6a67541aa4 Add Include A File With Message To Ant as an LLM TIL 2026-07-27 09:04:16 -05:00
jbranchaud 57188341a6 Add Configure Other Attributes Of Dataclass Field as a Python TIL 2026-07-26 19:12:31 -05:00
39 changed files with 1949 additions and 1 deletions
+39 -1
View File
@@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/).
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
_1839 TILs and counting..._
_1877 TILs and counting..._
See some of the other learning resources I work on:
@@ -148,6 +148,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Access A Value Logged To The Console](chrome/access-a-value-logged-to-the-console.md)
- [Chrome Supports Many Unix Keyboard Shortcuts](chrome/chrome-supports-many-unix-keyboard-shortcuts.md)
- [Copy Some Data From The Console](chrome/copy-some-data-from-the-console.md)
- [Duplicate Current Browser Tab](chrome/duplicate-current-browser-tab.md)
- [Duplicate The Current Tab](chrome/duplicate-the-current-tab.md)
- [Easier Access To Network Throttling Controls](chrome/easier-access-to-network-throttling-controls.md)
- [Keybinding To Focus The Address Bar](chrome/keybinding-to-focus-the-address-bar.md)
@@ -171,6 +172,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Monitor Usage Limits From CLI](claude-code/monitor-usage-limits-from-cli.md)
- [Open Current Prompt In Default Editor](claude-code/open-current-prompt-in-default-editor.md)
- [Resume Specific Session](claude-code/resume-specific-session.md)
- [Set Permission Mode When Starting Session](claude-code/set-permission-mode-when-starting-session.md)
- [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md)
### Clojure
@@ -402,6 +404,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [List All Files Added During Span Of Time](git/list-all-files-added-during-span-of-time.md)
- [List All Files Changed Between Two Branches](git/list-all-files-changed-between-two-branches.md)
- [List All Git Aliases From gitconfig](git/list-all-git-aliases-from-gitconfig.md)
- [List And Count All Posts In TIL Repo](git/list-and-count-all-posts-in-til-repo.md)
- [List Branches That Contain A Commit](git/list-branches-that-contain-a-commit.md)
- [List Commits On A Branch](git/list-commits-on-a-branch.md)
- [List Different Commits Between Two Branches](git/list-different-commits-between-two-branches.md)
@@ -410,6 +413,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [List Most Git Commands](git/list-most-git-commands.md)
- [List Untracked Files](git/list-untracked-files.md)
- [List Untracked Files For Scripting](git/list-untracked-files-for-scripting.md)
- [Mark A Release With An Annotated Tag](git/mark-a-release-with-an-annotated-tag.md)
- [Move The Latest Commit To A New Branch](git/move-the-latest-commit-to-a-new-branch.md)
- [Override The Global Git Ignore File](git/override-the-global-git-ignore-file.md)
- [Pick Specific Changes To Stash](git/pick-specific-changes-to-stash.md)
@@ -486,6 +490,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Capture An Output Value For Use In A Later Step](github-actions/capture-an-output-value-for-use-in-a-later-step.md)
- [Disable A Workflow With The gh CLI](github-actions/disable-a-workflow-with-the-gh-cli.md)
- [Reference An Encrypted Secret In An Action](github-actions/reference-an-encrypted-secret-in-an-action.md)
- [Run Schedule Action To Commit Regular Updates](github-actions/run-scheduled-action-to-commit-regular-updates.md)
- [Trigger A Workflow Via An API Call](github-actions/trigger-a-workflow-via-an-api-call.md)
- [Use Labels To Block PR Merge](github-actions/use-labels-to-block-pr-merge.md)
@@ -616,6 +621,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Easy Date Comparison With DayJS](javascript/easy-date-comparison-with-dayjs.md)
- [Expand Emojis With The Spread Operator](javascript/expand-emojis-with-the-spread-operator.md)
- [Fill An Input With A Ton Of Text](javascript/fill-an-input-with-a-ton-of-text.md)
- [Filter By Truthy Values With Boolean Function](javascript/filter-by-truthy-values-with-boolean-function.md)
- [Find The Version Of An Installed Dependency](javascript/find-the-version-of-an-installed-dependency.md)
- [Find Where Yarn Is Installing Binaries](javascript/find-where-yarn-is-installing-binaries.md)
- [for...in Iterates Over Object Properties](javascript/for-in-iterates-over-object-properties.md)
@@ -647,6 +653,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Matching Multiple Values In A Switch Statement](javascript/matching-multiple-values-in-a-switch-statement.md)
- [Mock A Function With Return Values Using Jest](javascript/mock-a-function-with-return-values-using-jest.md)
- [New Dates Can Take Out Of Bounds Values](javascript/new-dates-can-take-out-of-bounds-values.md)
- [`npm run` Has Some Typo Aliases](javascript/npm-run-has-some-typo-aliases.md)
- [Numbers Are Empty](javascript/numbers-are-empty.md)
- [Object Initialization With Shorthand Property Names](javascript/object-initialization-with-shorthand-property-names.md)
- [Obtain Undefined Value With The Void Operator](javascript/obtain-undefined-value-with-the-void-operator.md)
@@ -732,6 +739,7 @@ If you've learned something here, support my efforts writing daily TILs by
### LLM
- [Count Number Of Tokens In A File](llm/count-number-of-tokens-in-a-file.md)
- [Include A File With Message To `ant`](llm/include-a-file-with-message-to-ant.md)
- [Send cURL To Claude Text Completion API](llm/send-curl-to-claude-text-completion-api.md)
- [Use The llm CLI With Claude Models](llm/use-the-llm-cli-with-claude-models.md)
@@ -781,6 +789,7 @@ If you've learned something here, support my efforts writing daily TILs by
### Math
- [Convert Arbitrary Number To Probability With Sigmoid](math/convert-arbitrary-number-to-probability-with-sigmoid.md)
- [Generate Permutations Of All Valid 9-ball Racks](math/generate-permutations-of-all-valid-9-ball-racks.md)
### Mise
@@ -1008,6 +1017,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Restarting Sequences When Truncating Tables](postgres/restarting-sequences-when-truncating-tables.md)
- [Salt And Hash A Password With pgcrypto](postgres/salt-and-hash-a-password-with-pgcrypto.md)
- [Send A Command To psql](postgres/send-a-command-to-psql.md)
- [Sequence Side-Effect When Rolling Back Inserts](postgres/sequence-side-effect-when-rolling-back-inserts.md)
- [Set Inclusion With hstore](postgres/set-inclusion-with-hstore.md)
- [Set A Seed For The Random Number Generator](postgres/set-a-seed-for-the-random-number-generator.md)
- [Set A Statement Timeout Threshold For A Session](postgres/set-a-statement-timeout-threshold-for-a-session.md)
@@ -1071,6 +1081,8 @@ If you've learned something here, support my efforts writing daily TILs by
- [Access Most Recent Return Value In REPL](python/access-most-recent-return-value-in-repl.md)
- [Access SQLite Result Values By Name With Row Factory](python/access-sqlite-result-values-by-name-with-row-factory.md)
- [Access Variables Outside Loop Scope](python/access-variables-outside-loop-scope.md)
- [Annotate Return Type On Enum Function](python/annotate-return-type-on-enum-function.md)
- [Another Way To Mark Keyword-Only Dataclass Fields](python/another-way-to-mark-keyword-only-dataclass-fields.md)
- [Argument Defaults Are Evaluated When Function Is Defined](python/argument-defaults-are-evaluated-when-function-is-defined.md)
- [Assert Is Only A Development Check](python/assert-is-only-a-development-check.md)
- [Avoid Modification With Frozen Dataclass](python/avoid-modification-with-frozen-dataclass.md)
@@ -1078,23 +1090,31 @@ If you've learned something here, support my efforts writing daily TILs by
- [Break Debugger On First Line Of Program](python/break-debugger-on-first-line-of-program.md)
- [Check If Package Is Installed With Pip](python/check-if-package-is-installed-with-pip.md)
- [Check Precondition Before Click Arg Parsing](python/check-precondition-before-click-arg-parsing.md)
- [Combine StrEnum With Pydantic For Union Type](python/combine-strenum-with-pydantic-for-union-type.md)
- [Commit Writes From Executed SQLite Statements](python/commit-writes-from-executed-sqlite-statements.md)
- [Configure Other Attributes Of Dataclass Field](python/configure-other-attributes-of-dataclass-field.md)
- [Control Passing Of Time In Tests](python/control-passing-of-time-in-tests.md)
- [Create A Dummy DataFrame In Pandas](python/create-a-dummy-dataframe-in-pandas.md)
- [Create A Range Of Descending Values](python/create-a-range-of-descending-values.md)
- [Dedent Common Whitespace From Multiline String](python/dedent-common-whitespace-from-multiline-string.md)
- [Deduplicate A List Into A Tuple](python/deduplicate-a-list-into-a-tuple.md)
- [Define Sequence Of Tests With Parametrize Decorator](python/define-sequence-of-tests-with-parametrize-decorator.md)
- [Define Typed Class Interface With Protocol](python/define-typed-class-interface-with-protocol.md)
- [Dunder Methods](python/dunder-methods.md)
- [Easy Key-Value Aggregates With defaultdict](python/easy-key-value-aggregates-with-defaultdict.md)
- [Enable Pyright Type Checking In Cursor](python/enable-pyright-type-checking-in-cursor.md)
- [Escape Curly Braces Within Formatted String](python/escape-curly-braces-within-formatted-string.md)
- [Experiment With SQLite Queries In Memory](python/experiment-with-sqlite-queries-in-memory.md)
- [Force Remaining Arguments To Be Named](python/force-remaining-arguments-to-be-named.md)
- [Generate Sample PDFs With ReportLab](python/generate-sample-pdfs-with-reportlab.md)
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md)
- [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
- [Globally Install CLI Tool With UV](python/globally-install-cli-tool-with-uv.md)
- [Initialize A PyTorch Tensor](python/initialize-a-pytorch-tensor.md)
- [Install With PIP For Specific Interpreter](python/install-with-pip-for-specific-interpreter.md)
- [Iterate First N Items From Enumerable](python/iterate-first-n-items-from-enumerable.md)
- [Iterate Over A Dictionary](python/iterate-over-a-dictionary.md)
- [Join A List Of Strings](python/join-a-list-of-strings.md)
- [Keep A Tally With collections.Counter](python/keep-a-tally-with-collections-counter.md)
- [Lint And Format Project With Ruff](python/lint-and-format-project-with-ruff.md)
- [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.md)
@@ -1102,14 +1122,19 @@ If you've learned something here, support my efforts writing daily TILs by
- [Make Dataclass Sortable By Specific Field](python/make-dataclass-sortable-by-specific-field.md)
- [Make Secure Temp File For Atomic Write](python/make-secure-temp-file-for-atomic-write.md)
- [Override The Boolean Context Of A Class](python/override-the-boolean-context-of-a-class.md)
- [Override Timezone That All Tests Run In](python/override-timezone-that-all-tests-run-in.md)
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
- [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md)
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
- [Register SQLite Adapter To Serialize Datetimes](python/register-sqlite-adapter-to-serialize-datetimes.md)
- [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md)
- [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md)
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
- [Specify Parameter Name For Click Option](python/specify-parameter-name-for-click-option.md)
- [Start Jupyter Notebook With Extra Packages](python/start-jupyter-notebook-with-extra-packages.md)
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
- [Strictly Separate Positional And Keyword Arguments](python/strictly-separate-positional-and-keyword-arguments.md)
@@ -1208,6 +1233,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Get The Current Time](rails/get-the-current-time.md)
- [Grab A Random Record From The Database](rails/grab-a-random-record-from-the-database.md)
- [Halt ActionMailer Delivery With Callback](rails/halt-action-mailer-delivery-with-callback.md)
- [Handle Bad Numerical Amounts With BigDecimal](rails/handle-bad-numerical-amounts-with-big-decimal.md)
- [Handle Named Arguments In A Rake Task](rails/handle-named-arguments-in-a-rake-task.md)
- [Hash Slicing](rails/hash-slicing.md)
- [Ignore Poltergeist JavaScript Errors](rails/ignore-poltergeist-javascript-errors.md)
@@ -1251,6 +1277,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Read-Only Models](rails/read-only-models.md)
- [Rebuild Tailwind Bundle For Dev Server](rails/rebuild-tailwind-bundle-for-dev-server.md)
- [Remove A Database Column From A Table](rails/remove-a-database-column-from-a-table.md)
- [Remove Blank Values From A Hash](rails/remove-blank-values-from-a-hash.md)
- [Remove The Default Value On A Column](rails/remove-the-default-value-on-a-column.md)
- [Render An Alternative ActionMailer Template](rails/render-an-alternative-action-mailer-template.md)
- [Render The Response Body In Controller Specs](rails/render-the-response-body-in-controller-specs.md)
@@ -1534,6 +1561,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [OpenStruct Has Bad Performance Characteristics](ruby/open-struct-has-bad-performance-characteristics.md)
- [Or Operator Precedence](ruby/or-operator-precedence.md)
- [Output Bytecode For A Ruby Program](ruby/output-bytecode-for-a-ruby-program.md)
- [Override Output Field Separator](ruby/override-output-field-separator.md)
- [Override The Initial Sequence Value](ruby/override-the-initial-sequence-value.md)
- [Parallel Bundle Install](ruby/parallel-bundle-install.md)
- [Parse JSON Into An OpenStruct](ruby/parse-json-into-an-open-struct.md)
@@ -1624,8 +1652,11 @@ If you've learned something here, support my efforts writing daily TILs by
### SQLite
- [Add Check Constraint To Existing Column](sqlite/add-check-constraint-to-existing-column.md)
- [Display Results In Readable Column Format](sqlite/display-results-in-readable-column-format.md)
- [Explore The Database Schema](sqlite/explore-the-database-schema.md)
- [Manage Lightweight Schema Migrations With `user_version`](sqlite/manage-lightweight-schema-migrations-with-user-version.md)
- [Output Query Result In Nicely Formatted Table](sqlite/output-query-result-in-nicely-formatted-table.md)
### Streaming
@@ -1655,6 +1686,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Change Base Directory Of Existing Session](tmux/change-base-directory-of-existing-session.md)
- [Change Base Directory Without Detaching](tmux/change-base-directory-without-detaching.md)
- [Change The Default Prefix Key](tmux/change-the-default-prefix-key.md)
- [Connect To Individual Overmind Processes Via tmux](tmux/connect-to-individual-overmind-processes-via-tmux.md)
- [Create A Named tmux Session](tmux/create-a-named-tmux-session.md)
- [Create A New Session In A New Server](tmux/create-a-new-session-in-a-new-server.md)
- [Cycle Through Layouts](tmux/cycle-through-layouts.md)
@@ -1727,6 +1759,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Check SSH Key Fingerprints Of Known Hosts](unix/check-ssh-key-fingerprints-of-known-hosts.md)
- [Check The Current Working Directory](unix/check-the-current-working-directory.md)
- [Check The Installed OpenSSL Version](unix/check-the-installed-openssl-version.md)
- [Check What Is Inside A Zip File](unix/check-what-is-inside-a-zip-file.md)
- [Clear The Screen](unix/clear-the-screen.md)
- [Combine All My TILs Into A Single File](unix/combine-all-my-tils-into-a-single-file.md)
- [Command Line Length Limitations](unix/command-line-length-limitations.md)
@@ -1747,6 +1780,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Curling For Headers](unix/curling-for-headers.md)
- [Curling With Basic Auth Credentials](unix/curling-with-basic-auth-credentials.md)
- [Deduplicate List While Preserving Original Order](unix/deduplicate-list-while-preserving-original-order.md)
- [Delete Empty Files With Find](unix/delete-empty-files-with-find.md)
- [Determine ipv4 And ipv6 Public IP Addresses](unix/determine-ipv4-and-ipv6-public-ip-addresses.md)
- [Diff Two Files In Unified Format](unix/diff-two-files-in-unified-format.md)
- [Different Ways To Generate A v4 UUID](unix/different-ways-to-generate-a-v4-uuid.md)
@@ -2113,9 +2147,11 @@ If you've learned something here, support my efforts writing daily TILs by
- [Get Your Public IP Address](workflow/get-your-public-ip-address.md)
- [Import A Github Project Into CodeSandbox](workflow/import-a-github-project-into-codesandbox.md)
- [Interactively Kill A Process With fkill](workflow/interactively-kill-a-process-with-fkill.md)
- [Move YouTube Subtitles Out Of The Way](workflow/move-youtube-subtitles-out-of-the-way.md)
- [Open Slack's Keyboard Shortcuts Reference Panel](workflow/open-slacks-keyboard-shortcuts-reference-panel.md)
- [Pop Videos Out As Picture-in-Picture](workflow/pop-videos-out-as-picture-in-picture.md)
- [Prune The Excess From node_modules](workflow/prune-the-excess-from-node-modules.md)
- [Remove Pages From A PDF](workflow/remove-pages-from-a-pdf.md)
- [Rotate An Image To Be Oriented Upright](workflow/rotate-an-image-to-be-oriented-upright.md)
- [See Overlaps For A Set Of Time Zones](workflow/see-overlaps-for-a-set-of-time-zones.md)
- [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md)
@@ -2168,7 +2204,9 @@ If you've learned something here, support my efforts writing daily TILs by
- [Create And Jump Into A Directory](zsh/create-and-jump-into-a-directory.md)
- [Link A Scalar To An Array](zsh/link-a-scalar-to-an-array.md)
- [List Available Zle Keybindings](zsh/list-available-zle-keybindings.md)
- [Open Current Command Line Prompt In Editor](zsh/open-current-command-line-prompt-in-editor.md)
- [Use A Space To Exclude Command From History](zsh/use-a-space-to-exclude-command-from-history.md)
- [Where And Which Are Whence](zsh/where-and-which-are-whence.md)
## Usage
+24
View File
@@ -0,0 +1,24 @@
# Duplicate Current Browser Tab
Let's say I'm on a specific page within a web app. Maybe I'm typing out a
comment on that page. Before I can hit submit, I need to reference something
elsewhere in the app, perhaps on the previous page.
I can `cmd+t` to open a new tab, type out the URL, hit enter, and then navigate
around until I find the page I'm looking for. I do this exact thing sometimes,
but it feels slow and clunky.
The other way I accomplish this which feels way smoother is to _duplicate the
current tab_. I can do that by `cmd`-clicking the reload button next to the URL
bar.
This opens another tab at the same URL with the same navigation history. I can
switch to that tab and hit the back button to go to the previous page, find the
thing I wanted to reference, and then return to the previous tab to finish what
I was doing.
To give a more concrete example: this happens all the time with GitHub PR
comments where I want to copy the URL or number of a specific PR to reference in
a comment I'm leaving on the current PR. Duplicating the tab, going _back_ to
the PR index view, finding the other PR, and copying its URL -- to me that is a
quicker flow popping open a fresh browser tab.
@@ -0,0 +1,29 @@
# Set Permission Mode When Starting Session
The way I typically use Claude Code day-to-day is with a couple long-running
sessions for one to two clones of the project. I start a session with `claude`
and then hit `shift+tab` until I've toggled it to _auto_ mode. I do tightly
scoped features and `/clear` the context in between each.
I get used to being in _auto_ mode, so whenever I start a new `claude` session I
forget to first toggle from _manual_ to _auto_ mode.
This is where the
[`--permission-mode`](https://code.claude.com/docs/en/permission-modes) flag can
help. I can start a session directly in _auto_ mode like so:
```bash
claude --permission-mode auto
```
Or if I know I want to generate a plan first, I can start it in _plan_ mode.
```bash
claude --permission-mode plan
```
There is also the `--dangerously-skip-permissions` flag which is equivalent to
`--permission-mode bypassPermissions`. I tend to stay away from those unless I'm
working from a sandboxed dev container.
See `claude --help` for more details.
@@ -0,0 +1,39 @@
# List And Count All Posts In TIL Repo
I want to be able to reliably list and count all posts in [my TIL
repo](https://github.com/jbranchaud/til). I do this to check that the count in
the README is accurate and in [the workflow
script](https://github.com/jbranchaud/jbranchaud/blob/71cba39dffb2bff68bf16d8895e435065e400250/scripts/update_tils.py#L101)
that powers [my GitHub Profile
README](https://github.com/jbranchaud/jbranchaud). In the past, I've used
pattern matching on the listing of all TILs in the
[README.md](https://github.com/jbranchaud/til/blob/master/README.md). That is
error prone and has required me to use two different markdown list styles.
A better approach is to ask `git` how many posts it currently has under version
control. I use a consistent directory structure where each TIL post is a
markdown file that is nested within a single category directory.
```bash
git ls-files -- */*.md
ack/ack-bar.md
ack/case-insensitive-search.md
ack/list-available-file-types.md
...
```
Using `git ls-files` has the added benefit of only listing files that are
currently checked in to the project. So if I run this locally, it won't pick up
a draft post that hasn't been committed yet.
I can then pipe this to `wc -l` (count of all lines) to produce the count:
```bash
git ls-files -- */*.md | wc -l | xargs
1850
```
Note: the empty `xargs` is a trick to trim the whitespace padding that `wc`
introduces.
See `man git-ls-files` for more details.
@@ -0,0 +1,74 @@
# Mark A Release With An Annotated Tag
There are two kinds of tags in Git -- lightweight tags and annotated tags.
The [`git-tag` docs](https://git-scm.com/docs/git-tag) explain the distinction:
> Annotated tags are meant for release while lightweight tags are meant for
> private or temporary object labels.
When an annotated tag is created, a _tag object_ is created which has a creation
timestamp, a "tagger" (who created it), a message, and potentially a signature
if GPG commit signing is configured.
I can create an annotated tag for the `HEAD` commit like so:
```bash
git tag -a v0.1.0 -m "Release v0.1.0"
```
I can then inspect what was created in a number of ways using `git tag --list`,
`git show`, `git cat-file`, and `git log --show-signature`.
```bash
git tag --list
v0.1.0
git show --no-patch v0.1.0
tag v0.1.0
Tagger: jbranchaud <jbranchaud@gmail.com>
Date: Sun, 2 Aug 2026 13:57:37 -0500
Release v0.1.0
commit 8a533ecfda526ebd1a4695639830f5620dd8572d (HEAD -> main, tag: v0.1.0, origin/main, origin/HEAD)
Author: jbranchaud <jbranchaud@gmail.com>
Date: Sun, 2 Aug 2026 12:51:41 -0500
Add changelog with v0.1.0 release changes documented
git cat-file -t v0.1.0
tag
git cat-file -p v0.1.0
object 8a533ecfda526ebd1a4695639830f5620dd8572d
type commit
tag v0.1.0
tagger jbranchaud <jbranchaud@gmail.com> 1785697057 -0500
Release v0.1.0
git log --show-signature
commit 8a533ecfda526ebd1a4695639830f5620dd8572d (HEAD -> main, tag: v0.1.0, origin/main, origin/HEAD)
gpg: Signature made Sun Aug 2 13:57:09 2026 CDT
gpg: using RSA key B2570A9DA3E2A537781501B11A8656918A8D016B
gpg: Good signature from "jbranchaud <jbranchaud@gmail.com>" [ultimate]
Author: jbranchaud <jbranchaud@gmail.com>
Date: Sun, 2 Aug 2026 12:51:41 -0500
Add changelog with v0.1.0 release changes documented
...
```
This tag will be included in a push when I run either of the following:
```bash
git push origin main
git push origin v0.1.0
```
This tag, which is now tied to a _release_, can be seen at [_Releases /
v0.1.0_](https://github.com/jbranchaud/py-vmt/releases/tag/v0.1.0) on GitHub.
See `man git-tag` for more details.
@@ -0,0 +1,58 @@
# Run Scheduled Action To Commit Regular Updates
The quintessential example of what GitHub Actions are used for is running CI
tasks like the test suite, type checker, linter, etc. Each CI step runs to
completion and either passes or fails and you see the results in the GitHub PR
interface. That's a great use case, but far from the only one. GitHub Actions
are a much more general-purpose execution environment that can be used for much
more.
Here is a recent example that expanded my mind a bit on what is possible with
GitHub actions. Imagine a GitHub Action that is scheduled to run once a day, it
executes a script that might make changes to the repo itself (e.g. the
`README.md`), and then commits those changes (self-updating the repo).
Here is a minimal version of a workflow that does that:
```yaml
on:
schedule:
- cron: '17 11 * * *' # daily at 11:17 UTC (06:17 CT)
workflow_dispatch:
permissions:
contents: write
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # get the repo + push credentials
- run: ./bin/regenerate_readme.sh # apply updates to README
- run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A README.md
git diff --staged --quiet || {
git commit -m "chore: regenerate README with latest updates"
git push
} # commit updates to README if there are any, otherwise no-op
```
Once a day at 11:17 UTC, GitHub Actions will run this workflow which is made up
of a single job with permissions to write to the repo.
- First, it checks out the repo in the job container which includes push
credentials.
- Then it runs a script that might update the README (e.g. maybe based on other
things that have since been committed to the repo).
- Last, it attempts to stage any changes the script made to the README. If there
are any, then it will commit them with that generic commit message. Finally it
will push that commit to the main branch.
If this all sounds a bit untethered from a real-world example, then take a look
at how I use this exact pattern to [apply daily updates to my GitHub Profile
README](https://github.com/jbranchaud/jbranchaud/blob/main/.github/workflows/update-tils.yml)
based on the latest [TILs](https://github.com/jbranchaud/til) I have written.
@@ -0,0 +1,42 @@
# Filter By Truthy Values With Boolean Function
The `Boolean` function (not to be confused with the `Boolean` constructor)
evaluates any given value to its [boolean
coercion](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean#boolean_coercion).
```javascript
> Boolean(0)
false
> Boolean(1)
true
> Boolean(null)
false
> Boolean([])
true
```
One way that this can be put to use is as a _boolean identity function_ for
passing to other functions like `filter`.
```javascript
> [0, 1, "", [], "four", null, "six", undefined, 7].filter(Boolean)
[ 1, [], 'four', 'six', 7 ]
```
This filters out all the non-truthy values from a list.
Let's say I'm building a list of nav items that will be rendered to the UI for a
specific user. Based on permissions or feature flags, certain nav items may not
be available. Those "empty" entries can be filtered out in this way.
```javascript
nav_items = [
{ label: "Home", href: "/" },
isSystemAdmin && { label: "System", "/system" },
featureEnabled(user, "api") && { label: "API", "/api" },
].filter(Boolean)
```
If any of those conditional nav items evaluate to `false`, then they will be
filtered out. The resulting `nav_items` array is a clean list of actual nav
items I want to render.
@@ -0,0 +1,62 @@
# `npm run` Has Some Typo Aliases
The developers of the `npm` CLI know that sometimes we are trying to run
commands in a hurry. It's easy to be trying to type `npm run` and instead type
`npm rum` or `npm urn`. No worries though, the command will still work.
If I run `npm help run`, I'll see a manpage that opens with the following:
```
NPM-RUN(1) NPM-RUN(1)
NAME
npm-run - Run arbitrary package scripts
Synopsis
npm run <command> [-- <args>]
aliases: run-script, rum, urn
...
```
Notice it lists a few _aliases_ including `rum` and `urn`.
Here are two examples of me running my test suite with `rum` and then `urn`.
```bash
npm rum test:run
> test:run
> vitest run
RUN v3.0.7 /Users/lastword/dev/jbranchaud/still
✓ app/javascript/utils/urlUtils.test.js (6 tests) 2ms
✓ app/javascript/utils/clipboardImage.test.js (20 tests) 3ms
Test Files 2 passed (2)
Tests 26 passed (26)
Start at 22:07:53
Duration 298ms (transform 17ms, setup 0ms, collect 22ms, tests 5ms, environment 270ms, prepare 63ms)
npm urn test:run
> test:run
> vitest run
RUN v3.0.7 /Users/lastword/dev/jbranchaud/still
✓ app/javascript/utils/urlUtils.test.js (6 tests) 3ms
✓ app/javascript/utils/clipboardImage.test.js (20 tests) 3ms
Test Files 2 passed (2)
Tests 26 passed (26)
Start at 22:07:58
Duration 305ms (transform 20ms, setup 0ms, collect 25ms, tests 6ms, environment 269ms, prepare 61ms)
```
See `npm help run` for more details.
+50
View File
@@ -0,0 +1,50 @@
# Include A File With Message To `ant`
The [Anthropic CLI tool](https://github.com/anthropics/anthropic-cli) (`ant`)
allows including files, such as images, directly in the message being sent to
the model. This is done with the `@` symbol followed by a relative path to the
image on the file system.
I have a file `parameterized-behavioral-tests.png` in my current directory that
I'd like to include. In the `content` array of the message I include an object
of type `image` along with some `source` metadata. Within `source` the image
file is referenced with the `data` field.
With that set, I then include a `text` object with my prompt asking for alt text
of this image.
```bash
ant messages create \
--model claude-opus-5 \
--max-tokens 1024 \
--message '{role: user, content: [
{type: image, source: {type: base64, media_type: image/png, data: "@./parameterized-behavioral-tests.png"}},
{type: text, text: "Produce a concise, but descriptive alt text for this image."}
]}' \
--transform 'content.#(type=="text").text' --raw-output
A hand-drawn style diagram comparing two testing approaches, split by a vertical
line.
On the left, under the heading "Behavioral Tests," is a single column of six
rounded rectangular test rows: four outlined in green with green check-mark
icons (passing) and two outlined in red with red X icons (failing), each
containing black scribble lines representing text.
On the right, under the heading "Parameterized Behavioral Tests," a bracketed
list of three purple parameter symbols — a circle, a triangle, and a diamond —
sits at the top, with arrows pointing down to three separate columns of six test
rows each. Every row in a column is tagged with its corresponding parameter
shape on the right edge. The circle column shows all six rows passing (green
with check marks). The triangle column shows three passing and three failing
(red with X marks). The diamond column shows five passing and one failing. The
illustration conveys that a single behavioral test, when parameterized, expands
into multiple variants whose pass/fail outcomes can differ per parameter.
```
This is for [the image toward the top of this post on parameterized tests in
pytest](https://www.visualmode.dev/parameterize-a-fixture-instead-of-a-test-case-with-pytest).
The description is a bit wordier than I would have liked, but it is spot on. It
is both visually descriptive and conveys what is being conceptually illustrated.
This whole thing was 3800 input tokens, 328 output tokens, and cost ~$0.02.
@@ -0,0 +1,45 @@
# Convert Arbitrary Number To Probability With Sigmoid
A sigmoid function is a useful function in statistics and machine learning for
converting a number in the range of positive and negative real numbers into a
value between 0 and 1. Sigmoid functions can be a bit more diverse than this,
but this is a good basic definition.
Wikipedia defines another characteristic of sigmoid functions:
> A sigmoid function is any mathematical function whose graph has a
> characteristic S-shaped or sigmoid curve.
This S-shape is because it is asymptotic at the ends allowing it to cover all
real numbers in either direction.
A common sigmoid function and the one used by [PyTorch's `Sigmoid`](https://docs.pytorch.org/docs/2.13/generated/torch.nn.Sigmoid.html)
is this exponential form -- `σ(x) = 1 / (1 + exp(-x))`.
Here is what this looks like plotted on a graph:
![sigmoid function plotted on a graph](https://cdn.visualmode.dev/images/3cadb482-matplot-sigmoid-function-graph.png)
This function can be used any time we want to convert an arbitrary number into a
probability. Large negative numbers will approach 0. Large positive numbers will
approach 1. Numbers near 0 will settle somewhere in the middle.
Here are a few examples run through PyTorch's `sigmoid` function:
```python
print("σ(-99) => ", torch.sigmoid(torch.tensor(-99.0)))
print("σ(99) => ", torch.sigmoid(torch.tensor(99.0)))
print("σ(0.123) => ", torch.sigmoid(torch.tensor(0.123)))
print("σ(-2) => ", torch.sigmoid(torch.tensor(-2.0)))
print("σ(1) => ", torch.sigmoid(torch.tensor(1.0)))
```
which prints out:
```
σ(-99) => tensor(0.)
σ(99) => tensor(1.)
σ(0.123) => tensor(0.5307)
σ(-2) => tensor(0.1192)
σ(1) => tensor(0.7311)
```
@@ -0,0 +1,96 @@
# Sequence Side-Effect When Rolling Back Inserts
What is so powerful about the transaction guarantees of a system like PostgreSQL
is that I can make all kinds of changes (big or small) within a transaction,
then roll it back, and it's like nothing ever happened.
This is useful if I want to dry-run an crucial update statement in the
production environment. I can gauge how many rows I expect to be impacted, run
the update in a transaction, inspect the state of the table, and rollback if
needed. It's like the changes never happened.
Rolled back transactions aren't completely free of side-effects. They can leave
a trace. In this case, that trace is increments to a sequence like you'd find
attached to a primary key. Each `insert` of a row into a table with an
autoincrementing sequence will have an effect on that sequence.
First, I'll create a fresh table and check the state of the sequence.
```sql
> create table books (id bigserial primary key, title text not null, author text not null);
CREATE TABLE
> select * from books_id_seq;
last_value log_cnt is_called
1 0 f
(1 row)
```
Now, I'll start a transaction, do some inserts, and again check the state of the
sequence.
```sql
> begin;
BEGIN
> insert into books (title, author) values ('Python in a Nutshell', 'Martelli et al'), ('Build an LLM', 'Sebastian Raschka'), ('The Joy of Abstraction', 'Eugenia Cheng');
INSERT 0 3
> select * from books;
id title author
1 Python in a Nutshell Martelli et al
2 Build an LLM Sebastian Raschka
3 The Joy of Abstraction Eugenia Cheng
(3 rows)
> select * from books_id_seq;
last_value log_cnt is_called
3 30 t
(1 row)
```
I will now rollback the transaction. I expect `books` to be empty again. But
what happens to the state of `books_id_seq`?
```sql
> rollback;
ROLLBACK
> select * from books;
id title author
(0 rows)
> select * from books_id_seq;
last_value log_cnt is_called
3 30 t
(1 row)
```
Despite rolling back, the state of `books_id_seq` remains with a `last_value` of
`3`. This is because a sequence is used outside of transaction isolation.
Multiple concurrent transactions may both need sequence values. Instead of them
blocking on each other, the sequence can always be accessed. The tradeoff is
that the `last_value` of a sequence is always marching forward and gaps will
happen when transactions are rolled back.
This is normal and expected behavior. It is something to be aware of if you have
a high-volume system that is doing a ton of inserting and rolling back (I've
worked on systems that do this). However, the solution there is more to do with
picking `bigint` (`bigserial`) for your sequence-backed column. That would
reasonably eliminate concern for running out of sequence values. Or use UUIDs
instead.
@@ -0,0 +1,61 @@
# Annotate Return Type On Enum Function
I defined the following `StrEnum` class to capture the canonical set of valid
values for a _storage format_ in
[`py-vmt`](https://github.com/jbranchaud/py-vmt). It includes a `default`
function that can be called to get what the system considers the default storage
format.
```python
class StorageFormat(StrEnum):
SQLITE = "sqlite"
JSON = "json"
@staticmethod
def default():
return StorageFormat.SQLITE
```
This works, but what is missing is a type annotation for the return value of
`default`.
I'd like to use `StorageFormat` as the return type annotation. However, the
annotations are eagerly evaluated at the time the function definition is being
processed. The `StorageFormat` class is not fully evaluated at that point and so
isn't available.
Instead, what I can do is quote `StorageFormat` as a string to make it a
_forward reference_.
```python
class StorageFormat(StrEnum):
SQLITE = "sqlite"
JSON = "json"
@staticmethod
def default() -> "StorageFormat":
return StorageFormat.SQLITE
```
The string type annotation will be lazily evaluated at a later time when
`StorageFormat` fully exists. This is behavior that is slated to be deprecated.
And it is no longer needed as of [Python 3.14 which lazily evaluates all
annotations by default
now](https://docs.python.org/3/reference/compound_stmts.html#annotations).
If I want to be more specific with the above annotation, I can import [`Literal`
from `typing`](https://docs.python.org/3/library/typing.html#typing.Literal) and
then wrap a specific enum value.
```python
class StorageFormat(StrEnum):
SQLITE = "sqlite"
JSON = "json"
@staticmethod
def default() -> "Literal[StorageFormat.SQLITE]":
return StorageFormat.SQLITE
```
Now it will specifically enforce `SQLITE` rather than accepting `SQLITE` or
`JSON`.
@@ -0,0 +1,47 @@
# Another Way To Mark Keyword-Only Dataclass Fields
In [Configure Other Attributes Of Dataclass
Field](configure-other-attributes-of-dataclass-field), I showed how the
[`dataclasses.field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.field)
constructor function can be used. One of the parameters I demonstrated was
`kw_only`. Each field constructed with `kw_only=True` will be required to be
passed as a keyword-only parameter when constructing an instance of that
`dataclass`.
Another way to specify keyword-only parameters with `dataclass` fields is to
segment them with `KW_ONLY`. This sentinel value can be included as a
pseudo-field where all fields that come after it are treated as keyword-only.
Translating the example from that other post would look like this:
```python
from dataclasses import dataclass, field, KW_ONLY
from datetime import datetime
@dataclass
class Session:
start_time: datetime
project_name: str
_: KW_ONLY
tags: list[str] = field(default_factory=list, kw_only=True)
end_time: datetime | None = None
# ...
sesh1 = Session(start1, "my-project", tags=["pytorch", "numpy"])
sesh2 = Session(start2, "other-project", end_time=datetime.now())
```
The field whose value is `KW_ONLY` is only used to signal that keyword-only
boundary. It does not itself become a field of the dataclass.
On the one hand I like this approach because it feels closer to [the way this is
signaled in standard function definition
syntax](force-remaining-arguments-to-be-named.md).
```python
def build_session(start_time, project_name, *, tags, end_time=None)
```
On the other hand, it feels like magic `dataclass` syntax whereas the
`kw_only=True` parameter is more explicit.
@@ -0,0 +1,54 @@
# Combine StrEnum With Pydantic For Union Type
I want a [Pydantic](https://pydantic.dev/docs/validation/latest/get-started/)
model that I can use to verify the shape and values of user-provided config
file. Let's say, for now, the only config value supported by the file is
`storage_format` and it can be one of three values (`json`, `sqlite`,
`inmemory`).
I can start with a
[`StrEnum`](https://docs.python.org/3/library/enum.html#enum.StrEnum) class to
represent the set of valid values for the storage format field.
```python
from enum import StrEnum
from typing import Literal
class StorageFormat(StrEnum):
SQLITE = "sqlite"
JSON = "json"
INMEMORY = "inmemory"
@classmethod
def default(cls) -> Literal[StorageFormat.SQLITE]:
return cls.SQLITE
```
Notice I've also provided a class method called `default` for serving up a
fallback default value.
Here is the corresponding Pydantic `CliConfig` model which uses the
`StorageFormat` string enum class as the type signature for its `storage_format`
field.
```python
from pydantic import BaseModel
class CliConfig(BaseModel):
storage_format: StorageFormat = StorageFormat.default()
```
For `storage_format` to be valid in the context of `CliConfig`, it must be one
of the values defined by that `StrEnum`. I get auto-complete, I get type
checking, and I can validate a config file I've just read into memory (in this
case, using
[`model_validate_json`](https://pydantic.dev/docs/validation/dev/api/pydantic/base_model/#pydantic.BaseModel.model_validate_json)).
```python
def _read_config(self) -> CliConfig:
if self.exists():
raw_json = self.config_file.read_text()
return CliConfig.model_validate_json(raw_json)
return CliConfig()
```
@@ -0,0 +1,63 @@
# Configure Other Attributes Of Dataclass Field
I have a basic [`dataclass`](https://docs.python.org/3/library/dataclasses.html)
defined with a few fields that looks like this:
```python
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Session:
start_time: datetime
project_name: str
tags: list[str]
end_time: datetime | None = None
# ...
new_session = Session(start_time, "my-project", [])
```
This syntax lets me define the fields that make up arguments to the underlying
`__init__` function. This only gets me so far though. What if I want `tags` to
have a default value of `[]`? What if I want to force those optional fields
(`tags` and `end_time`) to be keyword-only arguments?
To take the field definitions of my `dataclass` a bit further, I need the
[`field` function](https://docs.python.org/3/library/dataclasses.html#dataclasses.field)
which lets me configure more aspects of each field.
```python
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class Session:
start_time: datetime
project_name: str
tags: list[str] = field(default_factory=list, kw_only=True)
end_time: datetime | None = field(default=None, kw_only=True)
# ...
sesh1 = Session(start1, "my-project", tags=["pytorch", "numpy"])
sesh2 = Session(start2, "other-project", end_time=datetime.now())
```
If I were to say the default value of `tags` is simply a `[]`, then the
`dataclass` definition gets evaluated with a reference to that specific instance
of an empty list. Every new `Session` I create would share and mutate that same
list. For this reason, `dataclass` detects and warns you away from that.
Instead, I specify `list` as the `default_factory` for `tags`. Each new
`Session` where `tags` is not provided will be factory'd a new empty list.
For scalar values, like `None`, I can use the `default` parameter instead like
I've done for `end_time`.
As for marking certain fields as keyword-only, the `kw_only` parameter can be
overridden with `True`.
See [the docs for `field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.field)
for all the other configurable options.
@@ -0,0 +1,46 @@
# Dedent Common Whitespace From Multiline String
I'm adding tests to cover some custom logic in the _usage_ messages that are
produced by Click [in a CLI I'm building](https://github.com/jbranchaud/py-vmt).
The usage output is a mutliline string formatted with some indentation. On top
of that, I'm putting it in a Python test file with some additional syntactical
indentation.
If this was Ruby, I would reach for the `<<~` heredoc syntax. The closest
equivalent to that in Python is
[`textwrap.dedent`](https://docs.python.org/3/library/textwrap.html#textwrap.dedent).
> Remove any common leading whitespace from every line in text.
>
> This can be used to make triple-quoted strings line up with the left edge of
> the display, while still presenting them in the source code in indented form.
```python
import textwrap
def test_base_help_message():
runner = BetterCliRunner()
help_result = runner.invoke(cli, ["--help"])
output = textwrap.dedent("""\
Usage: vmt [OPTIONS] COMMAND [ARGS]...
Options:
-v, --verbose See extra output when running commands
--help Show this message and exit.
Commands:
cancel
log
start
status
stop
""")
assert output in help_result.output
```
Before adding in `textwrap.dedent`, the test was failing because of bits of
extra whitespace that wasn't matching up with the actual CLI output.
Note: the leading `\` after the opening `"""` is needed to remove the leading
newline that would otherwise be included.
@@ -0,0 +1,47 @@
# Escape Curly Braces Within Formatted String
Python's
[_f-string_](https://docs.python.org/3/reference/lexical_analysis.html#f-strings)
allows you to interpolate values into a string literal like so:
```python
>>> amount = 25
>>> print(f"Price: ${amount}")
Price: $25
```
The `{` and `}` are betting special treatment when `f` precedes the string
literal. So, what if the resulting string is supposed to have literal `{` and
`}` characters in it?
```python
>>> f"JSON opens and closes with curlies: {...}"
'JSON opens and closes with curlies: Ellipsis'
```
I need a way of escaping them. The trick is to double them up.
```python
>>> f"JSON opens and closes with curlies: {{...}}"
'JSON opens and closes with curlies: {...}'
```
A more practical example of where this is useful is in formatting an expected
string of JSON in a test:
```python
info_result = runner.invoke(cli, ["info", "--json"])
output = textwrap.dedent(f"""
{{
"config_file": "{config_file}",
"data_dir": "{data_dir}",
"version": "{VMT_VERSION}",
"schema_version": 1
}}
""").strip()
assert output in info_result.output
```
I'm doing some interpolation at various points in the string literal, but the
outer curly braces are meant to be literal, the opening and closing of the JSON
object.
@@ -0,0 +1,44 @@
# Experiment With SQLite Queries In Memory
Instead of connecting
[`sqlite3`](https://docs.python.org/3/library/sqlite3.html) to a real file
(database) like so:
```python
db_file: Path = data_dir / "sessions.db"
conn: Connection = sqlite3.connect(db_file)
```
I can point it to `":memory:"`:
```python
conn: Connection = sqlite3.connect(":memory:")
```
One way this can be useful is when experimenting with some DDL (schema-modifying
SQL statements), e.g. creating a table, adding a column, and so forth.
```python
>>> import sqlite3
>>> conn = sqlite3.connect(":memory:")
>>> conn.execute("""
... create table projects (
... id integer primary key,
... name text not null unique,
... created_at text not null default (datetime('now')),
... updated_at text not null default (datetime('now'))
... );
... """)
<sqlite3.Cursor object at 0x105d6e2c0>
>>> conn.execute("insert into projects (name) values ('TIL'), ('py-vmt'), ('Pool League Pro');")
<sqlite3.Cursor object at 0x105d6e340>
>>> result = conn.execute("select * from projects;")
>>> rows = result.fetchall()
>>> rows
[(1, 'TIL', '2026-08-01 23:59:56', '2026-08-01 23:59:56'), (2, 'py-vmt', '2026-08-01 23:59:56', '2026-08-01 23:59:56'), (3, 'Pool League Pro', '2026-08-01 23:59:56', '2026-08-01 23:59:56')]
```
This does not create any sort of on-disk version of the database. Two separate
connections created this way will be independent in-memory database instances.
This is technically more of [a SQLite feature](https://sqlite.org/inmemorydb.html) than a Python one, but it was in a
Python context that I found this most useful.
@@ -0,0 +1,44 @@
# Generate Sample PDFs With ReportLab
The `reportlab` package has a `pdfgen` module that provides a `Canvas` API for
programmatically constructing a PDF.
I recently needed the most basic use of this to quickly construct a sample PDF
that I could use as I experimented with [different `qpdf`
commands](/workflow/remove-pages-from-a-pdf.md). I wanted to generate a 10-page
PDF where each page included a large font number in the center. This way I could
clearly see what was the first, second, third, and so on pages. Then I could
quickly verify that the `qpdf` manipulations I was doing were working as
expected.
I was able to do this without any setup or install. Using a `uv` one-liner (does
a 10-line heredoc count as a one-liner?), I generated a PDF with some basic
canvas rendering on each page.
```python
uv run --with reportlab python - <<'EOF'
from reportlab.pdfgen import canvas
N, W, H = 10, 612, 792
c = canvas.Canvas("test.pdf", pagesize=(W, H))
for i in range(1, N + 1):
c.setFont("Helvetica-Bold", 220)
c.drawCentredString(W / 2, H / 2 - 80, str(i))
c.setFont("Helvetica", 28)
c.drawCentredString(W / 2, 100, f"page {i} of {N}")
c.showPage()
c.save()
EOF
```
The `--with` makes the `reportlab` package available to the command being run.
The command being run is `python` which evaluates that block of heredoc text.
Within the heredoc, I import `canvas` from the `reportlab` package that has been
made available. I then create an in-memory PDF of a standard size. Then for-loop
10 times to build each page of the PDF with a big number in the middle and a
smaller `page x of N` lower down on the page. Finally, I save the PDF which
writes it to the directory I executed this whole command from.
I had Claude help with the `canvas` API, however there are some decent
[`reportlab.pdfgen` docs
here](https://docs.reportlab.com/reportlab/userguide/ch2_graphics/).
+81
View File
@@ -0,0 +1,81 @@
# Initialize A PyTorch Tensor
A _tensor_ is an n-dimensional array that takes on a specified shape and holds
scalars, typically floats, in each of its positions.
[PyTorch](https://pytorch.org/) has a comprehensive suite of tools for working
with tensors. To work with tenors, the first thing I need to do is initialize
one. Here are a handful of ways to create a 2-dimensional tensor (a matrix)
depending on various needs.
Here is a 3x4 matrix full of zeros:
```python
>>> torch.zeros(torch.Size((3, 4)))
tensor([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])
```
Here is a 3x4 matrix full of ones:
```python
>>> torch.ones(3,4)
tensor([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
```
And here is a 3x4 matrix full of a specific other value:
```python
>>> torch.full((3,4), 13.0)
tensor([[13., 13., 13., 13.],
[13., 13., 13., 13.],
[13., 13., 13., 13.]])
```
PyTorch is very flexible. I can specify the shape of the `tensor` with
positional arguments or a tuple for the dimensions. I can even construct a
`torch.Size` object.
Here is an arguably more useful example where the matrix is seeded with random
values in the range `[0,1)` using
[`torch.rand`](https://docs.pytorch.org/docs/2.13/generated/torch.rand.html):
```python
>>> torch.rand(torch.Size((3,4)))
tensor([[0.4148, 0.8045, 0.3093, 0.3363],
[0.0120, 0.7161, 0.1108, 0.5510],
[0.4805, 0.9430, 0.2852, 0.0966]])
```
These could be used as starting weights in a training process that then get
tweaked over time.
There are other random tensor functions like
[`torch.randint`](https://docs.pytorch.org/docs/2.13/generated/torch.randint.html)
and
[`torch.randn`](https://docs.pytorch.org/docs/2.13/generated/torch.randn.html).
How about a [random permutation](https://docs.pytorch.org/docs/2.13/generated/torch.randperm.html)
of integers `[0,12)` reshaped into a 3x4 matrix:
```python
>>> torch.randperm(12).reshape(3,4)
tensor([[ 0, 3, 1, 11],
[ 7, 8, 9, 4],
[10, 5, 6, 2]])
```
And though there are many other ways to initialize a tensor, the last one I will
show is
[`torch.eye`](https://docs.pytorch.org/docs/2.13/generated/torch.eye.html) which
creates a matrix with ones down the diagonal.
```python
>>> torch.eye(4)
tensor([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
```
+30
View File
@@ -0,0 +1,30 @@
# Join A List Of Strings
Though joining a list of strings in Python is a basic task, I wanted to write
about it because it is backward from how it is done in Ruby (which trips me up
every single time).
So, in Ruby I would do the following:
```ruby
> character = ["Gimli", "Dwarf", "Fighter", "Lvl 23"]
=> ["Gimli", "Dwarf", "Fighter", "Lvl 23"]
> character.join(" ~ ")
=> "Gimli ~ Dwarf ~ Fighter ~ Lvl 23"
```
Notice that I call
[`join`](https://docs.ruby-lang.org/en/master/Array.html#method-i-join) on the
list of strings, passing it the specific separator that I want to use.
Python does it the other way around:
```python
>>> character = ["Gimli", "Dwarf", "Fighter", "Lvl 23"]
>>> " ~ ".join(character)
'Gimli ~ Dwarf ~ Fighter ~ Lvl 23'
```
The separator is the object that I call
[`join`](https://docs.python.org/3/library/stdtypes.html#str.join) on, passing
it the list of strings that I want to join.
@@ -0,0 +1,61 @@
# Override Timezone That All Tests Run In
I was building out the initial functionality of a Python project in my local dev
env. I was writing tests along the way to guard against regressions. Many of the
tests assert things about time, durations, and formatted timestamps. All was
well.
Then I was getting the project setup on GitHub with a CI workflow to run the
test suite. And a ton of tests were failing. It was all the tests dealing with
time. They were all off by 5 hours.
I recognized that time difference immediately. That's the difference between my
time here in Chicago and UTC. When I run the tests locally, they are all
formatted the timezone setting on my machine -- `America/Chicago`. Whereas when
I run them in this CI environment, they were suddenly being formatted to `UTC`.
What I needed to do was explicitly set the timezone that all the tests would
always run in, regardless of environment.
I added the following to my `conftest.py` file:
```python
import os
import time
# Set the timezone that all tests run in
os.environ["TZ"] = "America/Chicago"
time.tzset()
```
First, I set the timezone (`TZ`) in `os.environ` to `America/Chicago`. That's a
copy of the executing environment that Python makes when it starts up.
Second, because `TZ` may be tied to some cached info under the hood, I have to
also run `time.tzset()`.
> Initialize, or reinitialize, the local timezone to the value stored in
> os.environ[TZ]. The TZ environment variable should be specified in standard
> Unix timezone format as documented in the tzset man page (eg. US/Eastern,
> Europe/Amsterdam).
Now, all of my tests run reliably in `America/Chicago`.
To demonstrate all of this from the Python repl, here is an example:
```python
>>> from datetime import datetime
>>> import os
>>> import time
>>> os.environ["TZ"] = "America/Los_Angeles"
>>> time.tzset()
>>> datetime.now()
datetime.datetime(2026, 8, 15, 14, 3, 19, 373682)
>>> os.environ["TZ"] = "America/Chicago"
>>> time.tzset()
>>> datetime.now()
datetime.datetime(2026, 8, 15, 16, 3, 33, 322803)
```
Notice the two hour difference between those two.
@@ -0,0 +1,35 @@
# Publish A Package To A Test Env As A Dry Run
As I was preparing to register a new Python package with PyPI and release my
first version cut, I felt like there were a lot of unknowns. Is my package's
name going to be valid? How will the package appear in PyPI? Did I configure
everything correctly? Etc.
It turns out that PyPI has a great way of answering a lot of these questions.
There is a [`test.pypi.org`](https://test.pypi.org/) site that parrots the
publishing flow of `pypi.org`. This makes for a great target to do a dry-run
publishing of a package.
First, I had to go through the same registration flow and 2FA setup as when I
registered with `pypi.org`.
Second, I deviated from my tag and CI-triggered publishing flow by instead doing
a one-off run of the `uv publish` command. That requires an API token which I
generated in the web UI for `test.pypi.org`. I added that to my env as
`TEST_PYPI_TOKEN`.
I then ran the following command:
```bash
uv publish --publish-url https://test.pypi.org/legacy/ --token "$TEST_PYPI_TOKEN"
```
When this first ran for [`py-vmt`](https://github.com/jbranchaud/py-vmt), I got
an error back from the publishing API telling me the package name is too similar
to an existing package. I then had to make a few updates across the project to
rename the published package name to `visualmode-tracker`. Running the `uv
publish` command again worked with the updated name.
I was then able to go into the web UI and verify everything looked as expected.
I now have the confidence to publish this thing for real to `pypi.org`.
@@ -0,0 +1,93 @@
# Register SQLite Adapter To Serialize Datetimes
SQLite doesn't have a datetime or timestamp data type, so that information has
to be stored as `text` or a unix epoch `int`. That means when using Python's
`sqlite3` module to perform writes I need to tell it what shape to write a
datetime value.
I could manually convert `datetime` values everywhere they are involved in
_write_ operations.
```python
# Prepare `sessions` insert payload
session_data = {
"active": 1 if active else 0,
"project_id": project_id,
"start_time": datetime.isoformat(session.start_time),
"end_time": None,
}
if session.end_time:
session_data["end_time"] = datetime.isoformat(session.end_time)
# Insert the new active session
cursor = self.conn.execute(
"""
insert into sessions (active, project_id, start_time, end_time)
values (:active, :project_id, :start_time, :end_time)
returning id;
""",
session_data,
)
```
I've used `datetime.isoformat` above which formats `datetime` objects like so:
```python
>>> datetime.now().isoformat()
'2026-08-28T11:52:04.709907'
```
I'd like to make two improvements.
1. I want these `datetime` values to be formatted instead like `2026-08-28T18:15:27.213Z`.
2. I want `datetime` values to be serialized automatically in the specific shape
without having to manually convert them everywhere.
I can achieve both of these things by [registering an adapter with `sqlite3`](https://docs.python.org/3/library/sqlite3.html#how-to-register-adapter-callables)
that handles the serialize of `datetime` objects.
First, I define a function that can perform the `datetime` to `str` conversion.
I decided to put this in `db.py` where I have some other database-specific
functions.
```python
from datetime import datetime, timezone
def to_db(dt: datetime) -> str:
if dt.tzinfo is None or dt.utcoffset() is None:
raise ValueError(f"Unable to store naive datetime: {dt!r}")
dt = dt.astimezone(timezone.utc)
return f"{dt:%Y-%m-%dT%H:%M:%S}.{dt.microsecond // 1000:03d}Z"
```
Then I register the adapter before creating the connection that gets used for
database interactions.
```python
import sqlite3
from datetime import datetime
from pathlib import Path
from sqlite3 import Connection
def initialize_conn(db_file: Path) -> Connection:
# register adapters
sqlite3.register_adapter(datetime, to_db)
conn: Connection = sqlite3.connect(db_file)
conn.row_factory = sqlite3.Row
return conn
```
Then I can run write operations with `datetime` objects knowing they will be
correctly serialized.
```python
with self.conn:
query = "update sessions set active = :active, end_time = :end_time where active = 1;"
self.conn.execute(
query,
{"active": 0, "end_time": session.end_time},
)
```
@@ -0,0 +1,55 @@
# Specify Parameter Name For Click Option
[Click](https://click.palletsprojects.com/en/stable/)'s option decorator
provides a versatile way to define flags for a command. It has good defaults
that minimize the aspects of a flag that I need to be explicit about.
For example, a boolean `--init` flag for the `config` command could be specified
like so:
```python
@cli.command()
@click.option(
"--init",
help="Initialize a config file with minimal defaults",
is_flag=True,
)
@pass_cli
def config(cli_ctx: CliContext, init: bool):
# ...
```
Notice, in particular, that the flag string (`--init`) that I pass as the first
argument to `@click.option` has to correspond to the name of the parameter
`init`. Click passes all the defined options as keyword arguments when invoking
`config`. If the `init` parameter was changed to `initial`, there would be a
runtime error like this: `TypeError: config() got an unexpected keyword argument 'init'`.
Like I said though, Click is flexible when I need it to be. I can leave the flag
name as it is, but specify a different name to be used for the function
parameter. I found this useful when I realized that as I added support for a
`--json` flag I was inadvertently superseding the `json` import.
The second positional argument to `@click.option` can be included to rename that
parameter:
```python
import json
@cli.command()
@click.option(
"--json",
"use_json",
help="Output all info details in JSON format",
is_flag=True,
)
@pass_cli
def info(cli_ctx: CliContext, use_json: bool):
# ...
if use_json:
click.echo(json.dumps(info_details, indent=2))
```
Both of these code blocks are excerpts from my [`py-vmt` time tracker project](https://github.com/jbranchaud/py-vmt).
@@ -0,0 +1,32 @@
# Start Jupyter Notebook With Extra Packages
I can start up a one-off [Jupyter notebook](https://jupyter.org/) with
[`uv`](https://docs.astral.sh/uv/) with the following command:
```bash
uv run --with jupyter jupyter lab
```
The `--with` flag is a `uv run` feature for [requesting additional
dependencies](https://docs.astral.sh/uv/concepts/projects/run/#requesting-additional-dependencies).
In this case, it includes the `jupyter` package as a dependency which is
necessary for the command to then run `jupyter lab`.
As I started following along with [Andrej Karpathy's "Neural Networks: Zero to
Hero" series](https://karpathy.ai/zero-to-hero.html), I quickly ran into an
issue with missing other dependencies. In the opening video, Andrej is importing
`numpy` and `matplotlib`. To ensure those packages are also available for import
in this notebook, I need to `--with` them as well.
```bash
uv run --with jupyter --with numpy --with matplotlib jupyter lab
```
Now, when I execute the following step, I don't have any import issues:
```python
import math
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```
@@ -0,0 +1,38 @@
# Handle Bad Numerical Amounts With BigDecimal
I'm working on a payment page with a backing Rails controller. The user can
select between their full balance or some partial payment amount. Because this
form accepts an arbitrary value for the amount, I need to do some server-side
validation.
While I could parse the `amount` value and handle the exception that gets raised
on bad numerical values, I'd rather have bad values coerce to `nil` and let
downstream validations handle it from there.
[`BigDecimal`](https://docs.ruby-lang.org/en/master/BigDecimal.html) can help
here with the support of its `exception` option.
```ruby
> BigDecimal('123', exception: false)
=> 0.123e3
> BigDecimal('taco', exception: false)
=> nil
```
Maybe that parsing logic ends up looking something like this:
```ruby
def parse_payment_amount(value, current_balance)
amount = BigDecimal(value, exception: false)
if amount.present? && amount == current_balance
[:full_balance, amount]
else
[:other, amount]
end
end
```
I return tuples where you either get a verified `:full_balance` amount or you
get some `:other` amount. The other amount could be `nil` which would trigger a
downstream validation.
+51
View File
@@ -0,0 +1,51 @@
# Remove Blank Values From A Hash
Ruby's [`Enumerable`](https://docs.ruby-lang.org/en/master/Enumerable.html) has
a method `#compact` that will remove `nil` values from `Enumerable` objects like
hashes and arrays.
```ruby
> { one: nil, two: 2, three: "" }.compact
=> {two: 2, three: ""}
> [nil, 2, ""].compact
=> [2, ""]
```
That is often what I want because I'm thinking in terms of values being either
`nil` or valid value.
Sometimes I want to remove all _blank_ values, not just the `nil` ones. This
happens in a Rails context like in a controller when dealing with parameters
from a request -- a form field was left blank (`""`) or no options were picked
from the multi-select (`[]`).
Rails adds
[`#compact_blank`](https://api.rubyonrails.org/classes/Enumerable.html#method-i-compact_blank)
to `Enumerable` to support these cases. Consider that `#compact` corresponds to
`#nil?` where as `#compact_blank` corresponds to `#blank?`.
Here is the example hash pulled from the docs:
```ruby
> { a: "", b: 1, c: nil, d: [], e: false, f: true }.compact
=> {a: "", b: 1, d: [], e: false, f: true}
> { a: "", b: 1, c: nil, d: [], e: false, f: true }.compact_blank
=> {b: 1, f: true}
```
I might want to `compact_blank` a set of search parameters coming from the
client before passing it into my search service:
```ruby
class BooksController < BaseController
def search
@results = SearchService.call(search_params.compact_blank)
end
private
def search_params
params.permit(...)
end
end
```
+52
View File
@@ -0,0 +1,52 @@
# Override Output Field Separator
Have you ever seen `$,` as a specific piece of syntax before? I've never
actually seen any Ruby code that uses this in the wild. It's called the _output
field separator_. By default it is `nil`. Like other keywords prefixed with `$`
it is a global variable.
It can be used to control what gets used by default as a separator in variety of
contexts, like when calling `#join` without any argument.
```ruby
> [1,2,3].join
=> "123"
> $,
=> nil
> $, = '~'
=> "~"
> [1,2,3].join
=> "1~2~3"
```
It is also what gets used when calling `print` with multiple arguments:
```ruby
> $, = ' |v| '
=> " |v| "
> print "hello", "world"
hello |v| world=> nil
> $, = "\t"
=> "\t"
> print "hello", "world"
hello world=> nil
```
This seems pretty esoteric so far, but might be useful in a one-liner where
we're doing something like parsing data from a CSV:
```ruby
# awk-style: pull fields 1 and 3 from a CSV, emit them tab-separated
ruby -F, -ane 'BEGIN { $, = "\t"; $\ = "\n" }; print $F[0], $F[2]' data.csv
```
My attention was originally drawn to `$,` by a [post from Chris
Oliver](https://lnkd.in/p/g6YH5KvQ) which showed this method definition from
Rails source:
```ruby
def safe_join(array, sep = $,)
```
Note that this is [now
deprecated](https://docs.ruby-lang.org/en/master/language/globals_md.html#deprecated).
@@ -0,0 +1,74 @@
# Add Check Constraint To Existing Column
To not bury the lede, SQLite does not support adding a check constraint to an
existing column. Instead, if I want to add one after the fact, I can reconstruct
the table with the check constraint specified up front and then migrate the
data.
Here is a `sessions` table that includes `start_time` and `end_time` columns
that use the `text` data type to represent points in time.
```sql
sqlite> .schema sessions
CREATE TABLE sessions (
id integer primary key,
active integer not null check (active in (0, 1)),
project_id integer not null references projects(id) on delete cascade,
start_time text not null,
end_time text,
created_at text not null default (datetime('now')),
updated_at text not null default (datetime('now'))
);
CREATE UNIQUE INDEX idx_sessions_single_active
on sessions(active)
where active = 1;
```
I want `start_time` and `end_time` to both enforce the shape of the timestamp
strings with `check` constraints. To do that, I need to start a transaction,
create a new version of the table with the check constraints, migrate the data,
rename `sessions` to `sessions_old`, rename `sessions_new` to `sessions` (that's
the in-place swap), and then commit the transaction. `sessions_old` can be
dropped later once I feel good about the migration.
```sql
begin transaction;
create table sessions_new (
id integer primary key,
active integer not null check (active in (0, 1)),
project_id integer not null references projects(id) on delete cascade,
start_time text not null,
end_time text,
created_at text not null default (datetime('now')),
updated_at text not null default (datetime('now')),
check(start_time is strftime('%Y-%m-%dT%H:%M:%fZ', start_time)),
check(end_time is strftime('%Y-%m-%dT%H:%M:%fZ', end_time))
);
insert into sessions_new (
id, active, project_id, start_time, end_time, created_at, updated_at
)
select id, active, project_id, start_time, end_time, created_at, updated_at
from sessions;
alter table sessions rename to sessions_old;
alter table sessions_new rename to sessions;
drop index if exists idx_sessions_single_active; -- on sessions_old
create unique index idx_sessions_single_active
on sessions(active)
where active = 1;
commit;
```
Notice that after the table renames I also drop index (now pointing to
`sessions_old`) and recreate it for the _new_ `sessions` table.
This approach worked well for my situation, but may not be a one-size-fits-all
solution. Depending on how the database is deployed, the size of the tables, and
usage, this approach may not scale. Always do a dry-run of database migrations
like this.
@@ -0,0 +1,62 @@
# Manage Lightweight Schema Migrations With `user_version`
The [`user_version` pragma](https://sqlite.org/pragma.html#pragma_user_version)
in SQLite is a bit of persistent state built into the header of a SQLite
database file.
> The user_version pragma will get or set the value of the user-version integer
> at offset 60 in the database header. The user-version is an integer that is
> available to applications to use however they want. SQLite makes no use of the
> user-version itself.
One use case for `user_version` is as a schema version indicator. It starts at
`0` which can mean nothing has been migrated yet.
```sql
sqlite> pragma user_version;
0
```
Then for each individual migration that my application applies, I can increment
the value of `user_version`. At any point in the application lifecycle, it knows
up to what point schema migrations have been run. If the number of migrations is
greater than the value of `user_version`, then migrations need to be applied.
Here is some Python code (from
[`py-vmt`](https://github.com/jbranchaud/py-vmt/blob/e426b3fda92f59c116494a4a4ed992965bac1dba/src/py_vmt/db.py))
that uses this pattern:
```python
from sqlite3 import Connection
MIGRATIONS = [
# ...
]
# Using the `user_version` pragma in SQLite, this checks if there are any
# sets of statements in `MIGRATIONS` that have not been run yet. It then
# executes those and updates `user_version`.
def migrate(conn: Connection):
version = conn.execute("pragma user_version").fetchone()[0]
for i, statement in enumerate(MIGRATIONS[version:], start=version):
conn.executescript(statement)
conn.execute(f"pragma user_version = {i + 1}")
conn.commit()
```
Notice that `user_version` can be updated directly with an assignment:
```sql
sqlite> pragma user_version = 3;
sqlite> pragma user_version;
3
```
A `select` statement can also be used to access the `user_version` value like
so:
```sql
sqlite> select * from pragma_user_version;
3
```
@@ -0,0 +1,50 @@
# Output Query Result In Nicely Formatted Table
When I start a fresh SQLite connection and run a query, all the results are
squished together in a way that is poorly formatted, hard to read, and missing
column headers.
```sql
sqlite> select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3;
1|2026-07-26T21:15:50.062936+00:00|2026-07-26T21:53:13.990Z|taco
2|2026-07-26T21:53:40.019946+00:00|2026-07-26T22:14:09.168Z|TIL
3|2026-08-02T17:00:16.119169+00:00|2026-08-02T17:30:16.119Z|py-vmt
```
I can drastically improve the look of this by turning _headers_ on and switching
to _box_ mode.
```sql
sqlite> .headers on
sqlite> .mode box
sqlite> select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3;
id start_time end_time name
1 2026-07-26T21:15:50.062936+00:00 2026-07-26T21:53:13.990Z taco
2 2026-07-26T21:53:40.019946+00:00 2026-07-26T22:14:09.168Z TIL
3 2026-08-02T17:00:16.119169+00:00 2026-08-02T17:30:16.119Z py-vmt
```
I personally find that much easier on the eyes. It is also a nicer format to
copy and paste into a post like this or a formatted code block that I'm sharing
with a colleague.
I can also do this directly from the CLI with a one-liner using `-header` and
`-box` like so:
```bash
sqlite3 /Users/lastword/.local/share/vmt/sessions.db -header -box "select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3"
┌────┬──────────────────────────────────┬──────────────────────────┬────────┐
│ id │ start_time │ end_time │ name │
├────┼──────────────────────────────────┼──────────────────────────┼────────┤
1 │ 2026-07-26T21:15:50.062936+00:00 │ 2026-07-26T21:53:13.990Z │ taco │
2 │ 2026-07-26T21:53:40.019946+00:00 │ 2026-07-26T22:14:09.168Z │ TIL │
3 │ 2026-08-02T17:00:16.119169+00:00 │ 2026-08-02T17:30:16.119Z │ py-vmt │
└────┴──────────────────────────────────┴──────────────────────────┴────────┘
```
Run the `.help` dot-command from a SQLite prompt for a full listing of these
commands. See also `sqlite3 --help` from the CLI for usage details about all
flags.
@@ -0,0 +1,39 @@
# Connect To Individual Overmind Processes Via tmux
A common `Procfile.dev` in a Rails project might look something like this.
```
web: bin/rails server -p $PORT
vite: bin/vite dev
worker: bundle exec good_job start
```
Instead of starting up each process that needs to be running for development to
work, I can instead run a tool that reads the procfile and sets it all up for me
-- like `overmind`.
```bash
overmind start -f Procfile.dev
```
What's cool about `overmind` is that it starts its own `tmux` session and then
runs each of these processes in its own window.
I can connect to any one of them by name with `overmind connect <name>`. Or I
can connect to the session defaulting to the first window with just `overmind
connect`.
If I need to see what is going on with my background jobs, I'll run:
```bash
overmind connect worker
```
This behaves like any other tmux session, so I can use my prefix key (`ctrl-z`
in my case) to access tmux-specific keybindings. Most notably, once I'm done
looking, I'll want to hit `ctrl-z d` to detach from the session.
I'm already using tmux as my daily driver which means its easy for me to end up
in a nested tmux session if I connect while already in my development session.
To help with that, I set up [a forwarding
prefix](set-up-forwarding-prefix-for-nested-session.md).
+31
View File
@@ -0,0 +1,31 @@
# Check What Is Inside A Zip File
Before unzipping a file, it can be useful to check the contents. You can learn a
few things from doing this. First off, are all the files nested under some extra
top-level directory? How many files are in there? How big are certain files? Etc.
The `-l` flag to `unzip` can help answer these questions.
```bash
unzip -l zipped-files.zip
Archive: zipped-files.zip
Length Date Time Name
--------- ---------- ----- ----
0 08-22-2026 08:54 files-to-zip/
14 08-22-2026 08:54 files-to-zip/data.json
38 08-22-2026 08:53 files-to-zip/notes.md
14 08-22-2026 08:53 files-to-zip/hello.txt
--------- -------
66 4 files
```
Notice that the first line is for `files-to-zip/` which tells us that everything
is nested under the top-level directory that was probably the target of the
`zip` command. Everything is organized into a table with labeled columns which
allows us to answer our questions and get a good idea of what is in there.
In my experience, zip files tend to have dozens to hundreds of files, so it is
even better to pipe the above to a pager like `less`. That way you can search
and navigate through everything that's in there.
See `man unzip` for more details.
+28
View File
@@ -0,0 +1,28 @@
# Delete Empty Files With Find
I was discussing a command with a colleague for finding and deleting empty files
from a given directory. This command involved a series of pipes and I probably
wouldn't have solved it too differently. I was curious what other ways there
were of doing such a task, so I asked Claude. The first option it came back with
taught me about two new-to-me flags that `find` supports.
First is the `-empty` flag which applies a filter on the results to files or
directories that are empty.
Second is the `-delete` flag which will delete found files and directories.
Those two can be combined to _delete_ any results that are _empty_. Then to
target just _files_, I can include the `-type f` flag. And if I want to prevent
it from recursing down some unexpected tree of directories, I could also add in
`-maxdepth 1`.
To delete all empty files in `specific-directory`, I can run the following:
```bash
find ./specific-directory -maxdepth 1 -type f -empty -delete
```
My first thought for a command like this is that I will need to `xargs rm`, so
it's neat to know about the `-delete` flag in particular.
See `man find` for more details.
@@ -0,0 +1,17 @@
# Move YouTube Subtitles Out Of The Way
The thing about following along to any kind of programming tutorial on YouTube
is that current thing being typed, whether into the terminal or an editor, is
eventually going to be toward the bottom of the screen. If subtitles are
enabled, which they mostly are now that they can be auto-transcribed, then they
will appear on top of text being typed.
It's nice having the subtitles, but not if they are constantly covering up the
current focal point of the video -- the latest line of code or command being
typed out.
YouTube's web player allows you to grab and drag the subtitles anywhere within
the player. So, once they start being in the way, you can drag them up to the
top of the screen and continue to follow along.
[source](https://bsky.app/profile/jbranchaud.bsky.social/post/3mrjd5xktqs2r)
+58
View File
@@ -0,0 +1,58 @@
# Remove Pages From A PDF
I have a two page PDF. I really only need to second page though. I want to send
that second page to someone and including the first page with it will only cause
confusion. I couldn't figure out a sane way to do this on MacOS with either
Preview or Notes (where the PDF originated).
So, I looked into a tool I'd heard of recently for manipulating PDFs called
[`qpdf`](https://github.com/qpdf/qpdf).
> qpdf is a command-line tool and C++ library that performs content-preserving
> transformations on PDF files.
I installed it with `homebrew` and then looked up how "removing" pages works.
Conceptually, `qpdf` is used to transform a PDF. That means there are a couple
ways I can think about approaching this.
If I want to preserve every page except for the first page, then I produce a PDF
that starts at page `2` and goes to the end (`z`) by specifying a range of
`2-z`.
```bash
qpdf in.pdf --pages . 2-z -- out.pdf
```
If I truly only want the 2nd page from the PDF and nothing else, then I can
produce a PDF made up of _only_ page `2`.
```bash
qpdf in.pdf --pages . 2 -- out.pdf
```
Yet another way to think about this is to produce a PDF that includes all pages,
but then explicitly mark page `1` for exclusion after the range with `x1`.
```bash
qpdf in.pdf --pages . 1-z,x1 -- out.pdf
```
And one last way, assuming that I am dealing specifically with a two-page PDF,
is to use the `rN` syntax where `N` is the number of pages from the back of the
list. `r1` is the last page, `r2` is the second to last page, and so forth. In a
two-page PDF, `r2` would then refer to the _first_page.
```bash
qpdf in.pdf --pages . 1-z,xr2 -- out.pdf
```
I know I said that was the last, but I'll also throw in that, again assuming a
two-page PDF, I could produce a PDF made up of only `:even` numbered pages
relative to the defined range.
```bash
qpdf in.pdf --pages . 1-z:even -- out.pdf
```
I'll stick with the first approach, but it was fun to experiment with all the
other ways this can be done with `qpdf`'s flexible syntax.
@@ -0,0 +1,37 @@
# Open Current Command Line Prompt In Editor
The [`fc` builtin](/unix/fix-previous-command-with-fc.md) is a nice trick for
being able to open my default editor with the previous command loaded in. I can
make quick edits, save and quit, and the updated previous command will populate
my prompt.
What if I want to work from a blank prompt or I want to edit the command I've
already started typing out?
[`zsh`](https://github.com/zsh-users/zsh) has a solution for this via a contrib
widget, `edit-command-line`, that ships with `zsh`. I can load this widget and
then bind it to something handy like `Ctrl-x Ctrl-e`. To do that, I can run the
following sequence of commands and even add them to my `zsh` config:
```bash
autoload -Uz edit-command-line
zle -N edit-command-line
bindkey '^X^E' edit-command-line
```
The `autoload` line creates a function stub with the name `edit-command-line`
(try `which edit-command-line` after running just that and you'll see what I
mean). The `zle` line binds the `edit-command-line` widget to the function named
`edit-command-line` (if they had different names, it would be `zle -N <widget> <function>`). Lastly, I bind `Ctrl-x Ctrl-e` to that `edit-command-line`
function.
When this runs, it will first try for the `VISUAL` editor, otherwise fallback
`EDITOR`. I happen to have both of those set to `nvim` in my config. When I hit
`Ctrl-x Ctrl-e`, `nvim` opens with the current command populated. I can make
edits, and then when I run `:wq`, the updated command replaces what was there. I
still have to hit enter to execute it.
I find this most useful when I'm typing a multi-line prompt, especially one
where I'm trying to get all the details right and I need to copy in values from
somewhere else. It is also invaluable for writing out [a command like this](/llm/include-a-file-with-message-to-ant.md) that has a real-world cost if
I accidentally bump enter too soon.
+61
View File
@@ -0,0 +1,61 @@
# Where And Which Are Whence
I was recently watch some of the commands being run by an AI agent in a coding
harness. One in particular caught my attention because it wasn't one I was
familiar with. It was the command `whence`.
The `man zshbuiltins` describe `whence` like so:
> For each name, indicate how it would be interpreted if used as a command name.
I'd describe it myself as a utility for surfacing what a given command name
corresponds to when it gets executed. For instance, when run against the name
`cat` in my local setup, I see `bat` which is explained by the alias I set up.
```bash
whence cat
bat
```
There are several flags that can be used with `whence`. The underlying behavior
for `whence` is what is also used for more familiar commands like `where` and
`which`.
```
where [ -wpmsS ] [ -x num ] name ...
Equivalent to whence -ca.
which [ -wpamsS ] [ -x num ] name ...
Equivalent to whence -c.
```
I can get a clearer answer that `cat` is an alias with `-c`.
```bash
whence -c cat
cat: aliased to bat
```
And with `-ca`, I can see that `cat` resolves several different times on the
path with the alias to `bat` taking precedence over all of that.
```bash
whence -ca cat
cat: aliased to bat
/bin/cat
/bin/cat
/bin/cat
/bin/cat
/bin/cat
/bin/cat
```
The other command that uses the underlying `whence` functionality is `type`.
```
type [ -wfpamsS ] name ...
Equivalent to whence -v.
```
See `man zshbuiltins` for more details. There are more flags which can be used
to get additional info.