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

Compare commits

25 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
25 changed files with 1102 additions and 1 deletions
+28 -1
View File
@@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/).
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter). For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
_1787 TILs and counting..._ _1810 TILs and counting..._
See some of the other learning resources I work on: See some of the other learning resources I work on:
@@ -60,6 +60,7 @@ If you've learned something here, support my efforts writing daily TILs by
* [Linux](#linux) * [Linux](#linux)
* [LLM](#llm) * [LLM](#llm)
* [Mac](#mac) * [Mac](#mac)
* [Math](#math)
* [Mise](#mise) * [Mise](#mise)
* [MongoDB](#mongodb) * [MongoDB](#mongodb)
* [MySQL](#mysql) * [MySQL](#mysql)
@@ -165,6 +166,7 @@ If you've learned something here, support my efforts writing daily TILs by
### Claude Code ### Claude Code
- [Allow Edits From The Start](claude-code/allow-edits-from-the-start.md) - [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) - [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) - [Open Current Prompt In Default Editor](claude-code/open-current-prompt-in-default-editor.md)
- [Resume Specific Session](claude-code/resume-specific-session.md) - [Resume Specific Session](claude-code/resume-specific-session.md)
@@ -239,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 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 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) - [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) - [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) - [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) - [Determine The IP Address Of A Domain](devops/determine-the-ip-address-of-a-domain.md)
@@ -406,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) - [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) - [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) - [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) - [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) - [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) - [Quicker Commit Fixes With The Fixup Flag](git/quicker-commit-fixes-with-the-fixup-flag.md)
@@ -467,6 +471,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [List PRs Awaiting Your Review](github/list-prs-awaiting-your-review.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 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) - [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) - [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) - [Tell gh What The Default Repo Is](github/tell-gh-what-the-default-repo-is.md)
@@ -620,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 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 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 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 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) - [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) - [Immutable Remove With The Spread Operator](javascript/immutable-remove-with-the-spread-operator.md)
@@ -732,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) - [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) - [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) - [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) - [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) - [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) - [Convert An HEIC Image File To JPG](mac/convert-an-heic-image-file-to-jpg.md)
@@ -749,6 +756,7 @@ 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) - [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) - [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) - [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) - [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) - [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) - [Resizing Both Corners Of A Window](mac/resizing-both-corners-of-a-window.md)
@@ -764,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) - [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) - [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 ### Mise
- [Create Umbrella Task For All Test Tasks](mise/create-umbrella-task-for-all-test-tasks.md) - [Create Umbrella Task For All Test Tasks](mise/create-umbrella-task-for-all-test-tasks.md)
@@ -1051,18 +1063,23 @@ If you've learned something here, support my efforts writing daily TILs by
- [Access Instance Variables](python/access-instance-variables.md) - [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 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) - [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) - [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) - [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) - [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 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) - [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 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) - [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) - [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 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) - [Dunder Methods](python/dunder-methods.md)
- [Easy Key-Value Aggregates With defaultdict](python/easy-key-value-aggregates-with-defaultdict.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 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) - [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 First N Items From Enumerable](python/iterate-first-n-items-from-enumerable.md)
- [Iterate Over A Dictionary](python/iterate-over-a-dictionary.md) - [Iterate Over A Dictionary](python/iterate-over-a-dictionary.md)
@@ -1070,18 +1087,22 @@ If you've learned something here, support my efforts writing daily TILs by
- [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.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) - [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 Dataclass Sortable By Specific Field](python/make-dataclass-sortable-by-specific-field.md)
- [Make Secure Temp File For Atomic Write](python/make-secure-temp-file-for-atomic-write.md)
- [Override The Boolean Context Of A Class](python/override-the-boolean-context-of-a-class.md) - [Override 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) - [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) - [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) - [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 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) - [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) - [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) - [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) - [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 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 `__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) - [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 ### Rails
@@ -1137,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 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 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) - [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) - [Define The Root Path For The App](rails/define-the-root-path-for-the-app.md)
- [Delete Paranoid Records](rails/delete-paranoid-records.md) - [Delete Paranoid Records](rails/delete-paranoid-records.md)
- [Demodulize A Class Name](rails/demodulize-a-class-name.md) - [Demodulize A Class Name](rails/demodulize-a-class-name.md)
@@ -1167,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 Column Names For A Model](rails/get-the-column-names-for-a-model.md)
- [Get The Current Time](rails/get-the-current-time.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) - [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) - [Handle Named Arguments In A Rake Task](rails/handle-named-arguments-in-a-rake-task.md)
- [Hash Slicing](rails/hash-slicing.md) - [Hash Slicing](rails/hash-slicing.md)
- [Ignore Poltergeist JavaScript Errors](rails/ignore-poltergeist-javascript-errors.md) - [Ignore Poltergeist JavaScript Errors](rails/ignore-poltergeist-javascript-errors.md)
@@ -1552,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) - [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 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 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) - [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) - [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) - [What To Do When You Don't Rescue](ruby/what-to-do-when-you-dont-rescue.md)
@@ -1700,6 +1724,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Curl With Cookies](unix/curl-with-cookies.md) - [Curl With Cookies](unix/curl-with-cookies.md)
- [Curling For Headers](unix/curling-for-headers.md) - [Curling For Headers](unix/curling-for-headers.md)
- [Curling With Basic Auth Credentials](unix/curling-with-basic-auth-credentials.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) - [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) - [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) - [Different Ways To Generate A v4 UUID](unix/different-ways-to-generate-a-v4-uuid.md)
@@ -1793,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 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 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 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 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) - [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) - [Produce A Lowercase V4 UUID](unix/produce-a-lowercase-v4-uuid.md)
@@ -2067,6 +2093,7 @@ 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 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) - [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) - [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) - [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) - [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) - [Toggle Between Stories In Storybook](workflow/toggle-between-stories-in-storybook.md)
+30
View File
@@ -11,6 +11,36 @@ tasks:
cmds: cmds:
- task --list - 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: notes:
desc: Interactive picker for notes tasks desc: Interactive picker for notes tasks
cmds: 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,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,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.
@@ -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"
```
@@ -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,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,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,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,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,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
# ...
```
@@ -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,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.
@@ -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,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.
@@ -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,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).
@@ -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.