mirror of
https://github.com/jbranchaud/til
synced 2026-07-06 17:20:33 +00:00
Compare commits
25 Commits
3402428aad
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d4766dad95 | |||
| 5f4308c1af | |||
| 8c463a90e3 | |||
| 329267c7a2 | |||
| 12180acb02 | |||
| 8f722061b4 | |||
| a126b13d7c | |||
| 0ae3f14c25 | |||
| b91527db30 | |||
| c8f8c2c1a3 | |||
| c397e35ffd | |||
| bbb28fd811 | |||
| 950e2f861a | |||
| 20bbdb6d55 | |||
| 03c11f9042 | |||
| cea6c75e1c | |||
| 539cbbefa6 | |||
| 3eddb54053 | |||
| 49628a7849 | |||
| 3919b721cd | |||
| 9c65e5c0b3 | |||
| e58ffffda0 | |||
| d87e125472 | |||
| facc606014 | |||
| be103b52dd |
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user