1
0
mirror of https://github.com/jbranchaud/til synced 2026-07-06 17:20:33 +00:00

Compare commits

60 Commits

Author SHA1 Message Date
jbranchaud d4766dad95 Create a shared task to deduplicate logic listing recent TILs 2026-07-05 18:25:02 -05:00
jbranchaud 5f4308c1af Add two new browsing tasks for popping open latest TIL in browser 2026-07-05 18:09:31 -05:00
jbranchaud 8c463a90e3 Add Deduplicate List While Preserving Original Order as a Unix TIL 2026-07-05 17:55:06 -05:00
jbranchaud 329267c7a2 Add Set Up Pyright Type Checking In GitHub as a Python TIL 2026-07-04 17:14:33 -05:00
jbranchaud 12180acb02 Add Generate Permutations Of All Valid 9-ball Racks as a Math TIL 2026-07-03 21:44:35 -04:00
jbranchaud 8f722061b4 Add Enable Pyright Type Checking In Cursor as a Python TIL 2026-06-29 11:44:11 -04:00
jbranchaud a126b13d7c Add Print Out File With Bat Without Formatting as a Unix TIL 2026-06-28 11:53:18 -05:00
jbranchaud 0ae3f14c25 Add Define Typed Class Interface With Protocol as a Python TIL 2026-06-27 13:10:51 -05:00
jbranchaud b91527db30 Add Use Rescue As Part Of Inline Statement as a Ruby TIL 2026-06-26 11:54:31 -05:00
jbranchaud c8f8c2c1a3 Add Turn Method Into Cached Property On Class Instance as a Python TIL 2026-06-25 11:30:18 -05:00
jbranchaud c397e35ffd Add Process JSON Output From gh With jq as a GitHub TIL 2026-06-24 16:56:26 -05:00
jbranchaud bbb28fd811 Add Show All Linear Keyboard Shortcuts as a Workflow TIL 2026-06-23 03:31:38 -05:00
jbranchaud 950e2f861a Add Define Conditional Routing Logic In Routes File as a Rails TIL 2026-06-22 17:52:13 -05:00
jbranchaud 20bbdb6d55 Add Make Secure Temp File For Atomic Write as a Python TIL 2026-06-22 17:26:14 -05:00
jbranchaud 03c11f9042 Add Clean Up Item Layout In Finder Window as a Mac TIL 2026-06-10 14:01:42 -05:00
jbranchaud cea6c75e1c Add Check Precondition Before Click Arg Parsing as a Python TIL 2026-05-24 17:35:11 -05:00
jbranchaud 539cbbefa6 Add Halt ActionMailer Delivery With Callback as a Rails TIL 2026-05-23 12:33:02 -05:00
jbranchaud 3eddb54053 Add Argument Defaults Are Evaluated When Function Is Defined as a Python TIL 2026-05-21 21:53:46 -05:00
jbranchaud 49628a7849 Add Read The Lid Angle Sensor For A MacBook as a Mac TIL 2026-05-20 12:39:28 -05:00
jbranchaud 3919b721cd Add Distinguish Sessions With Different Colors as a Claude Code TIL 2026-05-19 11:20:20 -05:00
jbranchaud 9c65e5c0b3 Add Get User's Preferred Language From Browser as a JavaScript TIL 2026-05-14 11:27:58 -05:00
jbranchaud e58ffffda0 Add Validate Click Option With Callback as a Python TIL 2026-05-09 20:28:01 -05:00
jbranchaud d87e125472 Add Cloudflare Allows CNAME For Apex Domain as a Devops TIL 2026-05-08 15:02:00 -05:00
jbranchaud facc606014 Add Get Quotient And Remainder In One Operation as a Python TIL 2026-05-05 16:57:07 -05:00
jbranchaud be103b52dd Add Programmatically Grab SHA For Head Commit as a Git TIL 2026-05-04 16:12:31 -05:00
jbranchaud 3402428aad Add Reclassify Certain Packagaes As Dev Dependencies as a Python TIL 2026-05-04 14:30:12 -05:00
jbranchaud 0c00e47141 Add Open File To Specific Line In Browser as a GitHub TIL 2026-05-03 19:18:14 -05:00
jbranchaud 1c90fdd823 Add Get Absolute Seconds From timedelta Object as a Python TIL 2026-05-02 16:56:49 -05:00
jbranchaud bf3991ce04 Add a missing link to the latest TIL 2026-05-02 12:37:50 -05:00
jbranchaud 3db5af78c1 Add View Nicely Formatted Markdown From Terminal as a Workflow TIL 2026-05-02 12:36:15 -05:00
jbranchaud a8c35e2458 Add Define Sequence Of Tests With Parametrize Decorator as a Python TIL 2026-05-01 21:30:20 -05:00
jbranchaud c0ad3cee4d Add Assert Is Only A Development Check as a Python TIL 2026-05-01 17:01:14 -05:00
jbranchaud 9bd1bb413a Add Reverse Each Line Of A File as a Unix TIL 2026-05-01 16:07:15 -05:00
jbranchaud ab8331000f Add Sort Normalized Version Of Data as a Python TIL 2026-04-29 11:34:32 -05:00
jbranchaud cd54360925 Add missing terminal prompt in code block 2026-04-23 12:24:23 -05:00
jbranchaud 75421685ea Add List PRs Awaiting Your Review as a GitHub TIL 2026-04-22 19:53:32 -05:00
jbranchaud 0cb5890fc0 Add Access Variables Outside Loop Scope as a Python TIL 2026-04-21 17:48:06 -05:00
jbranchaud 7de0e70d78 Add Define A Set Of Class Methods as a Ruby TIL 2026-04-17 10:51:31 -04:00
jbranchaud 36934aa56f Add Make Dataclass Sortable By Specific Field as a Python TIL 2026-04-15 22:53:29 -05:00
jbranchaud 2cd465bb08 Add Display All Git Log Entries In My Local Timezone as a Git TIL 2026-04-09 12:54:59 -05:00
jbranchaud 6ad376885b Add Stash The Current Prompt To Send Another First as a Claude Code TIL 2026-04-08 13:35:11 -05:00
jbranchaud 0c4702be97 Add Count Number Of Tokens In A File as an LLM TIL 2026-04-03 09:23:11 -05:00
jbranchaud b873f86f5b Add Sort A List Of Dataclass Instances as a Python TIL 2026-04-01 20:38:20 -05:00
jbranchaud 1120bb2018 Add Avoid Vulnerabilities In New Package Versions as a PNPM TIL 2026-03-31 11:33:13 -05:00
jbranchaud 906253b7dc Add List Available Zle Keybindings as a Zsh TIL 2026-03-30 13:45:35 -05:00
jbranchaud b4920c0397 Add Skip Specific Pytest Test Cases as a Python TIL 2026-03-29 11:05:32 -05:00
jbranchaud 119cc15c9a Add a couple more examples to most recent TIL 2026-03-29 01:22:39 -05:00
jbranchaud 1a4589f8f7 Add Use The Readline Keybindings Anywhere as a Unix TIL 2026-03-27 22:38:14 -05:00
jbranchaud 5f35404433 Add Avoid Modification With Frozen Dataclass as a Python TIL 2026-03-25 18:52:19 -05:00
jbranchaud 1766e45134 Add Start The Debugger When A Test Errors as a Python TIL 2026-03-23 21:27:10 -05:00
jbranchaud c875652725 Add Add Default Task To List All Tasks as a Taskfile TIL 2026-03-23 21:12:44 -05:00
jbranchaud 8af252f232 Add Use __post_init__ For dataclass Validations as a Python TIL 2026-03-22 14:37:59 -05:00
jbranchaud eb0a7e1b3d Link to the VisualMode blog from the README 2026-03-21 13:28:25 -05:00
jbranchaud c1cd40311f Add Browse And Search Help Docs as a Unix TIL 2026-03-21 12:34:49 -05:00
jbranchaud c744117eff Add Deduplicate A List Into A Tuple as a Python TIL 2026-03-21 12:33:09 -05:00
jbranchaud 329ce1aa3e Add Filter By Type as a Ruby TIL 2026-03-20 09:18:10 -05:00
jbranchaud 16082177aa Add Create A Range Of Descending Values as a Python TIL 2026-03-19 00:25:32 -05:00
jbranchaud 2276a57445 Add Look Inside Pytest tmp_path as a Python TIL 2026-03-18 21:03:51 -05:00
jbranchaud ceaab3da4f Add Reveal Location Of File In Finder.app as a Mac TIL 2026-03-16 09:40:18 -05:00
jbranchaud c7711ca337 Add Control Passing Of Time In Tests as a Python TIL 2026-03-14 16:18:36 -05:00
56 changed files with 2425 additions and 1 deletions
+60 -1
View File
@@ -10,10 +10,11 @@ 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).
_1756 TILs and counting..._
_1810 TILs and counting..._
See some of the other learning resources I work on:
- [The VisualMode Blog](https://visualmode.dev/blog)
- [Get Started with Vimium](https://egghead.io/courses/get-started-with-vimium~3t5f7)
- [Ruby Operator Lookup](https://www.visualmode.dev/ruby-operators)
- [Vim Un-Alphabet](https://www.youtube.com/playlist?list=PL46-cKSxMYYCMpzXo6p0Cof8hJInYgohU)
@@ -59,6 +60,7 @@ If you've learned something here, support my efforts writing daily TILs by
* [Linux](#linux)
* [LLM](#llm)
* [Mac](#mac)
* [Math](#math)
* [Mise](#mise)
* [MongoDB](#mongodb)
* [MySQL](#mysql)
@@ -164,9 +166,11 @@ If you've learned something here, support my efforts writing daily TILs by
### Claude Code
- [Allow Edits From The Start](claude-code/allow-edits-from-the-start.md)
- [Distinguish Sessions With Different Colors](claude-code/distinguish-sessions-with-different-colors.md)
- [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)
- [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md)
### Clojure
@@ -237,6 +241,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Check For Cached Site Assocation File For iOS](devops/check-for-cached-site-association-file-for-ios.md)
- [Check The Status of All Services](devops/check-the-status-of-all-services.md)
- [Check The Syntax Of nginx Files](devops/check-the-syntax-of-nginx-files.md)
- [Cloudflare Allows CNAME For Apex Domain](devops/cloudflare-allows-cname-for-apex-domain.md)
- [Connect To An RDS PostgreSQL Database](devops/connect-to-an-rds-postgresql-database.md)
- [Default Rails Deploy Script On Hatchbox](devops/default-rails-deploy-script-on-hatchbox.md)
- [Determine The IP Address Of A Domain](devops/determine-the-ip-address-of-a-domain.md)
@@ -360,6 +365,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Determine Absolute Path Of Top-Level Project Directory](git/determine-absolute-path-of-top-level-project-directory.md)
- [Determine The Hash Id For A Blob](git/determine-the-hash-id-for-a-blob.md)
- [Diffing With Patience](git/diffing-with-patience.md)
- [Display All Git Log Entries In My Local Timezone](git/display-all-git-log-entries-in-my-local-timezone.md)
- [Dropping Commits With Git Rebase](git/dropping-commits-with-git-rebase.md)
- [Dry Runs in Git](git/dry-runs-in-git.md)
- [Exclude A File From A Diff Output](git/exclude-a-file-from-a-diff-output.md)
@@ -403,6 +409,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [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)
- [Programmatically Grab SHA For Head Commit](git/programmatically-grab-sha-for-head-commit.md)
- [Pulling In Changes During An Interactive Rebase](git/pulling-in-changes-during-an-interactive-rebase.md)
- [Push To A Branch On Another Remote](git/push-to-a-branch-on-another-remote.md)
- [Quicker Commit Fixes With The Fixup Flag](git/quicker-commit-fixes-with-the-fixup-flag.md)
@@ -461,7 +468,10 @@ If you've learned something here, support my efforts writing daily TILs by
### GitHub
- [Access Your GitHub Profile Photo](github/access-your-github-profile-photo.md)
- [List PRs Awaiting Your Review](github/list-prs-awaiting-your-review.md)
- [Open A PR To An Unforked Repo](github/open-a-pr-to-an-unforked-repo.md)
- [Open File To Specific Line In Browser](github/open-file-to-specific-line-in-browser.md)
- [Process JSON Output From gh With jq](github/process-json-output-from-gh-with-jq.md)
- [Target Another Repo When Creating A PR](github/target-another-repo-when-creating-a-pr.md)
- [Tell gh What The Default Repo Is](github/tell-gh-what-the-default-repo-is.md)
@@ -615,6 +625,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Get The Response Status From An Axios Error](javascript/get-the-response-status-from-an-axios-error.md)
- [Get The Time Components Of A Date](javascript/get-the-time-components-of-a-date.md)
- [Get The Time Zone Of The Client Computer](javascript/get-the-time-zone-of-the-client-computer.md)
- [Get User's Preferred Language From Browser](javascript/get-users-preferred-language-from-browser.md)
- [Globally Install A Package With Yarn](javascript/globally-install-a-package-with-yarn.md)
- [Globally Install Specific Version Of PNPM](javascript/globally-install-specific-version-of-pnpm.md)
- [Immutable Remove With The Spread Operator](javascript/immutable-remove-with-the-spread-operator.md)
@@ -715,6 +726,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)
- [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)
@@ -726,6 +738,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Add A Bunch Of CLI Utilities With coreutils](mac/add-a-bunch-of-cli-utilities-with-coreutils.md)
- [Capture Screenshot To Clipboard From CLI](mac/capture-screenshot-to-clipboard-from-cli.md)
- [Check Network Quality Stats From The Command Line](mac/check-network-quality-stats-from-the-command-line.md)
- [Clean Up Item Layout In Finder Window](mac/clean-up-item-layout-in-finder-window.md)
- [Clean Up Old Homebrew Files](mac/clean-up-old-homebrew-files.md)
- [Control Which Monitor App Switcher Appears On](mac/control-which-monitor-app-switcher-appears-on.md)
- [Convert An HEIC Image File To JPG](mac/convert-an-heic-image-file-to-jpg.md)
@@ -743,9 +756,11 @@ If you've learned something here, support my efforts writing daily TILs by
- [Open Finder.app To Specific Directory](mac/open-finder-app-to-specific-directory.md)
- [Prevent Sleep With The Caffeinate Command](mac/prevent-sleep-with-the-caffeinate-command.md)
- [Quickly Type En Dashes And Em Dashes](mac/quickly-type-en-dashes-and-em-dashes.md)
- [Read The Lid Angle Sensor For A MacBook](mac/read-the-lid-angle-sensor-for-a-macbook.md)
- [Require Additional JS Libraries In Postman](mac/require-additional-js-libraries-in-postman.md)
- [Resize App Windows With AppleScript](mac/resize-app-windows-with-applescript.md)
- [Resizing Both Corners Of A Window](mac/resizing-both-corners-of-a-window.md)
- [Reveal Location Of File In Finder.app](mac/reveal-location-of-file-in-finder-app.md)
- [Run A Hardware Check](mac/run-a-hardware-check.md)
- [Run AppleScript Commands Inline In The Terminal](mac/run-applescript-commands-inline-in-the-terminal.md)
- [Set A Window To Its Default Zoom Level](mac/set-a-window-to-its-default-zoom-level.md)
@@ -757,6 +772,10 @@ If you've learned something here, support my efforts writing daily TILs by
- [View All Windows Of The Current App](mac/view-all-windows-of-the-current-app.md)
- [Write System Clipboard To A File](mac/write-system-clipboard-to-a-file.md)
### Math
- [Generate Permutations Of All Valid 9-ball Racks](math/generate-permutations-of-all-valid-9-ball-racks.md)
### Mise
- [Create Umbrella Task For All Test Tasks](mise/create-umbrella-task-for-all-test-tasks.md)
@@ -845,6 +864,7 @@ If you've learned something here, support my efforts writing daily TILs by
### pnpm
- [Avoid Vulnerabilities In New Package Versions](pnpm/avoid-vulnerabilities-in-new-package-versions.md)
- [Execute A Command From The Workspace Root](pnpm/execute-a-command-from-the-workspace-root.md)
- [Install Command Runs For Entire Workspace](pnpm/install-command-runs-for-entire-workspace.md)
- [List The Installed Version Of A Specific Package](pnpm/list-the-installed-version-of-a-specific-package.md)
@@ -1042,22 +1062,47 @@ If you've learned something here, support my efforts writing daily TILs by
- [Access Instance Variables](python/access-instance-variables.md)
- [Access Most Recent Return Value In REPL](python/access-most-recent-return-value-in-repl.md)
- [Access Variables Outside Loop Scope](python/access-variables-outside-loop-scope.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)
- [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)
- [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)
- [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)
- [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)
- [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)
- [Keep A Tally With collections.Counter](python/keep-a-tally-with-collections-counter.md)
- [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.md)
- [Look Inside Pytest tmp_path](python/look-inside-pytest-tmp-path.md)
- [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)
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.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)
- [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)
- [Test A Function With Pytest](python/test-a-function-with-pytest.md)
- [Turn Method Into Cached Property On Class Instance](python/turn-method-into-cached-property-on-class-instance.md)
- [Use pipx To Install End User Apps](python/use-pipx-to-install-end-user-apps.md)
- [Use `__post_init__` For `dataclass` Validations](python/use-post-init-for-dataclass-validations.md)
- [Use Verbose Flag To Get More Diff](python/use-verbose-flag-to-get-more-diff.md)
- [Validate Click Option With Callback](python/validate-click-option-with-callback.md)
### Rails
@@ -1113,6 +1158,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Customize Paths And Helpers For Devise Routes](rails/customize-paths-and-helpers-for-devise-routes.md)
- [Customize Template For New Schema Migration](rails/customize-template-for-new-schema-migration.md)
- [Customize The Path Of A Resource Route](rails/customize-the-path-of-a-resource-route.md)
- [Define Conditional Routing Logic In Routes File](rails/define-conditional-routing-logic-in-routes-file.md)
- [Define The Root Path For The App](rails/define-the-root-path-for-the-app.md)
- [Delete Paranoid Records](rails/delete-paranoid-records.md)
- [Demodulize A Class Name](rails/demodulize-a-class-name.md)
@@ -1143,6 +1189,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Get The Column Names For A Model](rails/get-the-column-names-for-a-model.md)
- [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 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)
@@ -1404,6 +1451,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Defaulting To Frozen String Literals](ruby/defaulting-to-frozen-string-literals.md)
- [Define A Custom RSpec Matcher](ruby/define-a-custom-rspec-matcher.md)
- [Define A Method On A Struct](ruby/define-a-method-on-a-struct.md)
- [Define A Set Of Class Methods](ruby/define-a-set-of-class-methods.md)
- [Define Multiline Strings With Heredocs](ruby/define-multiline-strings-with-heredocs.md)
- [Destructure The First Item From An Array](ruby/destructure-the-first-item-from-an-array.md)
- [Destructuring Arrays In Blocks](ruby/destructuring-arrays-in-blocks.md)
@@ -1424,6 +1472,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [FactoryGirl Sequences](ruby/factory-girl-sequences.md)
- [Fail](ruby/fail.md)
- [Fetch Warns About Superseding Block Argument](ruby/fetch-warns-about-superseding-block-argument.md)
- [Filter By Type](ruby/filter-by-type.md)
- [Find The Min And Max With A Single Call](ruby/find-the-min-and-max-with-a-single-call.md)
- [Finding The Source of Ruby Methods](ruby/finding-the-source-of-ruby-methods.md)
- [Format A Hash Into A String Template](ruby/format-a-hash-into-a-string-template.md)
@@ -1526,6 +1575,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Update The Gemfile Bundled With Version](ruby/update-the-gemfile-bundled-with-version.md)
- [Use A Case Statement As A Cond Statement](ruby/use-a-case-statement-as-a-cond-statement.md)
- [Use dotenv In A Non-Rails Project](ruby/use-dotenv-in-a-non-rails-project.md)
- [Use Rescue As Part Of Inline Statement](ruby/use-rescue-as-part-of-inline-statement.md)
- [Use Tap For Better Test Data Setup](ruby/use-tap-for-better-test-data-setup.md)
- [Using BCrypt To Create And Check Hashed Passwords](ruby/using-bcrypt-to-create-and-check-hashed-passwords.md)
- [What To Do When You Don't Rescue](ruby/what-to-do-when-you-dont-rescue.md)
@@ -1570,6 +1620,7 @@ If you've learned something here, support my efforts writing daily TILs by
### Taskfile
- [Add Default Task To List All Tasks](taskfile/add-default-task-to-list-all-tasks.md)
- [Create Interactive Picker For Set Of Subtasks](taskfile/create-interactive-picker-for-set-of-subtasks.md)
- [Run A Task If It Meets Criteria](taskfile/run-a-task-if-it-meets-criteria.md)
@@ -1643,6 +1694,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [All The Environment Variables](unix/all-the-environment-variables.md)
- [Apply Successive Filters To Lines In Less](unix/apply-successive-filters-to-lines-in-less.md)
- [Authorize A cURL Request](unix/authorize-a-curl-request.md)
- [Browse And Search Help Docs](unix/browse-and-search-help-docs.md)
- [Cat A File With Line Numbers](unix/cat-a-file-with-line-numbers.md)
- [Cat Files With Color Using Bat](unix/cat-files-with-color-using-bat.md)
- [Change Default Shell For A User](unix/change-default-shell-for-a-user.md)
@@ -1672,6 +1724,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Curl With Cookies](unix/curl-with-cookies.md)
- [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)
- [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)
@@ -1765,6 +1818,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Print A Range Of Lines For A File With Bat](unix/print-a-range-of-lines-for-a-file-with-bat.md)
- [Print DateTime Represented By Unix Timestamp](unix/print-datetime-represented-by-unix-timestamp.md)
- [Print Milliseconds In Human-Readable Format](unix/print-milliseconds-in-human-readable-format.md)
- [Print Out File With Bat Without Formatting](unix/print-out-file-with-bat-without-formatting.md)
- [Print Out Files In Reverse](unix/print-out-files-in-reverse.md)
- [Print The Current Date In Human-Readable Format](unix/print-the-current-date-in-human-readable-format.md)
- [Produce A Lowercase V4 UUID](unix/produce-a-lowercase-v4-uuid.md)
@@ -1773,6 +1827,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Rename A Bunch Of Files By Constructing mv Commands](unix/rename-a-bunch-of-files-by-constructing-mv-commands.md)
- [Repeat Yourself](unix/repeat-yourself.md)
- [Replace Pattern Across Many Files In A Project](unix/replace-pattern-across-many-files-in-a-project.md)
- [Reverse Each Line Of A File](unix/reverse-each-line-of-a-file.md)
- [Run A Command Repeatedly Several Times](unix/run-a-command-repeatedly-several-times.md)
- [Run A cURL Command Without The Progress Meter](unix/run-a-curl-command-without-the-progress-meter.md)
- [Safely Edit The Sudoers File With Vim](unix/safely-edit-the-sudoers-file-with-vim.md)
@@ -1806,6 +1861,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Use fzf To Change Directories](unix/use-fzf-to-change-directories.md)
- [Use Negative Lookbehind Matching With ripgrep](unix/use-negative-lookbehind-matching-with-ripgrep.md)
- [Use Regex Pattern Matching With Grep](unix/use-regex-pattern-matching-with-grep.md)
- [Use The Readline Keybindings Anywhere](unix/use-the-readline-keybindings-anywhere.md)
- [View A Web Page In The Terminal](unix/view-a-web-page-in-the-terminal.md)
- [View The Source For A Brew Formula](unix/view-the-source-for-a-brew-formula.md)
- [Watch The Difference](unix/watch-the-difference.md)
@@ -2037,11 +2093,13 @@ If you've learned something here, support my efforts writing daily TILs by
- [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md)
- [Send A PDF To Your Kindle](workflow/send-a-pdf-to-your-kindle.md)
- [Set Recurring Reminders In Slack](workflow/set-recurring-reminders-in-slack.md)
- [Show All Linear Keyboard Shortcuts](workflow/show-all-linear-keyboard-shortcuts.md)
- [Show Linting Errors In Zed](workflow/show-linting-errors-in-zed.md)
- [Temporarily Hide CleanShot X Capture Previews](workflow/temporarily-hide-cleanshot-x-capture-previews.md)
- [Toggle Between Stories In Storybook](workflow/toggle-between-stories-in-storybook.md)
- [Update asdf Plugins With Latest Package Versions](workflow/update-asdf-plugins-with-latest-package-versions.md)
- [View A Nicely-Formatted CSV In Terminal](workflow/view-a-nicely-formatted-csv-in-terminal.md)
- [View Nicely Formatted Markdown From Terminal](workflow/view-nicely-formatted-markdown-from-terminal.md)
- [View The PR For The Current GitHub Branch](workflow/view-the-pr-for-the-current-github-branch.md)
### XState
@@ -2076,6 +2134,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Add To The Path Via Path Array](zsh/add-to-the-path-via-path-array.md)
- [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)
- [Use A Space To Exclude Command From History](zsh/use-a-space-to-exclude-command-from-history.md)
## Usage
+30
View File
@@ -11,6 +11,36 @@ tasks:
cmds:
- task --list
browse:list:
desc: Print deduped, newest-first TIL paths
silent: true
cmds:
- |
git log --diff-filter=A --name-only --pretty=format: -- '*/*.md' \
| grep -v '^$' \
| awk '!seen[$0]++'
browse:
desc: Pick from 5 most recent TILs (fzf) and open in browser
interactive: true
silent: true
cmds:
- |
FILE=$(task browse:list | head -5 | fzf --prompt="Open TIL: " --height=40% --reverse) || true
if [ -n "$FILE" ]; then
gh browse "$FILE"
fi
browse:latest:
desc: Open the single most recent TIL in the browser
silent: true
cmds:
- |
FILE=$(task browse:list | head -1)
if [ -n "$FILE" ]; then
gh browse "$FILE"
fi
notes:
desc: Interactive picker for notes tasks
cmds:
@@ -0,0 +1,24 @@
# Distinguish Sessions With Different Colors
I sometimes have several Claude Code sessions open at once. As I bounce between
tmux windows, it can sometimes be tricky to tell them apart at a glance. One way
that Claude Code can help with this is with some light styling. You can change
the accent color of a session with the `/color` command.
Run it as is and it will choose a random color to set the session to.
Or you can pick from any of the available colors which it will give you a hint
for if you type a space after `/color`.
```
/color [red|blue|green|yellow|purple|orange|pink|cyan|default]
```
I can run the following to set it to cyan:
```
/color cyan
```
More details on this kinds of commands can be found in the [_Commands_
docs](https://code.claude.com/docs/en/commands).
@@ -0,0 +1,25 @@
# Stash The Current Prompt To Send Another First
I've been working my way through the current cohort of Matt Pocock's [Claude
Code for Real
Engineers](https://www.aihero.dev/cohorts/claude-code-for-real-engineers-2026-04).
The best part about going through a series of videos like this is being able to
pick up big and small tips and tricks from another person's workflow.
One of the small things I picked up in an early video is the ability to stash
the current prompt.
Let's say I've gone to the trouble of writing out a detailed prompt, `@`'ing
some files, and so forth. Then I realize I need first prompt Claude to do
something else first. Instead of copy-pasting that prompt into my notes,
deleting it, issuing a different prompt, and then pasting it back in, I can hit
`Ctrl-s`.
`Ctrl-s` will _stash_ the current prompt, clearing out the prompt input. I can
then type in something else. Once I hit enter for that new prompt, it will be
sent to Claude and the stashed prompt will be immediately populated back into
the input.
Though `Ctrl-s` is mentioned when you hit `?` from within `claude` session, I
don't see it documented anywhere in their [Interactive Mode
reference](https://code.claude.com/docs/en/interactive-mode).
@@ -0,0 +1,19 @@
# Cloudflare Allows CNAME For Apex Domain
If you want to set up a custom root (apex) domain with an app hosting provider
[like
Heroku](https://devcenter.heroku.com/articles/custom-domains#add-a-custom-root-domain),
you're going to need to work with a DNS provider that supports the non-standard
`ALIAS` records (or something equivalent).
In my case, I have my domain registered with Cloudflare. Cloudflare supports
this kind of CNAME lookup of an apex domain through [_CNAME
flattening_](https://developers.cloudflare.com/dns/cname-flattening/).
Unlike other registrars that use a separate `ALIAS` record concept, Cloudflare
allows you to set up a specialized `CNAME` record. Go into the DNS settings for
the domain of interest, click "Add Record", and then select `CNAME`. From there,
instead of entering a traditional subdomain like `www`, you put the `@` symbol
which tells Cloudflare that this is a record for the apex domain. That record
will still point to a target like `abc123.herokudns.com` as a more traditional
`CANME` would do.
@@ -0,0 +1,30 @@
# Display All Git Log Entries In My Local Timezone
I tend to work with remote teams distributed across across multiple time zones.
In that context, it is important to have an awareness of what time zone each
person is operating in and to communicate clearly around that.
When looking at the output for `git log` on a distributed team, the timestamps
for each entry can be all over the place. If I want to understand when something
was committed, I have to look at the time as well as the time zone offset and
mentally translate it to my own time zone.
There is a `git config` option to alleviate this issue by having `git log`
convert and display all timestamps into your local time zone.
```bash
$ git config --global log.date rfc-local
```
Running that will add this entry to your _global_ git config file:
```
[log]
date = rfc-local
```
Now the time that was displaying as `Wed Apr 8 20:12:33 2026 -0400` will display
as `Wed, 8 Apr 2026 19:12:33 -0500`.
This also helps with smoothing out differences from DST and for commits produced
by AI agents in sandbox environments where the locale is set to UTC.
@@ -0,0 +1,33 @@
# Programmatically Grab SHA For Head Commit
When I use `gh browse path/to/some-file.txt`, it opens the browser to that file
in GitHub. However, it targets the default branch (`main`) by default which is
not very useful as a permalink because what that file looks like on `main` is
liable to change.
There is a `--commit` flag you can use to have it instead open to that file at a
specific commit SHA.
So what SHA do I pass as an argument to that flag?
Often what I would like to grab is a reference to the current version of the
file which is whatever it looks like for the `HEAD` commit. But `HEAD` is
another moving target reference. The `git rev-parse` command can translate
`HEAD` into a specific SHA though.
```bash
git rev-parse --short HEAD
3402428
git rev-parse HEAD
3402428aadc02cfdc9825c8feb593443e72f50cd
```
Either of those will work. I can use a bash command substitution then to tie it
all together into a single command:
```bash
gh browse path/to/some-file.txt --commit=$(git rev-parse --short HEAD)
```
See `man git-rev-parse` for more details.
+31
View File
@@ -0,0 +1,31 @@
# List PRs Awaiting Your Review
If you work on a software team or steward an open-source project, then there are
likely some open PRs that you've been tagged to review. I am usually able to
catch most review requests as they come up either from the GitHub email
notifications or by keeping an eye on the PRs tab of active projects. Sometimes
I get consumed by a task and something slips through the cracks.
There are a couple other ways to quickly check if anything is waiting on my
review.
From the web UI I can visit the following URL which will show all PRs across all
projects where my review has been requested:
[https://github.com/pulls/review-requested](https://github.com/pulls/review-requested)
The GitHub CLI (`gh`) can do the same and I can do it right from the terminal
instead of navigating several clicks within GitHub's web UI.
```bash
$ gh search prs --review-requested=@me --state=open
```
That too will list PRs across all projects that are open and awaiting my review.
If that one ends up being a little too noisy, you can also use `gh` to _list_
just PRs for the current project:
```bash
$ gh pr list --search "review-requested:@me"
```
@@ -0,0 +1,46 @@
# Open File To Specific Line In Browser
Often one of the best ways to point a teammate to a line of code is to share a
GitHub link to a specific file and line number. Sometimes even a specific
commit.
For the longest time I would manually open GitHub, navigate to that file, and so
forth. The `gh` CLI supports this with the `browse` subcommand and it takes way
less time if you already have the repo in your local filesystem.
For instance, if I want to point you to line 11 of the `zshrc.local` file in my
`dotfiles` repo, I can run the following command:
```bash
$ gh browse zshrc.local:11
```
That would open a browser tab to
[https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11](https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11).
If I wanted a range of lines, I could change it from `11` to, say, `11-27`:
```bash
$ gh browse zshrc.local:11-27
```
And I would see this in the browser --
[https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11-L27](https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11-L27).
Both of these URLs are pointing to the `main` branch. If I instead want to
reference a specific commit, I can use the `--commit` flag.
```bash
$ gh browse zshrc.local:11-27 --commit=f2f9e78d4fc784643f725c88f7a5a7a077e7f261
```
I grabbed that from the latest commit in `git log`. That opens to
[https://github.com/jbranchaud/dotfiles/blob/f2f9e78d4fc784643f725c88f7a5a7a077e7f261/zshrc.local?plain=1#L11-L27](https://github.com/jbranchaud/dotfiles/blob/f2f9e78d4fc784643f725c88f7a5a7a077e7f261/zshrc.local?plain=1#L11-L27).
Another way of doing that would be to use `git rev-parse HEAD`:
```bash
$ gh browse zshrc.local:11-27 --commit=$(git rev-parse HEAD)
```
See `gh browse --help` for more details.
@@ -0,0 +1,58 @@
# Process JSON Output From gh With jq
The `gh` (GitHub) CLI is useful for accessing data about your profile and
projects from the terminal. With the `--json` flag, we can access the data in a
structured way which is useful for scripting.
Here is an example of pulling a list of all my repositories, limiting each
entity to just the `nameWithOwner` and `description`:
```bash
gh repo list --limit 1000 --json nameWithOwner,description
[
{
"description": "My personal site -- joshbranchaud.com",
"nameWithOwner": "jbranchaud/personal-site"
},
{
"description": "Private repo for the NOTES.md of my TIL repo",
"nameWithOwner": "jbranchaud/til-notes-private"
},
...
]
```
If I'm using the `--json` flag, then I can add in the `--jq` flag to apply a
`jq` query for additional processing of the output.
Here I convert it to a series of tuples:
```bash
gh repo list --limit 1000 --json nameWithOwner,description \
--jq '.[] | [.nameWithOwner, .description]'
[
"jbranchaud/personal-site",
"My personal site -- joshbranchaud.com"
]
[
"jbranchaud/til-notes-private",
"Private repo for the NOTES.md of my TIL repo"
]
...
```
Then I can add one more pipe to that `jq` query to turn it into _tab-separated
values_ using
[`@tsv`](https://jqlang.org/manual/v1.5/#format-strings-and-escaping):
```bash
gh repo list --limit 1000 --json nameWithOwner,description \
--jq '.[] | [.nameWithOwner, .description] | @tsv'
jbranchaud/personal-site My personal site -- joshbranchaud.com
jbranchaud/til-notes-private Private repo for the NOTES.md of my TIL repo
...
```
This is useful because I can then pipe it to another program, such as an `fzf`
command like [this repo selector that opens the selected one in the
browser](https://github.com/jbranchaud/dotfiles/commit/f964ca10c6c4db3475411c2991dc2f1dfd18c818).
@@ -0,0 +1,34 @@
# Get User's Preferred Language From Browser
A great way to determine a user's preferred language if you aren't able to ask
them directly is to look at the language setting for their browser's UI.
We can get this from the instance of
[`Navigator`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator) in the
user's JavaScript runtime within the browser.
My browser's UI is set to US English, which I can verify like so:
```javascript
> navigator.language
'en-US'
```
This is useful for all sorts of things like [formatting dates for
display](basic-date-formatting-without-a-library.md):
```javascript
> const now = new Date();
> Intl.DateTimeFormat(navigator.language).format(now)
'5/14/2026'
```
Or for [formatting other kinds of units for
display](formatting-values-with-units-for-display.md):
```javascript
> const milesFormat =
Intl.NumberFormat(navigator.language, { style: "unit", unit: "mile" });
> milesFormat.format(1500)
"1,500 mi"
```
+26
View File
@@ -0,0 +1,26 @@
# Count Number Of Tokens In A File
Over time you have accumulated a bunch of small directives, corrections, and
project details in your `CLAUDE.md` or `AGENTS.md` file. The file doesn't seem
too big, but you are mindful that it is being included in every prompt. How many
tokens is it eating from the context window?
OpenAI's BPE (Byte Pair Encoding) tokenization library,
[`tiktoken`](https://github.com/openai/tiktoken), is an open-source Python
package. If it is installed on our machine, then we can use it as part of the
following one-liner to check a file:
```bash
python -c "import tiktoken, sys; print(len(tiktoken.encoding_for_model('gpt-4o').encode(open(sys.argv[1], 'r', encoding='utf-8').read())))" \
AGENTS.md
1018
```
I ran this against the `AGENTS.md` file in a team project I'm on. It came out to
1018 tokens. This is a very good approximation based on the tokenizer trained
for `gpt-4o`. The tokenizers may vary a little from model to model, but the
differences for our purposes here are going to be negligible.
This one-liner gets the "first" argument to the command, reads it in, and runs
that string against the tokenizer. The length of the tokenized encoding is then
printed.
@@ -0,0 +1,16 @@
# Clean Up Item Layout In Finder Window
Sometimes while doing a bunch of manual drag-n-drop of files and folders in a
Finder.app window, I'll end up with a visual mess. Compared to other folders,
nothing is organized on the grid.
I can tell Finder.app to clean that up with the _Clean Up_ menu option.
While focused on the folder that I'm concerned about, I can go to _View_ >
_Clean Up_ in the top menu. Everything will snap into place.
On the specific Finder.app window, there is also a triple-dot actions menu that
appears on the top right. The _Clean Up_ action is available there as well.
There is also a _Clean Up By_ option which is a nice way to organize by some
attribute, such as the type (e.g Folder/File and extension).
@@ -0,0 +1,89 @@
# Read The Lid Angle Sensor For A MacBook
MacOS has a bunch of internal HID (Human Interface Device) data that can surface
details about all kinds of "devices" that comprise your machine. Some obvious
ones are the keyboard and trackpad as well as external mice and keyboards. The
battery and power source details are another which is sometimes integrated into
tools that display battery status (e.g.
[`tmux-battery`](https://github.com/tmux-plugins/tmux-battery)), though it uses
`pmset` directly). And many, many more.
One example I'd never considered is that there is a sensor for the lid angle of
the laptop that can tell the system whether the lid is open or closed and how
open it is (i.e. at what angle). There is no public interface for this lid angle
sensor, but people exploring all the HID devices have found the identifiers that
correspond to it (e.g.
[`pybooklid`](https://github.com/tcsenpai/pybooklid/blob/main/pybooklid/macbook_lid.py)).
Here is a minimal script that uses `uv`, `hidapi` (python bindings), and
`libhidapi` (shared runtime lib for those bindings):
```python
#!/usr/bin/env -S uv run --quiet --script
# /// script
# requires-python = ">=3.10"
# dependencies = ["hidapi"]
# ///
"""Print MacBook lid angle in degrees."""
import os, sys
if sys.platform == "darwin":
brew = "/opt/homebrew/lib"
if os.path.exists(brew):
os.environ["DYLD_LIBRARY_PATH"] = f"{brew}:{os.environ.get('DYLD_LIBRARY_PATH','')}"
import hid
VENDOR_ID, PRODUCT_ID = 0x05AC, 0x8104
USAGE_PAGE, USAGE = 0x0020, 0x008A
REPORT_ID = 1
def read_angle():
for info in hid.enumerate(VENDOR_ID, PRODUCT_ID):
if info.get("usage_page") == USAGE_PAGE and info.get("usage") == USAGE:
d = hid.device()
path = info["path"]
d.open_path(path if isinstance(path, bytes) else path.encode())
try:
data = d.get_feature_report(REPORT_ID, 8)
if data and len(data) >= 3:
return float((data[2] << 8) | data[1])
finally:
d.close()
return None
if __name__ == "__main__":
a = read_angle()
if a is None:
sys.exit("sensor not available")
print(f"{a:.0f}")
```
These IDs and usage values are the undocumented values that allow the script to
navigate specifically to the lid angle sensor and specifically to the usage page
and value that represent the current lid angle reading.
```
VENDOR_ID, PRODUCT_ID = 0x05AC, 0x8104
USAGE_PAGE, USAGE = 0x0020, 0x008A
REPORT_ID = 1
```
I added [this
script](https://github.com/jbranchaud/dotfiles/blob/cbc7196607d1d6b25885f5387ca85b658bd765de/bin/lidangle)
to [my dotfiles](https://github.com/jbranchaud/dotfiles) and made it executable
(`chmod +x bin/lidangle`) so that I can try it out. I first ran it while it was
closed and connected to my external monitor (`0`), then I opened it as far as it
could go (`129`), and then I tried angling it close to what I thought was 90
degress (`92`, so close).
```bash
lidangle
0
lidangle
129
lidangle
92
```
@@ -0,0 +1,20 @@
# Reveal Location Of File In Finder.app
In the terminal I have the path to an image file. I want to open Finder.app to
the location of that image file so that I can drag and drop it into a file
upload area in the browser.
Instead of opening a Finder.app window and navigating directory by directory to
the location, I can use the `open` command. Using `open` directly with the image
file will open the image in Preview.app. I want to reveal the directory that the
image file is in within Finder.app. _Reveal_ is the keyword and the `-R` flag
does just that.
Here is an example of this that I actually ran when uploading a screenshot that
went into [this blogmark post](https://still.visualmode.dev/blogmarks/255):
```bash
$ open -R /Users/lastword/images/tiobe-index-graph-march-2026.png
```
See `man open` for more details.
@@ -0,0 +1,42 @@
# Generate Permutations Of All Valid 9-ball Racks
I wanted to produce a full listing of all valid rack arrangements for the game
of [9-ball](https://en.wikipedia.org/wiki/Nine-ball). The constraints on how a
9-ball rack can be arranged are, first, that the 1 ball must be placed at the
head of the diamond and, second, that the 9 ball must be placed at the center of
the diamond. After that, all other balls (2 through 8) can be placed in any
arrangement.
Because each of those seven remaining balls can be arranged in distinct
orderings where each ball is placed once, this is a
[_permutation_](https://en.wikipedia.org/wiki/Permutation) problem.
> In elementary combinatorics, the k-permutations, or partial permutations, are
> the ordered arrangements of k distinct elements selected from a set. When k is
> equal to the size of the set, these are the permutations in the previous
> sense.
For this problem, the seven distinct elements can be arranged into `7!` (seven
factorial) unique permutations. That is, 5040 permutations.
I can use [Ruby's `Array#permutations`
method](https://docs.ruby-lang.org/en/4.0/Array.html#method-i-permutation) to
enumerate these 5040 permutations like so:
```ruby
[2,3,4,5,6,7,8].permutation.map do |perm|
[1, *perm[0..2], 9, *perm[3..7]]
end.to_a
=> [[1, 2, 3, 4, 9, 5, 6, 7, 8],
[1, 2, 3, 4, 9, 5, 6, 8, 7],
[1, 2, 3, 4, 9, 5, 7, 6, 8],
[1, 2, 3, 4, 9, 5, 7, 8, 6],
[1, 2, 3, 4, 9, 5, 8, 6, 7],
[1, 2, 3, 4, 9, 5, 8, 7, 6],
[1, 2, 3, 4, 9, 6, 5, 7, 8],
...
[1, 8, 7, 6, 9, 5, 3, 2, 4],
[1, 8, 7, 6, 9, 5, 3, 4, 2],
[1, 8, 7, 6, 9, 5, 4, 2, 3],
[1, 8, 7, 6, 9, 5, 4, 3, 2]]
```
@@ -0,0 +1,29 @@
# Avoid Vulnerabilities In New Package Versions
It seems like every week there is a new supply chain attack where malicious code
is embedded in a popular, widely-used OSS package. This week's is
[axios](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan).
The [`pnpm` package manager](https://pnpm.io/) has a nice feature that helps
avoid installing these vulnerable package versions in the first place.
> To reduce the risk of installing compromised packages, you can delay the
> installation of newly published versions. In most cases, malicious releases
> are discovered and removed from the registry within an hour.
The [`minimumReleaseAge` config option](https://pnpm.io/settings#minimumreleaseage) tells `pnpm` to not install
a dependency (including transitive ones) until it has been released for at least
that many minutes.
For instance, if you wanted to set this to 72 hours, then you'd set this option
to `4320` minutes like so:
```
$ pnpm config set minimum-release-age 4320 -g
```
The global flag (`-g`) will set that in your global config location, e.g.
`$XDG_CONFIG_HOME/pnpm/rc`. You could also add it specifically to your project
in the `pnpm-workspace.yaml` file.
[source](https://bsky.app/profile/styfle.dev/post/3miekuyeyrs2w)
@@ -0,0 +1,44 @@
# Access Variables Outside Loop Scope
Here is a function that loops over a list to find the first occurrence of a
falsy value.
```python
def find_false(self):
for item in self.items:
item_type = type(item)
print(f"Current item: {item} ({item_type})")
if not item:
break
print(f"First false item: {item} ({item_type})")
```
Notice how at the end of the function, outside of the loop, I am able to access
both `item` (defined in the loop definition) and `item_type` (defined within the
loop's body).
Both of these variables are defined, by the loop, in _function scope_ and are
accessible anywhere in the function after they have been defined.
The title of this TIL is a bit of a misnomer because Python doesn't have the
concept of a _loop scope_. There are two levels of scope in Python --
module/global scope and function scope.
I spend most of my time writing Ruby which also has _block scope_, so Python's
simplified two-level scoping took me by surprise.
Though the code sample above is contrived, this function scope assignment can be
taken advantage of with loop definitions in scenarios where you want to know
what the last `item` defined was before the loop terminated.
```python
for submission in submissions:
if passes(submission, criteria):
break
else:
raise ValueError("No submissions that meet given criteria")
print(f"Submit first passing submission: {submission.id}")
submit(submission)
```
@@ -0,0 +1,76 @@
# Argument Defaults Are Evaluated When Function Is Defined
When you define a function with any arguments that have default values, those
default values are evaluated and stored at the time that the function is defined
(i.e. when it is evaluated by the interpreter). This might feel counter
intuitive if you are coming from another language, like Ruby, where these kinds
of defaults are evaluated at call time. This is unremarkable for scalar values
like `4` or `"fallback"`. It's much more interesting when your defaults are
function calls.
What if our default is something like `datetime.now()`?
Here I've defined a `Timer` class that has a `start` and `stop` method. The
`stop` method can be called with a specific `datetime` value otherwise it falls
back to `datetime.now()` -- but when is _now_?
```python
from datetime import datetime, timezone
import time
class Timer:
def __init__(self):
self._start = None
self._stop = None
def start(self):
self._start = datetime.now(timezone.utc)
self._stop = None
def stop(self, at=datetime.now(timezone.utc)):
print(f"now: {datetime.now(timezone.utc)}")
print(f" at: {at}")
self._stop = at
elapsed = self._stop - self._start
return elapsed
```
Here I instantiate a timer, call `start`, sleep for 5 seconds, and then call
`stop`.
```python
timer = Timer()
timer.start()
time.sleep(5)
print(f"Elapsed: {timer.stop()}")
```
Here is what gets printed to `stdout`:
```
now: 2026-05-22 00:45:05.654878+00:00
at: 2026-05-22 00:45:00.649699+00:00
Elapsed: -1 day, 23:59:59.999875
```
Notice that the actual _now_ (when the `stop` method is running) is about 5
seconds after the value of `at`. That is because `at`, which takes on the
default argument value, is `datetime.now()` as evaluated at the time the
function is interpreted. It is for that same reason that `self._stop` ends up
being just a hair earlier than the call to `start` which sets `self._start`.
Which explains why the _elapsed_ time is a negative value.
To avoid this awkwardness all together, set the default as `None` and then
override `None` at the start of the function:
```python
def stop(self, at = None):
if at == None:
at = datetime.now(timezone.utc)
# ...
```
@@ -0,0 +1,38 @@
# Assert Is Only A Development Check
The `assert` keyword is used in Python to write a statement that will check some
assertion and raise an error if it isn't met. This is only meant to be used as a
check during development because it can be easily optimized out of the code.
```python
stuff = None
assert stuff, "We need to have some stuff to proceed"
print(f"We have {stuff or 'something'}!")
```
If I execute this code with `python`, it will raise on that second line of code.
```bash
python assert_example.py
Traceback (most recent call last):
File "/Users/lastword/dev/jbranchaud/py-vmt/assert_example.py", line 3, in <module>
assert stuff, "We need to have some stuff to proceed"
^^^^^
AssertionError: We need to have some stuff to proceed
```
This `assert` statement will be stripped out of the compiled bytecode if the
`-O` (capital o) flag is used. Notice how running the same file with that flag
does not lead to an `AssertionError`.
```python
python -O assert_example.py
We have something!
```
If I want to make sanity checks for situations that would be caused by a bug in
the code, an `assert` statement can be a good candidate. However, if I am making
runtime checks like validating user input, then an `if` statement and raising
something like a `ValueError` is better.
@@ -0,0 +1,47 @@
# Avoid Modification With Frozen Dataclass
The `@dataclass` decorator can be set as _frozen_ to prevent modification of
values on instances of that `dataclass`.
Without making it frozen, I can easily subvert validations by changing the value
of attributes after the `__post_init__` validations are called.
```python
>>> config = BPEConfig(300, []) # passes validations
>>> config.vocab_size = 22 # this is invalid, wish this was prevented
```
Here is the updated `@dataclass` declaration with `frozen=True` passed as a
parameter.
```python
from dataclasses import dataclass
from typing import ClassVar
@dataclass(frozen=True)
class BPEConfig:
BASE_VOCAB_SIZE: ClassVar[int] = 256
vocab_size: int
special_tokens: list[str]
def __post_init__(self):
if self.vocab_size < self.BASE_VOCAB_SIZE:
msg = f"vocab_size ({self.vocab_size}) must be greater than or equal to BASE_VOCAB_SIZE ({self.BASE_VOCAB_SIZE})"
raise ValueError(msg)
```
Now I am prevented from modifying a scalar value like `vocab_size` after the
instance has been created.
```python
>>> config = BPEConfig(300, [])
>>> config.vocab_size = 22
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'vocab_size'
```
This doesn't prevent you from modifying the contents of attributes that are
`list` or `dict` types.
@@ -0,0 +1,53 @@
# Check Precondition Before Click Arg Parsing
When setting up various [Click](https://click.palletsprojects.com/en/stable/)
subcommands with options, I ran into an issue with the order of some validation
checks. I was putting the same precondition validation logic at the beginning of
several subcommands. I was also putting callback validations on specific options
to those subcommands. Ideally the option validations could rely on those
precondition validations. However, the option callbacks run before anything in
the body of the subcommands.
The solution was to move those preconditions out of the subcommand body
(simplifying the subcommand) and into a `click.Command` subclass.
To demonstrate that, I'll first show the `click.Command` subclass:
```python
class RequireActiveSessionCommand(click.Command):
def parse_args(self, ctx, args):
if ctx.obj.active_session is None:
msg = "No active session being tracked. Start a session first."
raise click.UsageError(msg)
return super().parse_args(ctx, args)
```
The only thing this subclass overrides is `parse_args` where it gets ahead of
the standard arg parsing logic to first check the precondition. In this case, I
check that there is an active session. If there isn't, then I can raise a
`click.UsageError`. Otherwise, it delegates back to the super-class
implementation of `parse_args`.
This subclass then gets used for the commands that need to enforce this
precondition. Two prime examples of that are the `stop` and `cancel` subcommands.
```python
@cli.command(cls=RequireActiveSessionCommand)
@click.option("--at", help='Hours previous to end the timer, e.g. "2 hours ago"', callback=validate_stop_at)
@pass_cli
def stop(cli_ctx: CliContext, at: datetime) -> None:
# ... implementation omitted
@cli.command(cls=RequireActiveSessionCommand)
@pass_cli
def cancel(cli_ctx: CliContext):
# ... implementation omitted
```
Other subcommands, like `start` and `status` that don't need to enforce this
precondition use the `@cli.command()` decorator without passing in a custom
subclass.
This example is pulled directly from [this commit](https://github.com/jbranchaud/py-vmt/commit/505109b7a4013e05f085cded666c6b1ac7c3c250)
of my [`py-vmt` time tracker tool](https://github.com/jbranchaud/py-vmt).
@@ -0,0 +1,40 @@
# Control Passing Of Time In Tests
While it is nice to be able to write pure functional code, our software still
lives in the real world and may have to relate to or depend on the passing of
time. In order to test this kind of code, we need time to behave in a reliable,
deterministic way. One of the best ways to create a testing environment where
that is true is to bring in tooling that hijacks time.
The [`freezegun` module](https://github.com/spulec/freezegun) is a great tool
for that job. We can use it to freeze time at a specific testable point, advance
time a specific amount, and much more.
Here is an example from the tests for [my CLI-based time tracking
app](https://github.com/jbranchaud/py-vmt/blob/acb26e4840279d936a12f16c505ca7e75e9a6d20/tests/src/py_vmt/test_cli.py#L21)
where I freeze time before starting a session. That gives me a chance to assert
about the exact start time that is output by the command. Then I can advance
time a little and assert that the `status` command outputs the correct thing.
```python
import datetime
from freezegun import freeze_time
# some other test setup omitted ...
initial_datetime = datetime.datetime(
2026, 3, 14, 15, 5, 11, 0, datetime.timezone.utc
)
with freeze_time(initial_datetime) as frozen_datetime:
# start a session
start_result = runner.invoke(cli, ["start", "my-project"])
output = "Started tracking 'my-project' at 10:05AM"
assert output in start_result.output
frozen_datetime.tick(delta=datetime.timedelta(minutes=30))
# check status
status_result = runner.invoke(cli, ["status"])
output = "Tracking 'my-project' for 30m (since 10:05AM)"
assert output in status_result.output
```
@@ -0,0 +1,44 @@
# Create A Range Of Descending Values
A typical use of `range` looks something like this:
```python
>>> list(range(1, 5))
[1, 2, 3, 4]
```
Which is equivalent to this one where we give a `step` value of `1`.
```python
>>> list(range(1, 5, 1))
[1, 2, 3, 4]
```
If we try to create a _negative range_, that is, a range of values in decreasing
order, we get an empty list.
```python
>>> list(range(0, -7))
[]
```
That's because the `step` value still defaults to `1`. And there are no positive
steps between `0` and `-7`. So, let's give `range` a `step` value of `-1`.
```python
>>> list(range(0,-7, -1))
[0, -1, -2, -3, -4, -5, -6]
```
One practical use case of a negative range like this is using a list
comprehension to transform it into a list of the _last seven days_.
```python
>>> from datetime import datetime, timedelta
>>> [datetime.now().date() + timedelta(days=days) for days in range(0,-7, -1)]
[datetime.date(2026, 3, 19), datetime.date(2026, 3, 18), datetime.date(2026, 3, 17), datetime.date(2026, 3, 16), datetime.date(2026, 3, 15), datetime.date(2026, 3, 14), datetime.date(2026, 3, 13)]
```
Of course this could have been written with a positive range and then
subtracting the `timedelta`. I like that I have the option of doing this in
whatever way makes the code most readable.
+46
View File
@@ -0,0 +1,46 @@
# Deduplicate A List Into A Tuple
A `list` is not hashable which means you can't use it for things like `dict`
keys or cache keys. Instead you need to convert it into something like a `set`
or a `tuple`.
Here is an example list:
```python
>>> l1 = [3,4,1,2,5,4,1]
```
Turning this list into a `set` or `frozenset` is straightforward:
```python
>>> set(l1)
{1, 2, 3, 4, 5}
>>> frozenset(l1)
frozenset({1, 2, 3, 4, 5})
```
If you're trying to preserve the order after deduplicating, then you'll want to
use a `tuple` instead of a `set`. In order to deduplicate while maintaining the
ordering, you can exploit the fact that `dict` keys maintain their order. A
`list` can be transformed into the keys of a `dict` with
[`dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys):
```python
>>> dict.fromkeys(l1)
{3: None, 4: None, 1: None, 2: None, 5: None}
```
And here is your `tuple` which extracts the keys of the `dict`:
```python
>>> tuple(dict.fromkeys(l1))
(3, 4, 1, 2, 5)
```
By comparison, here is the `tuple` transformed directly from the `list` without
deduplication.
```python
>>> tuple(l1)
(3, 4, 1, 2, 5, 4, 1)
```
@@ -0,0 +1,60 @@
# Define Sequence Of Tests With Parametrize Decorator
I have a function that I want to test across a bunch of different inputs. That
way I can make sure the logic of that function handles all the different
scenarios I have in mind.
While working on [`py-vmt`](https://github.com/jbranchaud/py-vmt), I started by
writing a big single test function with a sequence of variable assignments and
`assert` statements. Here's my starting point:
```python
def test_format_time_delta_everything():
# less than a minute
thirty_seconds = timedelta(seconds=30)
assert "30s" == format_time_delta(thirty_seconds)
# one minute exactly
one_minute = timedelta(seconds=60)
assert "1m" == format_time_delta(one_minute)
# more than a minute
assert "1m30s" == format_time_delta(one_minute + thirty_seconds)
# bunch of minutes and seconds
delta = timedelta(minutes=24, seconds=8)
assert "24m8s" == format_time_delta(delta)
# one hour exactly
one_hour = timedelta(hours=1)
assert "1h" == format_time_delta(one_hour)
# more than one hour
assert "1h24m" == format_time_delta(one_hour + delta)
```
I knew I would eventually need to break it up into individual test functions,
but I couldn't bare to start there because it seemed quite repetitive.
There is another way to approach this without all the duplication. Pytest comes
with [a "parametrize" decorator](https://docs.pytest.org/en/stable/example/parametrize.html). This is
used to define a set of test data (and expected values) that will get passed
one-by-one to the test function as parameters.
```python
@pytest.mark.parametrize("input,expected", [
(timedelta(seconds=30), "30s"),
(timedelta(seconds=60), "1m"),
(timedelta(seconds=90), "1m30s"),
(timedelta(minutes=24, seconds=8), "24m8s"),
(timedelta(hours=1), "1h"),
(timedelta(hours=1, minutes=24, seconds=8), "1h24m"),
])
def test_format_time_delta(input, expected):
assert format_time_delta(input) == expected
```
I ditch all of the duplication this way. I define a list of tuples that
represent my input values and expected values. Then the body of the test can be
minimal. And I get a separate test execution for each parameter tuple making it
easier to see fine-grained pass/fail results.
@@ -0,0 +1,42 @@
# Define Typed Class Interface With Protocol
In [`py-vmt`](https://github.com/jbranchaud/py-vmt) I am defining different
storage access layers for the CLI to use. I want a consistent interface that the
core CLI logic can depend on regardless of whether it is a JSON file or a SQLite
database. To achieve that I can define a class of unimplemented functions that
inherits from
[`typing.Protocol`](https://typing.python.org/en/latest/spec/protocol.html).
```python
from typing import Protocol
class SessionRepository(Protocol):
def active_session(self) -> Session | None: ...
def write_active_session(self, session) -> None: ...
def append_session(self, session) -> None: ...
def all_sessions(self) -> list[Session]: ...
def clear_active_session(self) -> None: ...
```
Notice that none of these have default implementations. The `...` indicates that
class implementing this protocol will define the implementation of those
functions.
Now, my `CliContext` class, which needs some kind of `SessionRepository` to
function can indicate as much in `__init__`.
```python
class CliContext:
def __init__(self, verbose: bool, repo: SessionRepository | None = None) -> None:
self.verbose: bool = verbose
self.active_session: Session | None = None
self.repo: SessionRepository = repo or JsonRepository()
self.active_session = self.repo.active_session()
```
If `JsonRepository` doesn't define all of the methods specified in the protocol,
then a type error will occur wherever it clashes with `SessionRepository`. Now
as I implement `SqliteRepository` I have a standard interface to build against
that I know I can seamlessly swap in.
[source](https://typing.python.org/en/latest/reference/protocols.html#simple-user-defined-protocols)
@@ -0,0 +1,24 @@
# Enable Pyright Type Checking In Cursor
In most ways [Cursor](https://cursor.com/), a clone of VS Code, behaves like VS
Code and uses the same extensions as VS Code. It even offers to clone all your
existing extensions and setup from VS Code when you first install it.
However, the Pyright type checking setup that I had in VS Code stopped working
when I opened up the same Python project in Cursor. It seems that to get Pyright
to reliably work with forks of VS Code, you need to use a compatible fork like
[Based Pyright](https://docs.basedpyright.com/latest/).
Once I installed _Based Pyright_ from the extension marketplace, I was able to
enable it in `.vscode/settings.json`:
```json
{
...,
"basedpyright.analysis.typeCheckingMode": "basic"
}
```
I may have needed to restart Cursor at this point, I cannot remember exactly.
However, once this setup was in place the helpful type checking errors started
appearing as red squiggles.
@@ -0,0 +1,64 @@
# Get Absolute Seconds From `timedelta` Object
The [`timedelta` object provided by
`datetime`](https://docs.python.org/3/library/datetime.html#timedelta-objects)
is a useful built-in concept for representing a duration of time.
```python
>>> from datetime import timedelta
>>> diff = timedelta(hours=1, minutes=1, seconds=6)
>>> diff.seconds
3666
```
It is pretty minimal though. There are only a couple things you can inspect
about it -- `days`, `seconds` (as I did in the snippet above), and
`microseconds`.
And perhaps that is enough to hint at the issue I recently ran into with it --
specifically that you can access both `days` and `seconds`.
Let's look at what happens when I have a `timedelta` with more than a day worth
of seconds.
```python
>>> diff = timedelta(seconds=(3600 * 24 + 1))
>>> diff.seconds
1
>>> diff.days
1
```
I thought `seconds` was going to produce `86401` instead of `1`. The reason is
because any amount of duration over a day gets converted into the `days` value
and its the remaining time smaller than a day that is represented by `seconds`.
In my [original implementation of
`format_time_delta`](https://github.com/jbranchaud/py-vmt/blob/c14eaa56cf5f5c6d0120a95f04f95a6c87443e1c/src/py_vmt/time_helpers.py#L11-L14),
I was trying to build a relative time string by converting `seconds` into hours,
minutes, and seconds. That approach falls apart as soon as the delta is greater
than a day.
```python
def format_time_delta(diff) -> str:
hours, remainder = divmod(diff.seconds, 3600)
minutes, remainder = divmod(remainder, 60)
seconds = remainder
# ...
```
Instead, I needed to reach for [the `total_seconds()` function](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds).
This gives "the total number of seconds contained in the duration" and is
described as equivalent to `diff / timedelta(seconds=1)`.
Here is the [updated version of `format_time_delta`](https://github.com/jbranchaud/py-vmt/blob/ec1875a9d73552f5481e3945ddf522e94d0cc018/src/py_vmt/time_helpers.py?plain=1#L11-L16):
```python
def format_time_delta(diff: timedelta) -> str:
total_seconds = int(diff.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, remainder = divmod(remainder, 60)
seconds = remainder
```
@@ -0,0 +1,42 @@
# Get Quotient And Remainder In One Operation
While writing some custom code to transform a number of seconds into the
constituent hours, minutes, and seconds, I found myself needing to get both the
quotient and remainder from a division between two numbers.
```python
>>> import math
>>> math.floor(3666 / 3600)
1
>>> 3666 % 3600
66
```
Instead, I can use Python's built-in
[`divmod`](https://docs.python.org/3/library/functions.html#divmod) function to
compute both values in one statement.
```python
>>> divmod(3666, 3600)
(1, 66)
```
The result is a tuple with the first value being my quotient (in this case, the
number of hours) and the remainder (the remaining number of seconds).
This kind of operation is known as [Euclidian
Division](https://en.wikipedia.org/wiki/Euclidean_division).
Here is a snippet of some actual code where I use this in
[`py-vmt`](https://github.com/jbranchaud/py-vmt/blob/b9eae8b258e9fd720cfa3bb63b601225df352051/src/py_vmt/time_helpers.py#L14-L16):
```python
def format_time_delta(diff: timedelta) -> str:
total_seconds = int(diff.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, remainder = divmod(remainder, 60)
seconds = remainder
# ...
```
+41
View File
@@ -0,0 +1,41 @@
# Look Inside Pytest tmp_path
In [Isolate and Debug File Side-Effects with Pytest
`tmp_path`](https://www.visualmode.dev/isolate-and-debug-file-side-effects-with-pytest-tmp-path),
I wrote about how I use
[`tmp_path`](https://docs.pytest.org/en/stable/reference/reference.html#std-fixture-tmp_path)
in a Pytest fixture to test [my `py-vmt` CLI](https://github.com/jbranchaud/py-vmt). During testing of the CLI interface
via [`click`'s testing utilities](https://click.palletsprojects.com/en/stable/testing/), `vmt` creates,
modifies, and reads from files. Isolating that behavior with the `tmp_path`
fixture is useful because it prevents individual test cases from conflicting
with one another.
Here is what the fixture looks like at the top of my test file:
```python
# auto fixture for all test cases that monkeypatches the platform dirs to a tmp
# path so that test side-effects don't persist between runs
@pytest.fixture(autouse=True)
def use_tmp_platform_dirs(tmp_path, monkeypatch):
data_dir = tmp_path / "data"
config_dir = tmp_path / "config"
data_dir.mkdir()
config_dir.mkdir()
monkeypatch.setattr(CliContext, "get_data_dir", staticmethod(lambda: data_dir))
monkeypatch.setattr(CliContext, "get_config_dir", staticmethod(lambda: config_dir))
```
The root of the temp directory is located at `tempfile.gettempdir()` and the
directories from there are organized with this structure:
```
{temproot}/pytest-of-{user}/pytest-{num}/{testname}/
```
So, in the case of `vmt`, I can find the `config` and `data` dirs for a specific
test run here:
```bash
ls /var/folders/zc/q6gnvbgx6kq77828jn38716r0000gn/T/pytest-of-lastword/pytest-2/test_start_status_stop_flow0
config data
```
@@ -0,0 +1,45 @@
# Make Dataclass Sortable By Specific Field
One way to sort a list of some `dataclass` is to define the `key` parameter when
calling `sort` or `sorted` like I discussed in [Sort a List of Dataclass
Instances](sort-a-list-of-dataclass-instances.md):
```python
for date in sessions_grouped_by_day.keys():
sessions_grouped_by_day[date].sort(
key=lambda session: session.start_time.time()
)
```
But then that lambda for `key` needs to be defined everywhere you sort.
If the dataclass has a single, specific field that acts as a natural proxy for
sort order, then you can define that in the `dataclass` implementation with the
`__lt__` method.
As long as a class defines the _less than_ dunder method, it will be sortable.
Here is what that looks like for this `Session` dataclass:
```python
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass
class Session:
start_time: datetime
project_name: str
end_time: datetime | None = None
def __lt__(self, other):
if not isinstance(other, Session):
return NotImplemented
return self.start_time < other.start_time
# more methods below ...
```
This implementation of `__lt__` tells the sorting methods that _this_ (`self`)
instance of `Session` can be compared to some `other` instance of `Session` by
comparing their `start_time` values to see which is less than. The guard at the
beginning makes sure only instances of `Session` are being compared.
@@ -0,0 +1,48 @@
# Make Secure Temp File For Atomic Write
Two types of failure modes that can occur while writing to a shared file on the
file system are 1) a corrupted file due to a crash mid-write and 2) another
process reading a partial file mid-write.
One way I've handled this in [`py-vmt`](https://github.com/jbranchaud/py-vmt) is
to perform the write operations on a secure temp file and then use the OS-level
atomic `rename` operation. I do this by [creating a
`contextmanager`](https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager)
that uses
[`tempfile.mkstemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp)
and [`os.replace`](https://docs.python.org/3/library/os.html#os.replace).
Here is what the `contextmanager` looks like:
```python
from contextlib import contextmanager
from pathlib import Path
import os, tempfile
@contextmanager
def atomic_write(path: Path):
# write to a tmp file in the same directory, then atomically swap it
fd, temp_file_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
try:
with os.fdopen(fd, "w") as file:
yield file
os.replace(temp_file_path, path)
except BaseException:
os.unlink(temp_file_path)
raise
```
This explicitly creates a secure temp file in the same directory as the given
path with `.tmp` as the suffix. I then open the file descriptor using the
`os.fdopen` context manager (which will manage closing the file descriptor for
me). The `@contextmanager` decorator plus the `yield file` are what allow this
to be used as a `with` block. Once any file operations are done, then I use
`os.replace` to atomically swap out the original file with the temp file.
Here is how I use it to write updates to JSON data files:
```python
def write_active_session(self, session: Session) -> None:
with atomic_write(self.active_session_file) as file:
json.dump(session.marshal(), file)
```
@@ -0,0 +1,76 @@
# Reclassify Certain Packages As Dev Dependencies
When I first started working on [py-vmt](https://github.com/jbranchaud/py-vmt),
I wasn't differentiating certain test packages as _dev_ dependencies as opposed
to standard, production dependencies. This can lead to bloated installs across a
variety of distribution channels.
Notice that I have everything treated as production dependencies:
```bash
uv tree --no-dev
Resolved 18 packages in 2ms
py-vmt v0.1.0
├── click v8.3.1
├── dateparser v1.3.0
│ ├── python-dateutil v2.9.0.post0
│ │ └── six v1.17.0
│ ├── pytz v2026.1.post1
│ ├── regex v2026.2.28
│ └── tzlocal v5.3.1
├── freezegun v1.5.5
│ └── python-dateutil v2.9.0.post0 (*)
├── platformdirs v4.9.4
├── pytest v9.0.2
│ ├── iniconfig v2.3.0
│ ├── packaging v26.0
│ ├── pluggy v1.6.0
│ └── pygments v2.19.2
└── types-dateparser v1.3.0.20260211
(*) Package tree already displayed
```
`pytest`, `freezegun`, and `types-dateparser` are better suited as _dev_
dependencies.
I can reclassify them by moving them from `dependencies` into a `dev` dependency
group in `pyproject.toml`:
```toml
dependencies = ["click>=8.3.1", "dateparser>=1.3.0", "platformdirs>=4.9.4"]
[dependency-groups]
dev = ["freezegun>=1.5.5", "pytest>=9.0.2", "types-dateparser>=1.3.0.20260211"]
```
I only had `dependencies` before, so I had to add `[dependency-groups]` and `dev = []` to my `pyproject.toml` file.
I can then tell `uv` to sync up the installation and virtualenv based on the new
organization of the dependencies.
```bash
uv sync
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
Resolved 18 packages in 518ms
```
Now when I check the `--no-dev` tree of dependencies, it's just the essentials:
```bash
uv tree --no-dev
Resolved 18 packages in 1ms
py-vmt v0.1.0
├── click v8.3.1
├── dateparser v1.3.0
│ ├── python-dateutil v2.9.0.post0
│ │ └── six v1.17.0
│ ├── pytz v2026.1.post1
│ ├── regex v2026.2.28
│ └── tzlocal v5.3.1
└── platformdirs v4.9.
```
Another way to achieve this would have been to run `uv remove` and `uv add` with
the relevant sets of package names. In retrospect, I would have preferred using
that approach in the first place. If you're wanting to be pinned to specific
versions of certain packages, you'd have to be a little more careful to get this
right.
@@ -0,0 +1,62 @@
# Set Up Pyright Type Checking In GitHub
As I get into more of a PR workflow with my development of
[`py-vmt`](https://github.com/jbranchaud/py-vmt), I need to set up some basic CI
checks in GitHub. For starters I want the same `pyright` type checking that I
have locally to be run in CI for consistency.
Though my editor is set up to do Pyright type checking as I work locally, I can
also manually run it with:
```bash
$ uv run pyright
```
Pyright will look for the `tool.pyright` section in my `pyproject.toml` file
which currently looks like the following:
```toml
[tool.pyright]
include = ["src", "tests"]
```
I can get this same type checking in CI for PRs by adding the following
`.github/workflows/typecheck.yml` file:
```yaml
name: pyright
on:
pull_request:
push:
branches: [main]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Set up Python
run: uv python install
- name: Install dependencies
run: uv sync --all-extras --dev
- name: Run pyright
run: uv run pyright
```
This adds a single `typecheck` job that installs `uv`, `python`, and my project
dependencies, and then runs `uv run pyright` (just like I do locally) to perform
type checking. If `pyright` discovers any type errors, the job will fail and I
can view the output of the job to see what needs fixing. Once I have dealt with
everything, the job will quietly pass with a green check mark.
Here is [the PR](https://github.com/jbranchaud/py-vmt/pull/2) where I added this
CI job.
+38
View File
@@ -0,0 +1,38 @@
# Skip Specific Pytest Test Cases
While using a failing test case to build a small new feature for
[`py-vmt`](https://github.com/jbranchaud/py-vmt), I realized I needed to do some
refactoring first. It wasn't significant enough to warrant stashing my current
changes and switching to a different branch, so I kept all the changes around. I
did find the initial failing test distracting from the refactoring I was trying
to do. To temporarily shelve that failure, I can use a Pytest decorator to mark
it as _skipped_.
```python
@pytest.mark.skip(reason="not yet implemented")
def test_log_recent_activity():
runner = CliRunner()
# set up the data dir file with some existing session entries
initial_datetime = datetime.datetime(
2026, 3, 14, 15, 5, 11, 0, datetime.timezone.utc
)
with freeze_time(initial_datetime) as frozen_datetime:
# ...
```
The [`@pytest.mark.skip` decorator](https://docs.pytest.org/en/stable/how-to/skipping.html#skipping-test-functions)
tells the Pytest runner to skip of that specific test case instead of executing
it. In the test runner output, I'll see an `s` rather than a `.` or `F` and the
summary will include it in a count of skipped tests:
```
=========================== 3 failed, 4 passed, 1 skipped in 0.09s ===========================
```
Another way to think about this is to mark this test case as _expected to fail_
with `@pytest.mark.xfail`. That will display as an `x` and show up in the summary as:
```
=========================== 3 failed, 4 passed, 1 xfailed in 0.11s ===========================
```
@@ -0,0 +1,52 @@
# Sort A List Of Dataclass Instances
Sorting lists of scalar values (integers, strings, floats, even booleans) in
Python is simple because the natural ordering of the list elements will be used.
We can call `sorted` on the list and it _just works_.
```python
>>> items = ["orange", "apple", "banana", "mango"]
>>> sorted(items)
['apple', 'banana', 'mango', 'orange']
```
However, if we have a list of non-scalar values, it is a little more complex. We
have to give `sorted` some help with knowing how to sort things that don't have
a natural ordering.
Let's take this `dataclass` that represents a time-based `Session` as an
example.
```python
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass
class Session:
start_time: datetime
project_name: str
end_time: datetime | None = None
# plus several methods ...
```
If I have a list of `Session` instances that I want to sort, I have to give
`sorted` a `key` to sort on. In the case of these `Session` instances, we'll
pass a `lambda` that can be evaluated to determine the sort value (which needs
to be sortable). `datetime` instances are sortable and I want to sort these
sessions based on their `start_time` values.
Here is a snippet from my `py_vmt` CLI where I make sure that each list of
sessions in this day-by-day `dict` is sorted based on the `start_time`:
```python
for date in sessions_grouped_by_day.keys():
sessions_grouped_by_day[date].sort(
key=lambda session: session.start_time.time()
)
```
`sort` (and `sorted`) translates each item in the list to the values produced
by the lambda and then sorts them by those values.
[source](https://docs.python.org/3/howto/sorting.html)
+33
View File
@@ -0,0 +1,33 @@
# Sort Normalized Version Of Data
Let's say I have a list of names that I want to sort. However, because of
inconsistency in how the data was entered, sometimes those names are capitalized
and other times they are not. Using
[`methodcaller`](https://docs.python.org/3/library/operator.html#operator.methodcaller),
I can normalize the sorting `key` used when comparing list items.
First, let's look at calling `sorted` with the list and no `key`:
```python
>>> sorted(["butler", "Jemisin", "le guin", "Erdrich"])
['Erdrich', 'Jemisin', 'butler', 'le guin']
```
`butler` which starts with a `b` gets moved to the 3rd position because it is
lowercase.
To sort this list using a normalized comparison, we will use `methodcaller` to
create a callable out of `lower` which is then passed as the sort `key`:
```python
>>> from operator import methodcaller
>>> sorted(["butler", "Jemisin", "le guin", "Erdrich"], key=methodcaller("lower"))
['butler', 'Erdrich', 'Jemisin', 'le guin']
```
That's the sort order I was originally hoping for.
What `methodcaller` is doing is creating a callable function that will invoke
`lower` with each string instance as the target. Conceptually similar to
`"Erdrich".lower()` or even `getattr("Erdrich", "lower")()` (notice this needs
to be immediately invoked).
@@ -0,0 +1,27 @@
# Start The Debugger When A Test Errors
While working on [some
tests](https://github.com/jbranchaud/build-an-llm-from-scratch/blob/main/tests/chapter_02/test_bpe_tokenizer.py)
for my Byte Pair Encoding tokenizer, I was running into an unexpected test
failure. To better understand what was going on, I needed to inspect the state
of the program around the time the code raised an exception.
Instead of needing to manually set a breakpoint at the correct spot to begin
debugging, I can run the test with the Pytest-supported `--pdb` flag. That's
short for _python debugger_.
> Start the interactive Python debugger on errors or KeyboardInterrupt
What this does during a test run is opens you up to the interactive Python
debugger at the exact moment an exception is raised. This gives you the ability
to inspect values of the program state at that point in execution which could
help inform the needed fix.
```bash
uv run pytest -vv --pdb -k "test_train_bpe"
```
There I am running a specific test that matches against `-k "test_train_bpe"`
and the python debugger will start up if there is an error.
See `uv run pytest --help` for more details.
@@ -0,0 +1,48 @@
# Turn Method Into Cached Property On Class Instance
I have a class that encapsulates a few things including a somewhat expensive
data lookup from a file on disk. When this class is instantiated, it is
short-lived and the data that gets pulled from the file on disk is considered
fresh for the life of the instance.
```python
class CliContext:
def __init__(self, verbose: bool) -> None:
# ...
self.repo = JsonRepository()
# ...
def session_log(self) -> list[Session]:
return self.repo.load_session_log()
```
Because this method gets called from a couple places during a single lifecycle,
this class would benefit from caching it via the [`@cached_property`
decorator](https://docs.python.org/3/library/functools.html#functools.cached_property).
```python
from functools import cached_property
class CliContext:
def __init__(self, verbose: bool) -> None:
# ...
self.repo = JsonRepository()
# ...
@cached_property
def session_log(self) -> list[Session]:
return self.repo.load_session_log()
```
Now `session_log` can be treated like a property instead of a method. That means
when I want to load and access the session log, I can do `self.session_log` (no
parentheses) like I would any other property. The first time I reference it, the
method will run. Then that value will be cached and all subsequent references
will use that cache.
> Transform a method of a class into a property whose value is computed once and
> then cached as a normal attribute for the life of the instance.
Of course, anytime we use caching, we can create a footgun for ourselves. We
have to be careful that our program doesn't evolve in such a way where the
caching will create a subtle bug due to stale data.
@@ -0,0 +1,54 @@
# Use `__post_init__` For `dataclass` Validations
The [`dataclass`](https://docs.python.org/3/library/dataclasses.html) construct
is a handy stdlib way of modeling some data with many improvements over a `dict`
such as named attributes and type visibility.
```python
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class BPEConfig:
BASE_VOCAB_SIZE: ClassVar[int] = 256
vocab_size: int
special_tokens: list[str]
```
I want to enhance `BPEConfig` a little by validating the `vocab_size` which
cannot be less than the `BASE_VOCAB_SIZE`. The
[`__post_init__`](https://docs.python.org/3/library/dataclasses.html#dataclasses.__post_init__)
method is a good place for this kind of validation.
```python
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class BPEConfig:
BASE_VOCAB_SIZE: ClassVar[int] = 256
vocab_size: int
special_tokens: list[str]
def __post_init__(self):
if self.vocab_size < self.BASE_VOCAB_SIZE:
msg = f"vocab_size ({self.vocab_size}) must be greater than or equal to BASE_VOCAB_SIZE ({self.BASE_VOCAB_SIZE})"
raise ValueError(msg)
```
With this in place, my program will fail fast if I try to use an invalid
`vocab_size`:
```python
>>> BPEConfig(22, [])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 5, in __init__
File "/Users/lastword/dev/misc/build-an-llm/chapter_02/bpe_tokenizer.py", line 24, in __post_init__
raise ValueError(msg)
ValueError: vocab_size (22) must be greater than or equal to BASE_VOCAB_SIZE (256)
```
This example is pulled directly from [the `BPETokenizer` I'm building](https://github.com/jbranchaud/build-an-llm-from-scratch/blob/d3fd0acd65c3e7419b2d15a64c8d74266d0488f6/chapter_02/bpe_tokenizer.py#L14-L24).
@@ -0,0 +1,64 @@
# Validate Click Option With Callback
I have a [click](https://click.palletsprojects.com/en/stable/) subcommand in my
[`py-vmt` project](https://github.com/jbranchaud/py-vmt) that includes an
`option` specified with the `--at` flag. This is what it originally looked like:
```python
# define `start` subcommand
@cli.command()
@click.argument("project-name")
@click.option("--at", help='Relative time in past to start the time, e.g. "2 hours ago", "33 minutes ago"')
@pass_cli
def start(cli_ctx: CliContext, project_name: str, at: str | None) -> None:
# ...
```
The value of `at` needs to be in the past. I need a way validate that it is or
otherwise bail early with a useful error message. The optional
[`callback`](https://click.palletsprojects.com/en/stable/advanced/#callbacks-for-validation)
to `@click.option` plus `click.BadParameter` are a good way to handle that.
First, I define a callback handler that does the validation. I even take it a
step further and have it return the transformed value (`datetime`) that the
subcommand logic will need.
```python
def validate_past_time(_ctx, _param, value: str | None) -> datetime:
now = datetime.now(timezone.utc)
if value == None:
return now
start_at = time_helpers.parse_to_datetime(value)
if start_time == None or start_at > now:
raise click.BadParameter("must be a relative time in the past")
return start_at
```
I ignore the first two arguments because I only need to work with `value`. Value
might be something like `"33 minutes ago"` and I attempt to transform that with
`dateparser` into a `datetime` instance. If it can't be parsed or it isn't in
the past, then I raise `click.BadParameter` which presents the user with useful
usage details.
This callback can then be incorporated into the subcommand like so:
```python
# define `start` subcommand
@cli.command()
@click.argument("project-name")
@click.option(
"--at",
help='Relative time in past to start the time, e.g. "2 hours ago", "33 minutes ago"',
callback=validate_past_time
)
@pass_cli
def start(cli_ctx: CliContext, project_name: str, at: datetime) -> None:
# ...
```
Now I can expect the incoming `at` option to be a `datetime` which helps
simplify several lines of logic in the `start` implementation.
@@ -0,0 +1,37 @@
# Define Conditional Routing Logic In Routes File
I ran into a situation recently where I needed to intercept the behavior a
common public-facing route in an app. Broadly, the route is for company specific
rental pages with query parameters that correspond to their available inventory.
What I needed was a way to display a demo version of that rental page ignoring
everything else about how the request would otherwise be processed, validated,
and rendered.
Instead of introducing a bunch of weird conditional logic into this already
complex rental controller, I was able to intercept the request at the routing
layer when `demo=true` is set and send it to a different controller.
Here is what that section of `config/routes.rb` looks like:
```ruby
get "rentals/new", to: "rental_demos#show",
as: :rental_demo,
constraints: ->(request) { request.params[:demo] == "true" }
resources :rentals, only: %i[new create] do
# ...
end
```
This specifies a `constraint` on the `get` handler matching for a given request.
If the constraint isn't met, then the route handling logic proceeds where it
will instead find a match with the original new rentals resource routing.
Now I can reference a version of this URL that includes `demo=true` as a way of
having an always-available realistic-looking version of the rental page even if
one of these companies doesn't actively have available inventory.
Those requests will get intercepted by the first matching route handler which
will send them to the `RentalDemosController` instead of the
`RentalsController`.
@@ -0,0 +1,46 @@
# Halt ActionMailer Delivery With Callback
`ActionMailer` supports callbacks, similar to `ActiveRecord`, like
`before_deliver` and `after_delivery`. We can hook into the `before_deliver`
callback to interrupt the delivery of an email that shouldn't go out.
Here's the scenario: you schedule a bunch of payment reminders to go out to your
customers that still need to make their latest payment. Let's say the daily job
that schedules all of these reminders runs in the middle of the night, but
schedules the emails to land in inboxes at a more reasonable time, like 10am.
Between the time that the email is scheduled and it gets processed for delivery,
a customer makes their payment. In that case, we no longer want to send that
person an email reminder.
To handle this scenario, we can have a `before_deliver` callback that checks the
user's balance and raises `:abort` to halt the callback execution chain,
effectively preventing the email from going out. We can even scope the callback
to just the actions we care about using the `if` option and checking the
`action_name`.
```ruby
class UserMailer < ApplicationMailer
before_deliver :abort_if_payment_is_current,
if: -> { action_name.in?(%w[payment_reminder past_due_invoice]) }
def payment_reminder
# ...
end
def past_due_invoice
# ...
end
private
def abort_if_payment_is_current
if @user.check_latest_balance.zero?
raise :abort
end
end
end
```
See [Action Mailer
Callbacks](https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-callbacks)
for more details.
+46
View File
@@ -0,0 +1,46 @@
# Define A Set Of Class Methods
The most common way to define class methods is by defining them directly with
`self` (the class in the current context) on a method by method basis:
```ruby
class User
def self.find_by(attrs)
# lookup logic ...
end
end
```
If you have a group of class methods you want to define, you can stick them all
within a `class << self` block which does similarly defines each of them as
singleton methods of that class (`User` in this case):
```ruby
class User
class << self
def find_by_email(email)
# lookup logic ...
end
def find_by_last_name(last_name)
# lookup logic ...
end
end
end
```
This opens the singleton class of `User` for modification, adding these two new
methods.
We can see those defined alongside all other direct and inherited class methods:
```ruby
> User.methods
=>
[:find_by_email,
:find_by_last_name,
:yaml_tag,
:allocate,
...
]
```
+37
View File
@@ -0,0 +1,37 @@
# Filter By Type
In Ruby, we have several ways to check if something is a certain type (class or
subclass). A couple common approaches you might see are `#is_a?` and `===`
(case equality operator):
```ruby
> 3.is_a?(Integer)
=> true
> Integer === 3
=> true
> 3 === Integer
=> false
```
Notice it is important to get the ordering of class and value right when using
`===`.
We can use these concepts to filter collections down to just those values of a
certain type. We can also ditch those methods and instead use
[`#grep`](https://ruby-doc.org/3.4.1/Enumerable.html#method-i-grep) to pattern
match on the type directly.
```ruby
> nums = [1, :two, 3.0, 'four', 5, -> { 6 }, 0.7]
=> [1, :two, 3.0, "four", 5, #<Proc:0x0000000123af0338 (irb):5 (lambda)>, 0.7]
> nums.filter { it.is_a?(Numeric) }
=> [1, 3.0, 5, 0.7]
> nums.filter { Integer === it }
=> [1, 5]
> nums.grep(Integer)
=> [1, 5]
> nums.grep(Numeric)
=> [1, 3.0, 5, 0.7]
```
[source](https://bsky.app/profile/lucianghinda.com/post/3mhi5xp3xhk25)
@@ -0,0 +1,63 @@
# Use Rescue As Part Of Inline Statement
In Ruby I typically think of `rescue` as block syntax that I can use to handle
exceptions.
```ruby
begin
User.update!(password:)
rescue
puts "There was an issue updating the password"
end
```
The `rescue` keyword can also be used as part of an inline statement as a way of
providing a _fallback_ value when the first part of the statement raises.
For instance, if I'm trying to access some value on an array that happens to be
`nil`, it is going to raise:
```ruby
> scores.first
(irb):7:in '<main>': undefined method 'first' for nil (NoMethodError)
```
I can instead tack on a `rescue 0` which will give it `0` as a fallback value:
```ruby
> scores.first rescue 0
=> 0
```
Of course, there are more idiomatic ways to handle this kind of situation in
Ruby. Maybe something like this:
```ruby
> Array(scores).first || 0
=> 0
```
Another way I've seen this inline rescue used is to print out the exception
caused by that line of code, using `$!` (the global variable for the most
recently raised exception).
```ruby
> scores.first rescue puts $!
undefined method 'first' for nil
=> nil
```
That is a one-liner for the following:
```ruby
begin
scores.first
rescue => e
puts e
end
```
The big caveat that goes with this is the same one that goes with any other
blanket `rescue` block. If you are indiscriminately rescuing exceptions without
being intentional about what you are rescuing and why, you could be potentially
burying exceptions that you need to know about.
@@ -0,0 +1,52 @@
# Add Default Task To List All Tasks
One thing I like about [`just`](https://github.com/casey/just) is that if you
run `just` by itself, the default behavior is to list out all the commands it
can run.
[Taskfile](https://github.com/go-task/task) technically does this as well, but
with a warning at the end:
```
task
task: Available tasks for this project:
* notes: Interactive picker for notes tasks
* notes:diff: Show uncommitted changes in notes
* notes:edit: All-in-one edit, commit, and push notes
* notes:log: Show recent commit history for notes
* notes:open: Opens NOTES.md (syncs latest changes first) in default editor
* notes:push: Commit and push changes to notes submodule
* notes:status: Check status of notes submodule
* notes:sync: Sync latest changes from the notes submodule
task: Task "default" does not exist
```
I prefer to tidy this up a little by adding `task --list` as the _default_ in my
`Taskfile.yml`.
```yml
default:
desc: Show available commands
cmds:
- task --list
```
Now when I run `task` with no arguments, I get this minutely nicer version:
```
task
Alias tip: t
task: [default] task --list
task: Available tasks for this project:
* default: Show available commands
* notes: Interactive picker for notes tasks
* notes:diff: Show uncommitted changes in notes
* notes:edit: All-in-one edit, commit, and push notes
* notes:log: Show recent commit history for notes
* notes:open: Opens NOTES.md (syncs latest changes first) in default editor
* notes:push: Commit and push changes to notes submodule
* notes:status: Check status of notes submodule
* notes:sync: Sync latest changes from the notes submodule
```
Notice there is no `task: Task "default" does not exist` warning at the end.
+22
View File
@@ -0,0 +1,22 @@
# Browse And Search Help Docs
There are a lot of tools that don't have dedicated `man` pages, but do have
lengthy output when you pass them the `--help` flag.
We can make those details easier to browse and searchable by piping them to
`less`.
```bash
uv run pytest --help | less
```
First, we see the top of the output inside `less` instead of bottom of the
output right above our next terminal prompt.
From `less`, we can use down and up arrows (or `j` and `k`) to navigate through
the details. We can also jump to a specific word or phrase by searching -- type
`/` and then the pattern we're trying to match. `n` and `N` to go to the next or
previous match, respectively.
See `man less` more more details. And if you like these improvements to viewing
tool usage details, you may also be interested in [a better man page viewer](https://www.visualmode.dev/a-better-man-page-viewer).
@@ -0,0 +1,71 @@
# Deduplicate List While Preserving Original Order
Usually when I want to deduplicate a list coming out of some command, I'll reach
for `sort | uniq`. This is a nice Unix trick where `uniq` removes consecutive
duplicate lines which relies on `sort` first reorganizing all lines in
alphabetically sorted order, bringing all duplicate lines together.
The caveat to using `sort | uniq` (or even `sort -u`) is that it will reorder
entries alphabetically. That means you'll lose the original order, which may
have been important.
```bash
echo "red green blue red yellow green blue red green" | tr ' ' '\n' | sort -u
blue
green
red
yellow
```
Another approach is to use `awk` which can deduplicate while preserving the
order of entries as they first appear. This can be done with a pattern that
records the count of each line in an associative array.
```bash
echo "red green blue red yellow green blue red green" | tr ' ' '\n' | awk '!seen[$0]++'
red
green
blue
yellow
```
The above pattern accepts on the first occurrence of each line and rejects on
any subsequent occurrences. That is done by adding `$0` (the current line) to
`seen` (associative array that auto-initializes inline). If it doesn't exist in
`seen` yet, then `0` is returned which is negated to a truthy value with `!`.
That entry is then incremented from `0` to `1` via the `++`. As `awk` continues
to process each line, `seen` is continually added to and incremented. The
default _action_ for `awk` is to print the line. Those truthy lines are the ones
that are printed.
An example of where this might be useful is when creating a unique listing of
all authors of a git repository while maintaining the order that they become
committers. I wanted to show this with a high-contribution public repo that I
worked on, so I referenced the [`egghead-next`
repo](https://github.com/skillrecordings/egghead-next).
```bash
git log --reverse --format='%an <%ae>' | awk '!seen[$0]++'
Joel Hooks <joelhooks@gmail.com>
johnlindquist <johnlindquist@gmail.com>
John Lindquist <johnlindquist@gmail.com>
William Johnson <w.alexander.johnson@gmail.com>
depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Evgeniy Nagalskiy <evgeniy.nagalskiy@gmail.com>
Taylor Bell <taylorbell@gmail.com>
Maggie Appleton <maggie.fm.appleton@gmail.com>
John Lindquist <johnlindquist@work.local>
Vojta Holik <vojta@egghead.io>
Daniel Miller <dealingwith@gmail.com>
jh3y <jh3y@users.noreply.github.com>
Jhey Tompkins <jh3y@users.noreply.github.com>
Josh Branchaud <jbranchaud@gmail.com>
Lauro Silva <57044804+laurosilvacom@users.noreply.github.com>
LB <barth.laurie@gmail.com>
kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
samuelhulick <samuel@samuelhulick.com>
Ian Jones <jones58ian@gmail.com>
Zac Jones <zacjones93@gmail.com>
...
```
@@ -0,0 +1,28 @@
# Print Out File With Bat Without Formatting
The [`bat`](https://github.com/sharkdp/bat) utility is my daily driver and
replacement for anything used `cat` for before. I even have `bat` aliased to
`cat` so that I never had to rewire my muscle memory for typing `cat`.
Whether or not the creator of `cat` intended it, I'd guess that most terminal
users' main use case is printing the contents of a file. `bat` does that way
better with syntax highlighting, line numbers, and some layout formatting that
puts lines around the output and a heading with the filename.
All this formatting is great when I'm taking a quick look at a file. One way it
gets in the way is when I'm trying to highlight and copy a few lines to my
clipboard. Because the terminal is rendering lines, line numbers, and other
formatting, all that fluff gets included on the clipboard.
For this scenario, I can use the `-p` flag (or `--style=plain`) to print just
the (syntax-highlighted) file contents without all the extra formatting.
```bash
bat -p app/models/users.rb
# or
bat --style=plain app/models/users.rb
```
Another way I could have approached this was to [ignore the alias of `cat` to
`bat`](ignore-the-alias-when-running-a-command.md).
+64
View File
@@ -0,0 +1,64 @@
# Reverse Each Line Of A File
The [`rev` command](https://man7.org/linux/man-pages/man1/rev.1.html) can be
used to reverse each line in a file. Every line is left where it is relative to
other lines, but the contents of each line is reversed.
So a file that contains the following text:
```bash
cat stuff.md
Three
Two
One
go racecar go
```
can be piped to `rev` to get the following output:
```bash
rev stuff.md
eerhT
owT
enO
og racecar og
```
This is an odd utility that doesn't have too much use that I can imagine. After
a brief chat with Claude where I asked for some practical use cases, the one
that stood out the most to me is to reverse a list of filenames, sort them, and
then reverse them again (putting them back in readable order). This can shuffle
filenames with similar endings near each other like source and test files.
Here is a list of files for me [`py-vmt`
project](https://github.com/jbranchaud/py-vmt):
```bash
fd -t f .
README.md
pyproject.toml
src/py_vmt/__init__.py
src/py_vmt/cli.py
src/py_vmt/session.py
src/py_vmt/time_helpers.py
tests/src/py_vmt/test_cli.py
tests/src/py_vmt/test_session.py
```
Now I can pipe the output of that `fd` command through `rev | sort | rev` to get
my files organized in a different way.
```bash
fd -t f . | rev | sort | rev
README.md
pyproject.toml
src/py_vmt/__init__.py
tests/src/py_vmt/test_cli.py
src/py_vmt/cli.py
tests/src/py_vmt/test_session.py
src/py_vmt/session.py
src/py_vmt/time_helpers.py
```
Again the value of doing something like this is a bit tenuous. At the very least
it is fun to know about.
@@ -0,0 +1,30 @@
# Use The Readline Keybindings Anywhere
There are these features of the "shell" that I've often heard called _emac
keybindings_. These are things like `ctrl-a` (move the cursor to the beginning
of the line) and `ctrl-e` (move the cursor to the end of the line) that I use
every single day. There are several others that are in my heavy rotation,
however, I learned about a couple more reading through [Shell Tricks That
Actually Make Life Easier (And Save Your
Sanity)](https://blog.hofstede.it/shell-tricks-that-actually-make-life-easier-and-save-your-sanity/).
These are [Readline commands](https://www.gnu.org/software/bash/manual/html_node/Bindable-Readline-Commands.html)
(or keybindings) which means they are supported by anything that uses Readline
under the hood. So while you might be using these to great effect in `bash` and
`zsh`, you should look for other places they are available.
A non-exhaustive list includes:
- Ruby's `irb`
- Python's `python`
- Node.js' `node`
- PostgreSQL's `psql`
- Claude Code
And many more similar REPLs and command line tools.
Try these keybindings out in one of your favorites and when you're done hit
`ctrl-c` to exit out of it.
PS. subsets of these keybindings are sometimes supported in unexpected places
like the Chrome URL bar.
@@ -0,0 +1,25 @@
# Show All Linear Keyboard Shortcuts
Linear, the project management software, puts an incredible amount of attention
to detail into the UX and UI of their app. This includes making the app a power
tool for power users with tons of keyboard shortcuts.
I'm aware of some of Linear's keyboard shortcuts, but the discoverability of
many of them is tough.
A great way to list and browse through all of them right in the app is with
`Cmd+/`.
They are organized into sections that I can scroll through. There is also a
search box at the top of this _Keyboard Shortcuts_ panel where I can narrow down
the results to those that match a term.
A few that I'm finding immediately useful are:
- `gi` to go to my _Inbox_ in the current workspace
- `gm` to go to _My Issues_ in the current workspace
- `ow` to open a picker to switch between workspaces
Note: the _Keyboard Shortcuts_ panel lists many of the letter-based shortcuts as
being capitalized. I've found that these don't work when I hold shift. For that
reason, I've listed the above shortcuts with lowercase letters.
@@ -0,0 +1,40 @@
# View Nicely Formatted Markdown From Terminal
The [`glow`](https://github.com/charmbracelet/glow) utility is CLI markdown
renderer written in Go. It is part of the CCU
([charmbraclet](https://github.com/charmbracelet) CLI universe). And yes, I just
made up _CCU_.
`glow` is great because it processes and outputs a markdown file with some
styling tailored to a terminal including:
- colors to emphasize things like headings
- styling of inline code snippets
- syntax highlighting for fenced code blocks
- rendering of markdown tables
- and a lot more that I'm not thinking to mention
In the past I've installed this with `brew`, but I currently manage my `glow`
install with [this mise config](https://github.com/jbranchaud/dotfiles/blob/main/config/mise/config.toml?plain=1#L66).
To view a nicely rendered markdown file, I can run:
```bash
$ glow README.md
```
For long markdown files like [this `README.md`](https://github.com/jbranchaud/til/blob/master/README.md), this
doesn't work too well because it renders until the end and spits you at at the
bottom.
Fortunately, `glow` has a built-in pager that maintains all the styling while
allowing you to navigate and search similar to `less`.
```bash
$ glow -p README.md
```
There is also a TUI version (`-t`), but I find that less intuitive and useful
than the pager.
See `glow --help` for more details.
+44
View File
@@ -0,0 +1,44 @@
# List Available Zle Keybindings
Unlike `bash` which uses `readline`, `zsh` has its own implementation of a line
editing library -- `zle`. A lot of the core bindings between the two are the
same, e.g. `Ctrl-a` and `Ctrl-e` to go the beginning and end of the command line
prompt, respectively.
All available `zle` keybindings can be listed out by running `bindkey` without
any arguments.
The best way to check out an unaltered version of this list is by starting a
fresh `zsh` process with no RCS files loaded in. The `-f` flag does that. Note
though that when `zsh` is starting fresh, it has to decide whether to start in
_Emacs_ mode or _Vi_ mode. If it sees that your default editor is something like
`vi`, `vim` or `nvim`, then it will start you in _Vi_ mode.
Starting in _Vi_ mode can be confusing because none of the standard _Emacs_
keybindings like `Ctrl-a` and `Ctrl-e` are available in that context. So first
ensure you're in _Emacs_ mode by running:
```sh
zsh -f
lastword% bindkey -e
```
Now you can list out all the keybindings:
```sh
lastword% bindkey
"^@" set-mark-command
"^A" beginning-of-line
"^B" backward-char
"^D" delete-char-or-list
"^E" end-of-line
"^F" forward-char
"^G" send-break
"^H" backward-delete-char
"^I" expand-or-complete
"^J" accept-line
"^K" kill-line
...
```
See `man zshzle` for more details on `zle` and `bindkey`.