mirror of
https://github.com/jbranchaud/til
synced 2026-07-06 09:10:34 +00:00
Compare commits
52 Commits
eb0a7e1b3d
...
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 | |||
| 3402428aad | |||
| 0c00e47141 | |||
| 1c90fdd823 | |||
| bf3991ce04 | |||
| 3db5af78c1 | |||
| a8c35e2458 | |||
| c0ad3cee4d | |||
| 9bd1bb413a | |||
| ab8331000f | |||
| cd54360925 | |||
| 75421685ea | |||
| 0cb5890fc0 | |||
| 7de0e70d78 | |||
| 36934aa56f | |||
| 2cd465bb08 | |||
| 6ad376885b | |||
| 0c4702be97 | |||
| b873f86f5b | |||
| 1120bb2018 | |||
| 906253b7dc | |||
| b4920c0397 | |||
| 119cc15c9a | |||
| 1a4589f8f7 | |||
| 5f35404433 | |||
| 1766e45134 | |||
| c875652725 | |||
| 8af252f232 |
@@ -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).
|
||||||
|
|
||||||
_1763 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,9 +166,11 @@ 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)
|
||||||
|
- [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md)
|
||||||
|
|
||||||
### Clojure
|
### Clojure
|
||||||
|
|
||||||
@@ -238,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)
|
||||||
@@ -361,6 +365,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Determine Absolute Path Of Top-Level Project Directory](git/determine-absolute-path-of-top-level-project-directory.md)
|
- [Determine Absolute Path Of Top-Level Project Directory](git/determine-absolute-path-of-top-level-project-directory.md)
|
||||||
- [Determine The Hash Id For A Blob](git/determine-the-hash-id-for-a-blob.md)
|
- [Determine The Hash Id For A Blob](git/determine-the-hash-id-for-a-blob.md)
|
||||||
- [Diffing With Patience](git/diffing-with-patience.md)
|
- [Diffing With Patience](git/diffing-with-patience.md)
|
||||||
|
- [Display All Git Log Entries In My Local Timezone](git/display-all-git-log-entries-in-my-local-timezone.md)
|
||||||
- [Dropping Commits With Git Rebase](git/dropping-commits-with-git-rebase.md)
|
- [Dropping Commits With Git Rebase](git/dropping-commits-with-git-rebase.md)
|
||||||
- [Dry Runs in Git](git/dry-runs-in-git.md)
|
- [Dry Runs in Git](git/dry-runs-in-git.md)
|
||||||
- [Exclude A File From A Diff Output](git/exclude-a-file-from-a-diff-output.md)
|
- [Exclude A File From A Diff Output](git/exclude-a-file-from-a-diff-output.md)
|
||||||
@@ -404,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)
|
||||||
@@ -462,7 +468,10 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
### GitHub
|
### GitHub
|
||||||
|
|
||||||
- [Access Your GitHub Profile Photo](github/access-your-github-profile-photo.md)
|
- [Access Your GitHub Profile Photo](github/access-your-github-profile-photo.md)
|
||||||
|
- [List PRs Awaiting Your Review](github/list-prs-awaiting-your-review.md)
|
||||||
- [Open A PR To An Unforked Repo](github/open-a-pr-to-an-unforked-repo.md)
|
- [Open A PR To An Unforked Repo](github/open-a-pr-to-an-unforked-repo.md)
|
||||||
|
- [Open File To Specific Line In Browser](github/open-file-to-specific-line-in-browser.md)
|
||||||
|
- [Process JSON Output From gh With jq](github/process-json-output-from-gh-with-jq.md)
|
||||||
- [Target Another Repo When Creating A PR](github/target-another-repo-when-creating-a-pr.md)
|
- [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)
|
||||||
|
|
||||||
@@ -616,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)
|
||||||
@@ -716,6 +726,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
|
|
||||||
### LLM
|
### LLM
|
||||||
|
|
||||||
|
- [Count Number Of Tokens In A File](llm/count-number-of-tokens-in-a-file.md)
|
||||||
- [Send cURL To Claude Text Completion API](llm/send-curl-to-claude-text-completion-api.md)
|
- [Send cURL To Claude Text Completion API](llm/send-curl-to-claude-text-completion-api.md)
|
||||||
- [Use The llm CLI With Claude Models](llm/use-the-llm-cli-with-claude-models.md)
|
- [Use The llm CLI With Claude Models](llm/use-the-llm-cli-with-claude-models.md)
|
||||||
|
|
||||||
@@ -727,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)
|
||||||
@@ -744,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)
|
||||||
@@ -759,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)
|
||||||
@@ -847,6 +864,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
|
|
||||||
### pnpm
|
### pnpm
|
||||||
|
|
||||||
|
- [Avoid Vulnerabilities In New Package Versions](pnpm/avoid-vulnerabilities-in-new-package-versions.md)
|
||||||
- [Execute A Command From The Workspace Root](pnpm/execute-a-command-from-the-workspace-root.md)
|
- [Execute A Command From The Workspace Root](pnpm/execute-a-command-from-the-workspace-root.md)
|
||||||
- [Install Command Runs For Entire Workspace](pnpm/install-command-runs-for-entire-workspace.md)
|
- [Install Command Runs For Entire Workspace](pnpm/install-command-runs-for-entire-workspace.md)
|
||||||
- [List The Installed Version Of A Specific Package](pnpm/list-the-installed-version-of-a-specific-package.md)
|
- [List The Installed Version Of A Specific Package](pnpm/list-the-installed-version-of-a-specific-package.md)
|
||||||
@@ -1044,26 +1062,47 @@ 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)
|
||||||
|
- [Argument Defaults Are Evaluated When Function Is Defined](python/argument-defaults-are-evaluated-when-function-is-defined.md)
|
||||||
|
- [Assert Is Only A Development Check](python/assert-is-only-a-development-check.md)
|
||||||
|
- [Avoid Modification With Frozen Dataclass](python/avoid-modification-with-frozen-dataclass.md)
|
||||||
- [Break Debugger On First Line Of Program](python/break-debugger-on-first-line-of-program.md)
|
- [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 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 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)
|
||||||
- [Keep A Tally With collections.Counter](python/keep-a-tally-with-collections-counter.md)
|
- [Keep A Tally With collections.Counter](python/keep-a-tally-with-collections-counter.md)
|
||||||
- [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.md)
|
- [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 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)
|
||||||
|
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
|
||||||
|
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
|
||||||
|
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
|
||||||
|
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
|
||||||
|
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
|
||||||
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
|
- [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 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
|
||||||
|
|
||||||
@@ -1119,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)
|
||||||
@@ -1149,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)
|
||||||
@@ -1410,6 +1451,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Defaulting To Frozen String Literals](ruby/defaulting-to-frozen-string-literals.md)
|
- [Defaulting To Frozen String Literals](ruby/defaulting-to-frozen-string-literals.md)
|
||||||
- [Define A Custom RSpec Matcher](ruby/define-a-custom-rspec-matcher.md)
|
- [Define A Custom RSpec Matcher](ruby/define-a-custom-rspec-matcher.md)
|
||||||
- [Define A Method On A Struct](ruby/define-a-method-on-a-struct.md)
|
- [Define A Method On A Struct](ruby/define-a-method-on-a-struct.md)
|
||||||
|
- [Define A Set Of Class Methods](ruby/define-a-set-of-class-methods.md)
|
||||||
- [Define Multiline Strings With Heredocs](ruby/define-multiline-strings-with-heredocs.md)
|
- [Define Multiline Strings With Heredocs](ruby/define-multiline-strings-with-heredocs.md)
|
||||||
- [Destructure The First Item From An Array](ruby/destructure-the-first-item-from-an-array.md)
|
- [Destructure The First Item From An Array](ruby/destructure-the-first-item-from-an-array.md)
|
||||||
- [Destructuring Arrays In Blocks](ruby/destructuring-arrays-in-blocks.md)
|
- [Destructuring Arrays In Blocks](ruby/destructuring-arrays-in-blocks.md)
|
||||||
@@ -1533,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)
|
||||||
@@ -1577,6 +1620,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
|
|
||||||
### Taskfile
|
### Taskfile
|
||||||
|
|
||||||
|
- [Add Default Task To List All Tasks](taskfile/add-default-task-to-list-all-tasks.md)
|
||||||
- [Create Interactive Picker For Set Of Subtasks](taskfile/create-interactive-picker-for-set-of-subtasks.md)
|
- [Create Interactive Picker For Set Of Subtasks](taskfile/create-interactive-picker-for-set-of-subtasks.md)
|
||||||
- [Run A Task If It Meets Criteria](taskfile/run-a-task-if-it-meets-criteria.md)
|
- [Run A Task If It Meets Criteria](taskfile/run-a-task-if-it-meets-criteria.md)
|
||||||
|
|
||||||
@@ -1680,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)
|
||||||
@@ -1773,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)
|
||||||
@@ -1781,6 +1827,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Rename A Bunch Of Files By Constructing mv Commands](unix/rename-a-bunch-of-files-by-constructing-mv-commands.md)
|
- [Rename A Bunch Of Files By Constructing mv Commands](unix/rename-a-bunch-of-files-by-constructing-mv-commands.md)
|
||||||
- [Repeat Yourself](unix/repeat-yourself.md)
|
- [Repeat Yourself](unix/repeat-yourself.md)
|
||||||
- [Replace Pattern Across Many Files In A Project](unix/replace-pattern-across-many-files-in-a-project.md)
|
- [Replace Pattern Across Many Files In A Project](unix/replace-pattern-across-many-files-in-a-project.md)
|
||||||
|
- [Reverse Each Line Of A File](unix/reverse-each-line-of-a-file.md)
|
||||||
- [Run A Command Repeatedly Several Times](unix/run-a-command-repeatedly-several-times.md)
|
- [Run A Command Repeatedly Several Times](unix/run-a-command-repeatedly-several-times.md)
|
||||||
- [Run A cURL Command Without The Progress Meter](unix/run-a-curl-command-without-the-progress-meter.md)
|
- [Run A cURL Command Without The Progress Meter](unix/run-a-curl-command-without-the-progress-meter.md)
|
||||||
- [Safely Edit The Sudoers File With Vim](unix/safely-edit-the-sudoers-file-with-vim.md)
|
- [Safely Edit The Sudoers File With Vim](unix/safely-edit-the-sudoers-file-with-vim.md)
|
||||||
@@ -1814,6 +1861,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Use fzf To Change Directories](unix/use-fzf-to-change-directories.md)
|
- [Use fzf To Change Directories](unix/use-fzf-to-change-directories.md)
|
||||||
- [Use Negative Lookbehind Matching With ripgrep](unix/use-negative-lookbehind-matching-with-ripgrep.md)
|
- [Use Negative Lookbehind Matching With ripgrep](unix/use-negative-lookbehind-matching-with-ripgrep.md)
|
||||||
- [Use Regex Pattern Matching With Grep](unix/use-regex-pattern-matching-with-grep.md)
|
- [Use Regex Pattern Matching With Grep](unix/use-regex-pattern-matching-with-grep.md)
|
||||||
|
- [Use The Readline Keybindings Anywhere](unix/use-the-readline-keybindings-anywhere.md)
|
||||||
- [View A Web Page In The Terminal](unix/view-a-web-page-in-the-terminal.md)
|
- [View A Web Page In The Terminal](unix/view-a-web-page-in-the-terminal.md)
|
||||||
- [View The Source For A Brew Formula](unix/view-the-source-for-a-brew-formula.md)
|
- [View The Source For A Brew Formula](unix/view-the-source-for-a-brew-formula.md)
|
||||||
- [Watch The Difference](unix/watch-the-difference.md)
|
- [Watch The Difference](unix/watch-the-difference.md)
|
||||||
@@ -2045,11 +2093,13 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md)
|
- [Send A 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)
|
||||||
- [Update asdf Plugins With Latest Package Versions](workflow/update-asdf-plugins-with-latest-package-versions.md)
|
- [Update asdf Plugins With Latest Package Versions](workflow/update-asdf-plugins-with-latest-package-versions.md)
|
||||||
- [View A Nicely-Formatted CSV In Terminal](workflow/view-a-nicely-formatted-csv-in-terminal.md)
|
- [View A Nicely-Formatted CSV In Terminal](workflow/view-a-nicely-formatted-csv-in-terminal.md)
|
||||||
|
- [View Nicely Formatted Markdown From Terminal](workflow/view-nicely-formatted-markdown-from-terminal.md)
|
||||||
- [View The PR For The Current GitHub Branch](workflow/view-the-pr-for-the-current-github-branch.md)
|
- [View The PR For The Current GitHub Branch](workflow/view-the-pr-for-the-current-github-branch.md)
|
||||||
|
|
||||||
### XState
|
### XState
|
||||||
@@ -2084,6 +2134,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Add To The Path Via Path Array](zsh/add-to-the-path-via-path-array.md)
|
- [Add To The Path Via Path Array](zsh/add-to-the-path-via-path-array.md)
|
||||||
- [Create And Jump Into A Directory](zsh/create-and-jump-into-a-directory.md)
|
- [Create And Jump Into A Directory](zsh/create-and-jump-into-a-directory.md)
|
||||||
- [Link A Scalar To An Array](zsh/link-a-scalar-to-an-array.md)
|
- [Link A Scalar To An Array](zsh/link-a-scalar-to-an-array.md)
|
||||||
|
- [List Available Zle Keybindings](zsh/list-available-zle-keybindings.md)
|
||||||
- [Use A Space To Exclude Command From History](zsh/use-a-space-to-exclude-command-from-history.md)
|
- [Use A Space To Exclude Command From History](zsh/use-a-space-to-exclude-command-from-history.md)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|||||||
@@ -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,25 @@
|
|||||||
|
# Stash The Current Prompt To Send Another First
|
||||||
|
|
||||||
|
I've been working my way through the current cohort of Matt Pocock's [Claude
|
||||||
|
Code for Real
|
||||||
|
Engineers](https://www.aihero.dev/cohorts/claude-code-for-real-engineers-2026-04).
|
||||||
|
The best part about going through a series of videos like this is being able to
|
||||||
|
pick up big and small tips and tricks from another person's workflow.
|
||||||
|
|
||||||
|
One of the small things I picked up in an early video is the ability to stash
|
||||||
|
the current prompt.
|
||||||
|
|
||||||
|
Let's say I've gone to the trouble of writing out a detailed prompt, `@`'ing
|
||||||
|
some files, and so forth. Then I realize I need first prompt Claude to do
|
||||||
|
something else first. Instead of copy-pasting that prompt into my notes,
|
||||||
|
deleting it, issuing a different prompt, and then pasting it back in, I can hit
|
||||||
|
`Ctrl-s`.
|
||||||
|
|
||||||
|
`Ctrl-s` will _stash_ the current prompt, clearing out the prompt input. I can
|
||||||
|
then type in something else. Once I hit enter for that new prompt, it will be
|
||||||
|
sent to Claude and the stashed prompt will be immediately populated back into
|
||||||
|
the input.
|
||||||
|
|
||||||
|
Though `Ctrl-s` is mentioned when you hit `?` from within `claude` session, I
|
||||||
|
don't see it documented anywhere in their [Interactive Mode
|
||||||
|
reference](https://code.claude.com/docs/en/interactive-mode).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Cloudflare Allows CNAME For Apex Domain
|
||||||
|
|
||||||
|
If you want to set up a custom root (apex) domain with an app hosting provider
|
||||||
|
[like
|
||||||
|
Heroku](https://devcenter.heroku.com/articles/custom-domains#add-a-custom-root-domain),
|
||||||
|
you're going to need to work with a DNS provider that supports the non-standard
|
||||||
|
`ALIAS` records (or something equivalent).
|
||||||
|
|
||||||
|
In my case, I have my domain registered with Cloudflare. Cloudflare supports
|
||||||
|
this kind of CNAME lookup of an apex domain through [_CNAME
|
||||||
|
flattening_](https://developers.cloudflare.com/dns/cname-flattening/).
|
||||||
|
|
||||||
|
Unlike other registrars that use a separate `ALIAS` record concept, Cloudflare
|
||||||
|
allows you to set up a specialized `CNAME` record. Go into the DNS settings for
|
||||||
|
the domain of interest, click "Add Record", and then select `CNAME`. From there,
|
||||||
|
instead of entering a traditional subdomain like `www`, you put the `@` symbol
|
||||||
|
which tells Cloudflare that this is a record for the apex domain. That record
|
||||||
|
will still point to a target like `abc123.herokudns.com` as a more traditional
|
||||||
|
`CANME` would do.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Display All Git Log Entries In My Local Timezone
|
||||||
|
|
||||||
|
I tend to work with remote teams distributed across across multiple time zones.
|
||||||
|
In that context, it is important to have an awareness of what time zone each
|
||||||
|
person is operating in and to communicate clearly around that.
|
||||||
|
|
||||||
|
When looking at the output for `git log` on a distributed team, the timestamps
|
||||||
|
for each entry can be all over the place. If I want to understand when something
|
||||||
|
was committed, I have to look at the time as well as the time zone offset and
|
||||||
|
mentally translate it to my own time zone.
|
||||||
|
|
||||||
|
There is a `git config` option to alleviate this issue by having `git log`
|
||||||
|
convert and display all timestamps into your local time zone.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git config --global log.date rfc-local
|
||||||
|
```
|
||||||
|
|
||||||
|
Running that will add this entry to your _global_ git config file:
|
||||||
|
|
||||||
|
```
|
||||||
|
[log]
|
||||||
|
date = rfc-local
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the time that was displaying as `Wed Apr 8 20:12:33 2026 -0400` will display
|
||||||
|
as `Wed, 8 Apr 2026 19:12:33 -0500`.
|
||||||
|
|
||||||
|
This also helps with smoothing out differences from DST and for commits produced
|
||||||
|
by AI agents in sandbox environments where the locale is set to UTC.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Programmatically Grab SHA For Head Commit
|
||||||
|
|
||||||
|
When I use `gh browse path/to/some-file.txt`, it opens the browser to that file
|
||||||
|
in GitHub. However, it targets the default branch (`main`) by default which is
|
||||||
|
not very useful as a permalink because what that file looks like on `main` is
|
||||||
|
liable to change.
|
||||||
|
|
||||||
|
There is a `--commit` flag you can use to have it instead open to that file at a
|
||||||
|
specific commit SHA.
|
||||||
|
|
||||||
|
So what SHA do I pass as an argument to that flag?
|
||||||
|
|
||||||
|
Often what I would like to grab is a reference to the current version of the
|
||||||
|
file which is whatever it looks like for the `HEAD` commit. But `HEAD` is
|
||||||
|
another moving target reference. The `git rev-parse` command can translate
|
||||||
|
`HEAD` into a specific SHA though.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ git rev-parse --short HEAD
|
||||||
|
3402428
|
||||||
|
|
||||||
|
❯ git rev-parse HEAD
|
||||||
|
3402428aadc02cfdc9825c8feb593443e72f50cd
|
||||||
|
```
|
||||||
|
|
||||||
|
Either of those will work. I can use a bash command substitution then to tie it
|
||||||
|
all together into a single command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ gh browse path/to/some-file.txt --commit=$(git rev-parse --short HEAD)
|
||||||
|
```
|
||||||
|
|
||||||
|
See `man git-rev-parse` for more details.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# List PRs Awaiting Your Review
|
||||||
|
|
||||||
|
If you work on a software team or steward an open-source project, then there are
|
||||||
|
likely some open PRs that you've been tagged to review. I am usually able to
|
||||||
|
catch most review requests as they come up either from the GitHub email
|
||||||
|
notifications or by keeping an eye on the PRs tab of active projects. Sometimes
|
||||||
|
I get consumed by a task and something slips through the cracks.
|
||||||
|
|
||||||
|
There are a couple other ways to quickly check if anything is waiting on my
|
||||||
|
review.
|
||||||
|
|
||||||
|
From the web UI I can visit the following URL which will show all PRs across all
|
||||||
|
projects where my review has been requested:
|
||||||
|
|
||||||
|
[https://github.com/pulls/review-requested](https://github.com/pulls/review-requested)
|
||||||
|
|
||||||
|
The GitHub CLI (`gh`) can do the same and I can do it right from the terminal
|
||||||
|
instead of navigating several clicks within GitHub's web UI.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ gh search prs --review-requested=@me --state=open
|
||||||
|
```
|
||||||
|
|
||||||
|
That too will list PRs across all projects that are open and awaiting my review.
|
||||||
|
|
||||||
|
If that one ends up being a little too noisy, you can also use `gh` to _list_
|
||||||
|
just PRs for the current project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ gh pr list --search "review-requested:@me"
|
||||||
|
```
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Open File To Specific Line In Browser
|
||||||
|
|
||||||
|
Often one of the best ways to point a teammate to a line of code is to share a
|
||||||
|
GitHub link to a specific file and line number. Sometimes even a specific
|
||||||
|
commit.
|
||||||
|
|
||||||
|
For the longest time I would manually open GitHub, navigate to that file, and so
|
||||||
|
forth. The `gh` CLI supports this with the `browse` subcommand and it takes way
|
||||||
|
less time if you already have the repo in your local filesystem.
|
||||||
|
|
||||||
|
For instance, if I want to point you to line 11 of the `zshrc.local` file in my
|
||||||
|
`dotfiles` repo, I can run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ gh browse zshrc.local:11
|
||||||
|
```
|
||||||
|
|
||||||
|
That would open a browser tab to
|
||||||
|
[https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11](https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11).
|
||||||
|
|
||||||
|
If I wanted a range of lines, I could change it from `11` to, say, `11-27`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ gh browse zshrc.local:11-27
|
||||||
|
```
|
||||||
|
|
||||||
|
And I would see this in the browser --
|
||||||
|
[https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11-L27](https://github.com/jbranchaud/dotfiles/blob/main/zshrc.local?plain=1#L11-L27).
|
||||||
|
|
||||||
|
Both of these URLs are pointing to the `main` branch. If I instead want to
|
||||||
|
reference a specific commit, I can use the `--commit` flag.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ gh browse zshrc.local:11-27 --commit=f2f9e78d4fc784643f725c88f7a5a7a077e7f261
|
||||||
|
```
|
||||||
|
|
||||||
|
I grabbed that from the latest commit in `git log`. That opens to
|
||||||
|
[https://github.com/jbranchaud/dotfiles/blob/f2f9e78d4fc784643f725c88f7a5a7a077e7f261/zshrc.local?plain=1#L11-L27](https://github.com/jbranchaud/dotfiles/blob/f2f9e78d4fc784643f725c88f7a5a7a077e7f261/zshrc.local?plain=1#L11-L27).
|
||||||
|
|
||||||
|
Another way of doing that would be to use `git rev-parse HEAD`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ gh browse zshrc.local:11-27 --commit=$(git rev-parse HEAD)
|
||||||
|
```
|
||||||
|
|
||||||
|
See `gh browse --help` for more details.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Process JSON Output From gh With jq
|
||||||
|
|
||||||
|
The `gh` (GitHub) CLI is useful for accessing data about your profile and
|
||||||
|
projects from the terminal. With the `--json` flag, we can access the data in a
|
||||||
|
structured way which is useful for scripting.
|
||||||
|
|
||||||
|
Here is an example of pulling a list of all my repositories, limiting each
|
||||||
|
entity to just the `nameWithOwner` and `description`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ gh repo list --limit 1000 --json nameWithOwner,description
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"description": "My personal site -- joshbranchaud.com",
|
||||||
|
"nameWithOwner": "jbranchaud/personal-site"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Private repo for the NOTES.md of my TIL repo",
|
||||||
|
"nameWithOwner": "jbranchaud/til-notes-private"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
If I'm using the `--json` flag, then I can add in the `--jq` flag to apply a
|
||||||
|
`jq` query for additional processing of the output.
|
||||||
|
|
||||||
|
Here I convert it to a series of tuples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ gh repo list --limit 1000 --json nameWithOwner,description \
|
||||||
|
--jq '.[] | [.nameWithOwner, .description]'
|
||||||
|
[
|
||||||
|
"jbranchaud/personal-site",
|
||||||
|
"My personal site -- joshbranchaud.com"
|
||||||
|
]
|
||||||
|
[
|
||||||
|
"jbranchaud/til-notes-private",
|
||||||
|
"Private repo for the NOTES.md of my TIL repo"
|
||||||
|
]
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I can add one more pipe to that `jq` query to turn it into _tab-separated
|
||||||
|
values_ using
|
||||||
|
[`@tsv`](https://jqlang.org/manual/v1.5/#format-strings-and-escaping):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ gh repo list --limit 1000 --json nameWithOwner,description \
|
||||||
|
--jq '.[] | [.nameWithOwner, .description] | @tsv'
|
||||||
|
jbranchaud/personal-site My personal site -- joshbranchaud.com
|
||||||
|
jbranchaud/til-notes-private Private repo for the NOTES.md of my TIL repo
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful because I can then pipe it to another program, such as an `fzf`
|
||||||
|
command like [this repo selector that opens the selected one in the
|
||||||
|
browser](https://github.com/jbranchaud/dotfiles/commit/f964ca10c6c4db3475411c2991dc2f1dfd18c818).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Get User's Preferred Language From Browser
|
||||||
|
|
||||||
|
A great way to determine a user's preferred language if you aren't able to ask
|
||||||
|
them directly is to look at the language setting for their browser's UI.
|
||||||
|
|
||||||
|
We can get this from the instance of
|
||||||
|
[`Navigator`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator) in the
|
||||||
|
user's JavaScript runtime within the browser.
|
||||||
|
|
||||||
|
My browser's UI is set to US English, which I can verify like so:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
> navigator.language
|
||||||
|
'en-US'
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for all sorts of things like [formatting dates for
|
||||||
|
display](basic-date-formatting-without-a-library.md):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
> const now = new Date();
|
||||||
|
> Intl.DateTimeFormat(navigator.language).format(now)
|
||||||
|
'5/14/2026'
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for [formatting other kinds of units for
|
||||||
|
display](formatting-values-with-units-for-display.md):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
> const milesFormat =
|
||||||
|
Intl.NumberFormat(navigator.language, { style: "unit", unit: "mile" });
|
||||||
|
> milesFormat.format(1500)
|
||||||
|
"1,500 mi"
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Count Number Of Tokens In A File
|
||||||
|
|
||||||
|
Over time you have accumulated a bunch of small directives, corrections, and
|
||||||
|
project details in your `CLAUDE.md` or `AGENTS.md` file. The file doesn't seem
|
||||||
|
too big, but you are mindful that it is being included in every prompt. How many
|
||||||
|
tokens is it eating from the context window?
|
||||||
|
|
||||||
|
OpenAI's BPE (Byte Pair Encoding) tokenization library,
|
||||||
|
[`tiktoken`](https://github.com/openai/tiktoken), is an open-source Python
|
||||||
|
package. If it is installed on our machine, then we can use it as part of the
|
||||||
|
following one-liner to check a file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ python -c "import tiktoken, sys; print(len(tiktoken.encoding_for_model('gpt-4o').encode(open(sys.argv[1], 'r', encoding='utf-8').read())))" \
|
||||||
|
AGENTS.md
|
||||||
|
1018
|
||||||
|
```
|
||||||
|
|
||||||
|
I ran this against the `AGENTS.md` file in a team project I'm on. It came out to
|
||||||
|
1018 tokens. This is a very good approximation based on the tokenizer trained
|
||||||
|
for `gpt-4o`. The tokenizers may vary a little from model to model, but the
|
||||||
|
differences for our purposes here are going to be negligible.
|
||||||
|
|
||||||
|
This one-liner gets the "first" argument to the command, reads it in, and runs
|
||||||
|
that string against the tokenizer. The length of the tokenized encoding is then
|
||||||
|
printed.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Clean Up Item Layout In Finder Window
|
||||||
|
|
||||||
|
Sometimes while doing a bunch of manual drag-n-drop of files and folders in a
|
||||||
|
Finder.app window, I'll end up with a visual mess. Compared to other folders,
|
||||||
|
nothing is organized on the grid.
|
||||||
|
|
||||||
|
I can tell Finder.app to clean that up with the _Clean Up_ menu option.
|
||||||
|
|
||||||
|
While focused on the folder that I'm concerned about, I can go to _View_ >
|
||||||
|
_Clean Up_ in the top menu. Everything will snap into place.
|
||||||
|
|
||||||
|
On the specific Finder.app window, there is also a triple-dot actions menu that
|
||||||
|
appears on the top right. The _Clean Up_ action is available there as well.
|
||||||
|
|
||||||
|
There is also a _Clean Up By_ option which is a nice way to organize by some
|
||||||
|
attribute, such as the type (e.g Folder/File and extension).
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Read The Lid Angle Sensor For A MacBook
|
||||||
|
|
||||||
|
MacOS has a bunch of internal HID (Human Interface Device) data that can surface
|
||||||
|
details about all kinds of "devices" that comprise your machine. Some obvious
|
||||||
|
ones are the keyboard and trackpad as well as external mice and keyboards. The
|
||||||
|
battery and power source details are another which is sometimes integrated into
|
||||||
|
tools that display battery status (e.g.
|
||||||
|
[`tmux-battery`](https://github.com/tmux-plugins/tmux-battery)), though it uses
|
||||||
|
`pmset` directly). And many, many more.
|
||||||
|
|
||||||
|
One example I'd never considered is that there is a sensor for the lid angle of
|
||||||
|
the laptop that can tell the system whether the lid is open or closed and how
|
||||||
|
open it is (i.e. at what angle). There is no public interface for this lid angle
|
||||||
|
sensor, but people exploring all the HID devices have found the identifiers that
|
||||||
|
correspond to it (e.g.
|
||||||
|
[`pybooklid`](https://github.com/tcsenpai/pybooklid/blob/main/pybooklid/macbook_lid.py)).
|
||||||
|
|
||||||
|
Here is a minimal script that uses `uv`, `hidapi` (python bindings), and
|
||||||
|
`libhidapi` (shared runtime lib for those bindings):
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env -S uv run --quiet --script
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.10"
|
||||||
|
# dependencies = ["hidapi"]
|
||||||
|
# ///
|
||||||
|
"""Print MacBook lid angle in degrees."""
|
||||||
|
import os, sys
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
brew = "/opt/homebrew/lib"
|
||||||
|
if os.path.exists(brew):
|
||||||
|
os.environ["DYLD_LIBRARY_PATH"] = f"{brew}:{os.environ.get('DYLD_LIBRARY_PATH','')}"
|
||||||
|
|
||||||
|
import hid
|
||||||
|
|
||||||
|
VENDOR_ID, PRODUCT_ID = 0x05AC, 0x8104
|
||||||
|
USAGE_PAGE, USAGE = 0x0020, 0x008A
|
||||||
|
REPORT_ID = 1
|
||||||
|
|
||||||
|
def read_angle():
|
||||||
|
for info in hid.enumerate(VENDOR_ID, PRODUCT_ID):
|
||||||
|
if info.get("usage_page") == USAGE_PAGE and info.get("usage") == USAGE:
|
||||||
|
d = hid.device()
|
||||||
|
path = info["path"]
|
||||||
|
d.open_path(path if isinstance(path, bytes) else path.encode())
|
||||||
|
try:
|
||||||
|
data = d.get_feature_report(REPORT_ID, 8)
|
||||||
|
if data and len(data) >= 3:
|
||||||
|
return float((data[2] << 8) | data[1])
|
||||||
|
finally:
|
||||||
|
d.close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
a = read_angle()
|
||||||
|
if a is None:
|
||||||
|
sys.exit("sensor not available")
|
||||||
|
print(f"{a:.0f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
These IDs and usage values are the undocumented values that allow the script to
|
||||||
|
navigate specifically to the lid angle sensor and specifically to the usage page
|
||||||
|
and value that represent the current lid angle reading.
|
||||||
|
|
||||||
|
```
|
||||||
|
VENDOR_ID, PRODUCT_ID = 0x05AC, 0x8104
|
||||||
|
USAGE_PAGE, USAGE = 0x0020, 0x008A
|
||||||
|
REPORT_ID = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
I added [this
|
||||||
|
script](https://github.com/jbranchaud/dotfiles/blob/cbc7196607d1d6b25885f5387ca85b658bd765de/bin/lidangle)
|
||||||
|
to [my dotfiles](https://github.com/jbranchaud/dotfiles) and made it executable
|
||||||
|
(`chmod +x bin/lidangle`) so that I can try it out. I first ran it while it was
|
||||||
|
closed and connected to my external monitor (`0`), then I opened it as far as it
|
||||||
|
could go (`129`), and then I tried angling it close to what I thought was 90
|
||||||
|
degress (`92`, so close).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ lidangle
|
||||||
|
0
|
||||||
|
|
||||||
|
❯ lidangle
|
||||||
|
129
|
||||||
|
|
||||||
|
❯ lidangle
|
||||||
|
92
|
||||||
|
```
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Generate Permutations Of All Valid 9-ball Racks
|
||||||
|
|
||||||
|
I wanted to produce a full listing of all valid rack arrangements for the game
|
||||||
|
of [9-ball](https://en.wikipedia.org/wiki/Nine-ball). The constraints on how a
|
||||||
|
9-ball rack can be arranged are, first, that the 1 ball must be placed at the
|
||||||
|
head of the diamond and, second, that the 9 ball must be placed at the center of
|
||||||
|
the diamond. After that, all other balls (2 through 8) can be placed in any
|
||||||
|
arrangement.
|
||||||
|
|
||||||
|
Because each of those seven remaining balls can be arranged in distinct
|
||||||
|
orderings where each ball is placed once, this is a
|
||||||
|
[_permutation_](https://en.wikipedia.org/wiki/Permutation) problem.
|
||||||
|
|
||||||
|
> In elementary combinatorics, the k-permutations, or partial permutations, are
|
||||||
|
> the ordered arrangements of k distinct elements selected from a set. When k is
|
||||||
|
> equal to the size of the set, these are the permutations in the previous
|
||||||
|
> sense.
|
||||||
|
|
||||||
|
For this problem, the seven distinct elements can be arranged into `7!` (seven
|
||||||
|
factorial) unique permutations. That is, 5040 permutations.
|
||||||
|
|
||||||
|
I can use [Ruby's `Array#permutations`
|
||||||
|
method](https://docs.ruby-lang.org/en/4.0/Array.html#method-i-permutation) to
|
||||||
|
enumerate these 5040 permutations like so:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
[2,3,4,5,6,7,8].permutation.map do |perm|
|
||||||
|
[1, *perm[0..2], 9, *perm[3..7]]
|
||||||
|
end.to_a
|
||||||
|
=> [[1, 2, 3, 4, 9, 5, 6, 7, 8],
|
||||||
|
[1, 2, 3, 4, 9, 5, 6, 8, 7],
|
||||||
|
[1, 2, 3, 4, 9, 5, 7, 6, 8],
|
||||||
|
[1, 2, 3, 4, 9, 5, 7, 8, 6],
|
||||||
|
[1, 2, 3, 4, 9, 5, 8, 6, 7],
|
||||||
|
[1, 2, 3, 4, 9, 5, 8, 7, 6],
|
||||||
|
[1, 2, 3, 4, 9, 6, 5, 7, 8],
|
||||||
|
...
|
||||||
|
[1, 8, 7, 6, 9, 5, 3, 2, 4],
|
||||||
|
[1, 8, 7, 6, 9, 5, 3, 4, 2],
|
||||||
|
[1, 8, 7, 6, 9, 5, 4, 2, 3],
|
||||||
|
[1, 8, 7, 6, 9, 5, 4, 3, 2]]
|
||||||
|
```
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Avoid Vulnerabilities In New Package Versions
|
||||||
|
|
||||||
|
It seems like every week there is a new supply chain attack where malicious code
|
||||||
|
is embedded in a popular, widely-used OSS package. This week's is
|
||||||
|
[axios](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan).
|
||||||
|
|
||||||
|
The [`pnpm` package manager](https://pnpm.io/) has a nice feature that helps
|
||||||
|
avoid installing these vulnerable package versions in the first place.
|
||||||
|
|
||||||
|
> To reduce the risk of installing compromised packages, you can delay the
|
||||||
|
> installation of newly published versions. In most cases, malicious releases
|
||||||
|
> are discovered and removed from the registry within an hour.
|
||||||
|
|
||||||
|
The [`minimumReleaseAge` config option](https://pnpm.io/settings#minimumreleaseage) tells `pnpm` to not install
|
||||||
|
a dependency (including transitive ones) until it has been released for at least
|
||||||
|
that many minutes.
|
||||||
|
|
||||||
|
For instance, if you wanted to set this to 72 hours, then you'd set this option
|
||||||
|
to `4320` minutes like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pnpm config set minimum-release-age 4320 -g
|
||||||
|
```
|
||||||
|
|
||||||
|
The global flag (`-g`) will set that in your global config location, e.g.
|
||||||
|
`$XDG_CONFIG_HOME/pnpm/rc`. You could also add it specifically to your project
|
||||||
|
in the `pnpm-workspace.yaml` file.
|
||||||
|
|
||||||
|
[source](https://bsky.app/profile/styfle.dev/post/3miekuyeyrs2w)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Access Variables Outside Loop Scope
|
||||||
|
|
||||||
|
Here is a function that loops over a list to find the first occurrence of a
|
||||||
|
falsy value.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def find_false(self):
|
||||||
|
for item in self.items:
|
||||||
|
item_type = type(item)
|
||||||
|
print(f"Current item: {item} ({item_type})")
|
||||||
|
if not item:
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"First false item: {item} ({item_type})")
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice how at the end of the function, outside of the loop, I am able to access
|
||||||
|
both `item` (defined in the loop definition) and `item_type` (defined within the
|
||||||
|
loop's body).
|
||||||
|
|
||||||
|
Both of these variables are defined, by the loop, in _function scope_ and are
|
||||||
|
accessible anywhere in the function after they have been defined.
|
||||||
|
|
||||||
|
The title of this TIL is a bit of a misnomer because Python doesn't have the
|
||||||
|
concept of a _loop scope_. There are two levels of scope in Python --
|
||||||
|
module/global scope and function scope.
|
||||||
|
|
||||||
|
I spend most of my time writing Ruby which also has _block scope_, so Python's
|
||||||
|
simplified two-level scoping took me by surprise.
|
||||||
|
|
||||||
|
Though the code sample above is contrived, this function scope assignment can be
|
||||||
|
taken advantage of with loop definitions in scenarios where you want to know
|
||||||
|
what the last `item` defined was before the loop terminated.
|
||||||
|
|
||||||
|
```python
|
||||||
|
for submission in submissions:
|
||||||
|
if passes(submission, criteria):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("No submissions that meet given criteria")
|
||||||
|
|
||||||
|
print(f"Submit first passing submission: {submission.id}")
|
||||||
|
submit(submission)
|
||||||
|
```
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Argument Defaults Are Evaluated When Function Is Defined
|
||||||
|
|
||||||
|
When you define a function with any arguments that have default values, those
|
||||||
|
default values are evaluated and stored at the time that the function is defined
|
||||||
|
(i.e. when it is evaluated by the interpreter). This might feel counter
|
||||||
|
intuitive if you are coming from another language, like Ruby, where these kinds
|
||||||
|
of defaults are evaluated at call time. This is unremarkable for scalar values
|
||||||
|
like `4` or `"fallback"`. It's much more interesting when your defaults are
|
||||||
|
function calls.
|
||||||
|
|
||||||
|
What if our default is something like `datetime.now()`?
|
||||||
|
|
||||||
|
Here I've defined a `Timer` class that has a `start` and `stop` method. The
|
||||||
|
`stop` method can be called with a specific `datetime` value otherwise it falls
|
||||||
|
back to `datetime.now()` -- but when is _now_?
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class Timer:
|
||||||
|
def __init__(self):
|
||||||
|
self._start = None
|
||||||
|
self._stop = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self._start = datetime.now(timezone.utc)
|
||||||
|
self._stop = None
|
||||||
|
|
||||||
|
def stop(self, at=datetime.now(timezone.utc)):
|
||||||
|
print(f"now: {datetime.now(timezone.utc)}")
|
||||||
|
print(f" at: {at}")
|
||||||
|
self._stop = at
|
||||||
|
|
||||||
|
elapsed = self._stop - self._start
|
||||||
|
return elapsed
|
||||||
|
```
|
||||||
|
|
||||||
|
Here I instantiate a timer, call `start`, sleep for 5 seconds, and then call
|
||||||
|
`stop`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
timer = Timer()
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
print(f"Elapsed: {timer.stop()}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is what gets printed to `stdout`:
|
||||||
|
|
||||||
|
```
|
||||||
|
now: 2026-05-22 00:45:05.654878+00:00
|
||||||
|
at: 2026-05-22 00:45:00.649699+00:00
|
||||||
|
Elapsed: -1 day, 23:59:59.999875
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that the actual _now_ (when the `stop` method is running) is about 5
|
||||||
|
seconds after the value of `at`. That is because `at`, which takes on the
|
||||||
|
default argument value, is `datetime.now()` as evaluated at the time the
|
||||||
|
function is interpreted. It is for that same reason that `self._stop` ends up
|
||||||
|
being just a hair earlier than the call to `start` which sets `self._start`.
|
||||||
|
Which explains why the _elapsed_ time is a negative value.
|
||||||
|
|
||||||
|
To avoid this awkwardness all together, set the default as `None` and then
|
||||||
|
override `None` at the start of the function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def stop(self, at = None):
|
||||||
|
if at == None:
|
||||||
|
at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Assert Is Only A Development Check
|
||||||
|
|
||||||
|
The `assert` keyword is used in Python to write a statement that will check some
|
||||||
|
assertion and raise an error if it isn't met. This is only meant to be used as a
|
||||||
|
check during development because it can be easily optimized out of the code.
|
||||||
|
|
||||||
|
```python
|
||||||
|
stuff = None
|
||||||
|
|
||||||
|
assert stuff, "We need to have some stuff to proceed"
|
||||||
|
|
||||||
|
print(f"We have {stuff or 'something'}!")
|
||||||
|
```
|
||||||
|
|
||||||
|
If I execute this code with `python`, it will raise on that second line of code.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ python assert_example.py
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/Users/lastword/dev/jbranchaud/py-vmt/assert_example.py", line 3, in <module>
|
||||||
|
assert stuff, "We need to have some stuff to proceed"
|
||||||
|
^^^^^
|
||||||
|
AssertionError: We need to have some stuff to proceed
|
||||||
|
```
|
||||||
|
|
||||||
|
This `assert` statement will be stripped out of the compiled bytecode if the
|
||||||
|
`-O` (capital o) flag is used. Notice how running the same file with that flag
|
||||||
|
does not lead to an `AssertionError`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
❯ python -O assert_example.py
|
||||||
|
We have something!
|
||||||
|
```
|
||||||
|
|
||||||
|
If I want to make sanity checks for situations that would be caused by a bug in
|
||||||
|
the code, an `assert` statement can be a good candidate. However, if I am making
|
||||||
|
runtime checks like validating user input, then an `if` statement and raising
|
||||||
|
something like a `ValueError` is better.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Avoid Modification With Frozen Dataclass
|
||||||
|
|
||||||
|
The `@dataclass` decorator can be set as _frozen_ to prevent modification of
|
||||||
|
values on instances of that `dataclass`.
|
||||||
|
|
||||||
|
Without making it frozen, I can easily subvert validations by changing the value
|
||||||
|
of attributes after the `__post_init__` validations are called.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> config = BPEConfig(300, []) # passes validations
|
||||||
|
>>> config.vocab_size = 22 # this is invalid, wish this was prevented
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is the updated `@dataclass` declaration with `frozen=True` passed as a
|
||||||
|
parameter.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BPEConfig:
|
||||||
|
BASE_VOCAB_SIZE: ClassVar[int] = 256
|
||||||
|
|
||||||
|
vocab_size: int
|
||||||
|
special_tokens: list[str]
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.vocab_size < self.BASE_VOCAB_SIZE:
|
||||||
|
msg = f"vocab_size ({self.vocab_size}) must be greater than or equal to BASE_VOCAB_SIZE ({self.BASE_VOCAB_SIZE})"
|
||||||
|
raise ValueError(msg)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now I am prevented from modifying a scalar value like `vocab_size` after the
|
||||||
|
instance has been created.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> config = BPEConfig(300, [])
|
||||||
|
>>> config.vocab_size = 22
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
File "<string>", line 4, in __setattr__
|
||||||
|
dataclasses.FrozenInstanceError: cannot assign to field 'vocab_size'
|
||||||
|
```
|
||||||
|
|
||||||
|
This doesn't prevent you from modifying the contents of attributes that are
|
||||||
|
`list` or `dict` types.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Check Precondition Before Click Arg Parsing
|
||||||
|
|
||||||
|
When setting up various [Click](https://click.palletsprojects.com/en/stable/)
|
||||||
|
subcommands with options, I ran into an issue with the order of some validation
|
||||||
|
checks. I was putting the same precondition validation logic at the beginning of
|
||||||
|
several subcommands. I was also putting callback validations on specific options
|
||||||
|
to those subcommands. Ideally the option validations could rely on those
|
||||||
|
precondition validations. However, the option callbacks run before anything in
|
||||||
|
the body of the subcommands.
|
||||||
|
|
||||||
|
The solution was to move those preconditions out of the subcommand body
|
||||||
|
(simplifying the subcommand) and into a `click.Command` subclass.
|
||||||
|
|
||||||
|
To demonstrate that, I'll first show the `click.Command` subclass:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RequireActiveSessionCommand(click.Command):
|
||||||
|
def parse_args(self, ctx, args):
|
||||||
|
if ctx.obj.active_session is None:
|
||||||
|
msg = "No active session being tracked. Start a session first."
|
||||||
|
raise click.UsageError(msg)
|
||||||
|
|
||||||
|
return super().parse_args(ctx, args)
|
||||||
|
```
|
||||||
|
|
||||||
|
The only thing this subclass overrides is `parse_args` where it gets ahead of
|
||||||
|
the standard arg parsing logic to first check the precondition. In this case, I
|
||||||
|
check that there is an active session. If there isn't, then I can raise a
|
||||||
|
`click.UsageError`. Otherwise, it delegates back to the super-class
|
||||||
|
implementation of `parse_args`.
|
||||||
|
|
||||||
|
This subclass then gets used for the commands that need to enforce this
|
||||||
|
precondition. Two prime examples of that are the `stop` and `cancel` subcommands.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@cli.command(cls=RequireActiveSessionCommand)
|
||||||
|
@click.option("--at", help='Hours previous to end the timer, e.g. "2 hours ago"', callback=validate_stop_at)
|
||||||
|
@pass_cli
|
||||||
|
def stop(cli_ctx: CliContext, at: datetime) -> None:
|
||||||
|
# ... implementation omitted
|
||||||
|
|
||||||
|
@cli.command(cls=RequireActiveSessionCommand)
|
||||||
|
@pass_cli
|
||||||
|
def cancel(cli_ctx: CliContext):
|
||||||
|
# ... implementation omitted
|
||||||
|
```
|
||||||
|
|
||||||
|
Other subcommands, like `start` and `status` that don't need to enforce this
|
||||||
|
precondition use the `@cli.command()` decorator without passing in a custom
|
||||||
|
subclass.
|
||||||
|
|
||||||
|
This example is pulled directly from [this commit](https://github.com/jbranchaud/py-vmt/commit/505109b7a4013e05f085cded666c6b1ac7c3c250)
|
||||||
|
of my [`py-vmt` time tracker tool](https://github.com/jbranchaud/py-vmt).
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Define Sequence Of Tests With Parametrize Decorator
|
||||||
|
|
||||||
|
I have a function that I want to test across a bunch of different inputs. That
|
||||||
|
way I can make sure the logic of that function handles all the different
|
||||||
|
scenarios I have in mind.
|
||||||
|
|
||||||
|
While working on [`py-vmt`](https://github.com/jbranchaud/py-vmt), I started by
|
||||||
|
writing a big single test function with a sequence of variable assignments and
|
||||||
|
`assert` statements. Here's my starting point:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_format_time_delta_everything():
|
||||||
|
# less than a minute
|
||||||
|
thirty_seconds = timedelta(seconds=30)
|
||||||
|
assert "30s" == format_time_delta(thirty_seconds)
|
||||||
|
|
||||||
|
# one minute exactly
|
||||||
|
one_minute = timedelta(seconds=60)
|
||||||
|
assert "1m" == format_time_delta(one_minute)
|
||||||
|
|
||||||
|
# more than a minute
|
||||||
|
assert "1m30s" == format_time_delta(one_minute + thirty_seconds)
|
||||||
|
|
||||||
|
# bunch of minutes and seconds
|
||||||
|
delta = timedelta(minutes=24, seconds=8)
|
||||||
|
assert "24m8s" == format_time_delta(delta)
|
||||||
|
|
||||||
|
# one hour exactly
|
||||||
|
one_hour = timedelta(hours=1)
|
||||||
|
assert "1h" == format_time_delta(one_hour)
|
||||||
|
|
||||||
|
# more than one hour
|
||||||
|
assert "1h24m" == format_time_delta(one_hour + delta)
|
||||||
|
```
|
||||||
|
|
||||||
|
I knew I would eventually need to break it up into individual test functions,
|
||||||
|
but I couldn't bare to start there because it seemed quite repetitive.
|
||||||
|
|
||||||
|
There is another way to approach this without all the duplication. Pytest comes
|
||||||
|
with [a "parametrize" decorator](https://docs.pytest.org/en/stable/example/parametrize.html). This is
|
||||||
|
used to define a set of test data (and expected values) that will get passed
|
||||||
|
one-by-one to the test function as parameters.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.parametrize("input,expected", [
|
||||||
|
(timedelta(seconds=30), "30s"),
|
||||||
|
(timedelta(seconds=60), "1m"),
|
||||||
|
(timedelta(seconds=90), "1m30s"),
|
||||||
|
(timedelta(minutes=24, seconds=8), "24m8s"),
|
||||||
|
(timedelta(hours=1), "1h"),
|
||||||
|
(timedelta(hours=1, minutes=24, seconds=8), "1h24m"),
|
||||||
|
])
|
||||||
|
def test_format_time_delta(input, expected):
|
||||||
|
assert format_time_delta(input) == expected
|
||||||
|
```
|
||||||
|
|
||||||
|
I ditch all of the duplication this way. I define a list of tuples that
|
||||||
|
represent my input values and expected values. Then the body of the test can be
|
||||||
|
minimal. And I get a separate test execution for each parameter tuple making it
|
||||||
|
easier to see fine-grained pass/fail results.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Define Typed Class Interface With Protocol
|
||||||
|
|
||||||
|
In [`py-vmt`](https://github.com/jbranchaud/py-vmt) I am defining different
|
||||||
|
storage access layers for the CLI to use. I want a consistent interface that the
|
||||||
|
core CLI logic can depend on regardless of whether it is a JSON file or a SQLite
|
||||||
|
database. To achieve that I can define a class of unimplemented functions that
|
||||||
|
inherits from
|
||||||
|
[`typing.Protocol`](https://typing.python.org/en/latest/spec/protocol.html).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
class SessionRepository(Protocol):
|
||||||
|
def active_session(self) -> Session | None: ...
|
||||||
|
def write_active_session(self, session) -> None: ...
|
||||||
|
def append_session(self, session) -> None: ...
|
||||||
|
def all_sessions(self) -> list[Session]: ...
|
||||||
|
def clear_active_session(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that none of these have default implementations. The `...` indicates that
|
||||||
|
class implementing this protocol will define the implementation of those
|
||||||
|
functions.
|
||||||
|
|
||||||
|
Now, my `CliContext` class, which needs some kind of `SessionRepository` to
|
||||||
|
function can indicate as much in `__init__`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CliContext:
|
||||||
|
def __init__(self, verbose: bool, repo: SessionRepository | None = None) -> None:
|
||||||
|
self.verbose: bool = verbose
|
||||||
|
self.active_session: Session | None = None
|
||||||
|
self.repo: SessionRepository = repo or JsonRepository()
|
||||||
|
self.active_session = self.repo.active_session()
|
||||||
|
```
|
||||||
|
|
||||||
|
If `JsonRepository` doesn't define all of the methods specified in the protocol,
|
||||||
|
then a type error will occur wherever it clashes with `SessionRepository`. Now
|
||||||
|
as I implement `SqliteRepository` I have a standard interface to build against
|
||||||
|
that I know I can seamlessly swap in.
|
||||||
|
|
||||||
|
[source](https://typing.python.org/en/latest/reference/protocols.html#simple-user-defined-protocols)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Enable Pyright Type Checking In Cursor
|
||||||
|
|
||||||
|
In most ways [Cursor](https://cursor.com/), a clone of VS Code, behaves like VS
|
||||||
|
Code and uses the same extensions as VS Code. It even offers to clone all your
|
||||||
|
existing extensions and setup from VS Code when you first install it.
|
||||||
|
|
||||||
|
However, the Pyright type checking setup that I had in VS Code stopped working
|
||||||
|
when I opened up the same Python project in Cursor. It seems that to get Pyright
|
||||||
|
to reliably work with forks of VS Code, you need to use a compatible fork like
|
||||||
|
[Based Pyright](https://docs.basedpyright.com/latest/).
|
||||||
|
|
||||||
|
Once I installed _Based Pyright_ from the extension marketplace, I was able to
|
||||||
|
enable it in `.vscode/settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
...,
|
||||||
|
"basedpyright.analysis.typeCheckingMode": "basic"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
I may have needed to restart Cursor at this point, I cannot remember exactly.
|
||||||
|
However, once this setup was in place the helpful type checking errors started
|
||||||
|
appearing as red squiggles.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Get Absolute Seconds From `timedelta` Object
|
||||||
|
|
||||||
|
The [`timedelta` object provided by
|
||||||
|
`datetime`](https://docs.python.org/3/library/datetime.html#timedelta-objects)
|
||||||
|
is a useful built-in concept for representing a duration of time.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> from datetime import timedelta
|
||||||
|
>>> diff = timedelta(hours=1, minutes=1, seconds=6)
|
||||||
|
>>> diff.seconds
|
||||||
|
3666
|
||||||
|
```
|
||||||
|
|
||||||
|
It is pretty minimal though. There are only a couple things you can inspect
|
||||||
|
about it -- `days`, `seconds` (as I did in the snippet above), and
|
||||||
|
`microseconds`.
|
||||||
|
|
||||||
|
And perhaps that is enough to hint at the issue I recently ran into with it --
|
||||||
|
specifically that you can access both `days` and `seconds`.
|
||||||
|
|
||||||
|
Let's look at what happens when I have a `timedelta` with more than a day worth
|
||||||
|
of seconds.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> diff = timedelta(seconds=(3600 * 24 + 1))
|
||||||
|
>>> diff.seconds
|
||||||
|
1
|
||||||
|
>>> diff.days
|
||||||
|
1
|
||||||
|
```
|
||||||
|
|
||||||
|
I thought `seconds` was going to produce `86401` instead of `1`. The reason is
|
||||||
|
because any amount of duration over a day gets converted into the `days` value
|
||||||
|
and its the remaining time smaller than a day that is represented by `seconds`.
|
||||||
|
|
||||||
|
In my [original implementation of
|
||||||
|
`format_time_delta`](https://github.com/jbranchaud/py-vmt/blob/c14eaa56cf5f5c6d0120a95f04f95a6c87443e1c/src/py_vmt/time_helpers.py#L11-L14),
|
||||||
|
I was trying to build a relative time string by converting `seconds` into hours,
|
||||||
|
minutes, and seconds. That approach falls apart as soon as the delta is greater
|
||||||
|
than a day.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def format_time_delta(diff) -> str:
|
||||||
|
hours, remainder = divmod(diff.seconds, 3600)
|
||||||
|
minutes, remainder = divmod(remainder, 60)
|
||||||
|
seconds = remainder
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, I needed to reach for [the `total_seconds()` function](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds).
|
||||||
|
This gives "the total number of seconds contained in the duration" and is
|
||||||
|
described as equivalent to `diff / timedelta(seconds=1)`.
|
||||||
|
|
||||||
|
Here is the [updated version of `format_time_delta`](https://github.com/jbranchaud/py-vmt/blob/ec1875a9d73552f5481e3945ddf522e94d0cc018/src/py_vmt/time_helpers.py?plain=1#L11-L16):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def format_time_delta(diff: timedelta) -> str:
|
||||||
|
total_seconds = int(diff.total_seconds())
|
||||||
|
|
||||||
|
hours, remainder = divmod(total_seconds, 3600)
|
||||||
|
minutes, remainder = divmod(remainder, 60)
|
||||||
|
seconds = remainder
|
||||||
|
```
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Get Quotient And Remainder In One Operation
|
||||||
|
|
||||||
|
While writing some custom code to transform a number of seconds into the
|
||||||
|
constituent hours, minutes, and seconds, I found myself needing to get both the
|
||||||
|
quotient and remainder from a division between two numbers.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> import math
|
||||||
|
>>> math.floor(3666 / 3600)
|
||||||
|
1
|
||||||
|
>>> 3666 % 3600
|
||||||
|
66
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, I can use Python's built-in
|
||||||
|
[`divmod`](https://docs.python.org/3/library/functions.html#divmod) function to
|
||||||
|
compute both values in one statement.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> divmod(3666, 3600)
|
||||||
|
(1, 66)
|
||||||
|
```
|
||||||
|
|
||||||
|
The result is a tuple with the first value being my quotient (in this case, the
|
||||||
|
number of hours) and the remainder (the remaining number of seconds).
|
||||||
|
|
||||||
|
This kind of operation is known as [Euclidian
|
||||||
|
Division](https://en.wikipedia.org/wiki/Euclidean_division).
|
||||||
|
|
||||||
|
Here is a snippet of some actual code where I use this in
|
||||||
|
[`py-vmt`](https://github.com/jbranchaud/py-vmt/blob/b9eae8b258e9fd720cfa3bb63b601225df352051/src/py_vmt/time_helpers.py#L14-L16):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def format_time_delta(diff: timedelta) -> str:
|
||||||
|
total_seconds = int(diff.total_seconds())
|
||||||
|
|
||||||
|
hours, remainder = divmod(total_seconds, 3600)
|
||||||
|
minutes, remainder = divmod(remainder, 60)
|
||||||
|
seconds = remainder
|
||||||
|
|
||||||
|
# ...
|
||||||
|
```
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Make Dataclass Sortable By Specific Field
|
||||||
|
|
||||||
|
One way to sort a list of some `dataclass` is to define the `key` parameter when
|
||||||
|
calling `sort` or `sorted` like I discussed in [Sort a List of Dataclass
|
||||||
|
Instances](sort-a-list-of-dataclass-instances.md):
|
||||||
|
|
||||||
|
```python
|
||||||
|
for date in sessions_grouped_by_day.keys():
|
||||||
|
sessions_grouped_by_day[date].sort(
|
||||||
|
key=lambda session: session.start_time.time()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
But then that lambda for `key` needs to be defined everywhere you sort.
|
||||||
|
|
||||||
|
If the dataclass has a single, specific field that acts as a natural proxy for
|
||||||
|
sort order, then you can define that in the `dataclass` implementation with the
|
||||||
|
`__lt__` method.
|
||||||
|
|
||||||
|
As long as a class defines the _less than_ dunder method, it will be sortable.
|
||||||
|
|
||||||
|
Here is what that looks like for this `Session` dataclass:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Session:
|
||||||
|
start_time: datetime
|
||||||
|
project_name: str
|
||||||
|
end_time: datetime | None = None
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if not isinstance(other, Session):
|
||||||
|
return NotImplemented
|
||||||
|
return self.start_time < other.start_time
|
||||||
|
|
||||||
|
# more methods below ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This implementation of `__lt__` tells the sorting methods that _this_ (`self`)
|
||||||
|
instance of `Session` can be compared to some `other` instance of `Session` by
|
||||||
|
comparing their `start_time` values to see which is less than. The guard at the
|
||||||
|
beginning makes sure only instances of `Session` are being compared.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Make Secure Temp File For Atomic Write
|
||||||
|
|
||||||
|
Two types of failure modes that can occur while writing to a shared file on the
|
||||||
|
file system are 1) a corrupted file due to a crash mid-write and 2) another
|
||||||
|
process reading a partial file mid-write.
|
||||||
|
|
||||||
|
One way I've handled this in [`py-vmt`](https://github.com/jbranchaud/py-vmt) is
|
||||||
|
to perform the write operations on a secure temp file and then use the OS-level
|
||||||
|
atomic `rename` operation. I do this by [creating a
|
||||||
|
`contextmanager`](https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager)
|
||||||
|
that uses
|
||||||
|
[`tempfile.mkstemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp)
|
||||||
|
and [`os.replace`](https://docs.python.org/3/library/os.html#os.replace).
|
||||||
|
|
||||||
|
Here is what the `contextmanager` looks like:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
import os, tempfile
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def atomic_write(path: Path):
|
||||||
|
# write to a tmp file in the same directory, then atomically swap it
|
||||||
|
fd, temp_file_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w") as file:
|
||||||
|
yield file
|
||||||
|
os.replace(temp_file_path, path)
|
||||||
|
except BaseException:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
This explicitly creates a secure temp file in the same directory as the given
|
||||||
|
path with `.tmp` as the suffix. I then open the file descriptor using the
|
||||||
|
`os.fdopen` context manager (which will manage closing the file descriptor for
|
||||||
|
me). The `@contextmanager` decorator plus the `yield file` are what allow this
|
||||||
|
to be used as a `with` block. Once any file operations are done, then I use
|
||||||
|
`os.replace` to atomically swap out the original file with the temp file.
|
||||||
|
|
||||||
|
Here is how I use it to write updates to JSON data files:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def write_active_session(self, session: Session) -> None:
|
||||||
|
with atomic_write(self.active_session_file) as file:
|
||||||
|
json.dump(session.marshal(), file)
|
||||||
|
```
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Reclassify Certain Packages As Dev Dependencies
|
||||||
|
|
||||||
|
When I first started working on [py-vmt](https://github.com/jbranchaud/py-vmt),
|
||||||
|
I wasn't differentiating certain test packages as _dev_ dependencies as opposed
|
||||||
|
to standard, production dependencies. This can lead to bloated installs across a
|
||||||
|
variety of distribution channels.
|
||||||
|
|
||||||
|
Notice that I have everything treated as production dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ uv tree --no-dev
|
||||||
|
Resolved 18 packages in 2ms
|
||||||
|
py-vmt v0.1.0
|
||||||
|
├── click v8.3.1
|
||||||
|
├── dateparser v1.3.0
|
||||||
|
│ ├── python-dateutil v2.9.0.post0
|
||||||
|
│ │ └── six v1.17.0
|
||||||
|
│ ├── pytz v2026.1.post1
|
||||||
|
│ ├── regex v2026.2.28
|
||||||
|
│ └── tzlocal v5.3.1
|
||||||
|
├── freezegun v1.5.5
|
||||||
|
│ └── python-dateutil v2.9.0.post0 (*)
|
||||||
|
├── platformdirs v4.9.4
|
||||||
|
├── pytest v9.0.2
|
||||||
|
│ ├── iniconfig v2.3.0
|
||||||
|
│ ├── packaging v26.0
|
||||||
|
│ ├── pluggy v1.6.0
|
||||||
|
│ └── pygments v2.19.2
|
||||||
|
└── types-dateparser v1.3.0.20260211
|
||||||
|
(*) Package tree already displayed
|
||||||
|
```
|
||||||
|
|
||||||
|
`pytest`, `freezegun`, and `types-dateparser` are better suited as _dev_
|
||||||
|
dependencies.
|
||||||
|
|
||||||
|
I can reclassify them by moving them from `dependencies` into a `dev` dependency
|
||||||
|
group in `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
dependencies = ["click>=8.3.1", "dateparser>=1.3.0", "platformdirs>=4.9.4"]
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["freezegun>=1.5.5", "pytest>=9.0.2", "types-dateparser>=1.3.0.20260211"]
|
||||||
|
```
|
||||||
|
|
||||||
|
I only had `dependencies` before, so I had to add `[dependency-groups]` and `dev = []` to my `pyproject.toml` file.
|
||||||
|
|
||||||
|
I can then tell `uv` to sync up the installation and virtualenv based on the new
|
||||||
|
organization of the dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ uv sync
|
||||||
|
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
|
||||||
|
Resolved 18 packages in 518ms
|
||||||
|
```
|
||||||
|
|
||||||
|
Now when I check the `--no-dev` tree of dependencies, it's just the essentials:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ uv tree --no-dev
|
||||||
|
Resolved 18 packages in 1ms
|
||||||
|
py-vmt v0.1.0
|
||||||
|
├── click v8.3.1
|
||||||
|
├── dateparser v1.3.0
|
||||||
|
│ ├── python-dateutil v2.9.0.post0
|
||||||
|
│ │ └── six v1.17.0
|
||||||
|
│ ├── pytz v2026.1.post1
|
||||||
|
│ ├── regex v2026.2.28
|
||||||
|
│ └── tzlocal v5.3.1
|
||||||
|
└── platformdirs v4.9.
|
||||||
|
```
|
||||||
|
|
||||||
|
Another way to achieve this would have been to run `uv remove` and `uv add` with
|
||||||
|
the relevant sets of package names. In retrospect, I would have preferred using
|
||||||
|
that approach in the first place. If you're wanting to be pinned to specific
|
||||||
|
versions of certain packages, you'd have to be a little more careful to get this
|
||||||
|
right.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Set Up Pyright Type Checking In GitHub
|
||||||
|
|
||||||
|
As I get into more of a PR workflow with my development of
|
||||||
|
[`py-vmt`](https://github.com/jbranchaud/py-vmt), I need to set up some basic CI
|
||||||
|
checks in GitHub. For starters I want the same `pyright` type checking that I
|
||||||
|
have locally to be run in CI for consistency.
|
||||||
|
|
||||||
|
Though my editor is set up to do Pyright type checking as I work locally, I can
|
||||||
|
also manually run it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ uv run pyright
|
||||||
|
```
|
||||||
|
|
||||||
|
Pyright will look for the `tool.pyright` section in my `pyproject.toml` file
|
||||||
|
which currently looks like the following:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.pyright]
|
||||||
|
include = ["src", "tests"]
|
||||||
|
```
|
||||||
|
|
||||||
|
I can get this same type checking in CI for PRs by adding the following
|
||||||
|
`.github/workflows/typecheck.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: pyright
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
typecheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v3
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --all-extras --dev
|
||||||
|
|
||||||
|
- name: Run pyright
|
||||||
|
run: uv run pyright
|
||||||
|
```
|
||||||
|
|
||||||
|
This adds a single `typecheck` job that installs `uv`, `python`, and my project
|
||||||
|
dependencies, and then runs `uv run pyright` (just like I do locally) to perform
|
||||||
|
type checking. If `pyright` discovers any type errors, the job will fail and I
|
||||||
|
can view the output of the job to see what needs fixing. Once I have dealt with
|
||||||
|
everything, the job will quietly pass with a green check mark.
|
||||||
|
|
||||||
|
Here is [the PR](https://github.com/jbranchaud/py-vmt/pull/2) where I added this
|
||||||
|
CI job.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Skip Specific Pytest Test Cases
|
||||||
|
|
||||||
|
While using a failing test case to build a small new feature for
|
||||||
|
[`py-vmt`](https://github.com/jbranchaud/py-vmt), I realized I needed to do some
|
||||||
|
refactoring first. It wasn't significant enough to warrant stashing my current
|
||||||
|
changes and switching to a different branch, so I kept all the changes around. I
|
||||||
|
did find the initial failing test distracting from the refactoring I was trying
|
||||||
|
to do. To temporarily shelve that failure, I can use a Pytest decorator to mark
|
||||||
|
it as _skipped_.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.skip(reason="not yet implemented")
|
||||||
|
def test_log_recent_activity():
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
# set up the data dir file with some existing session entries
|
||||||
|
initial_datetime = datetime.datetime(
|
||||||
|
2026, 3, 14, 15, 5, 11, 0, datetime.timezone.utc
|
||||||
|
)
|
||||||
|
with freeze_time(initial_datetime) as frozen_datetime:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The [`@pytest.mark.skip` decorator](https://docs.pytest.org/en/stable/how-to/skipping.html#skipping-test-functions)
|
||||||
|
tells the Pytest runner to skip of that specific test case instead of executing
|
||||||
|
it. In the test runner output, I'll see an `s` rather than a `.` or `F` and the
|
||||||
|
summary will include it in a count of skipped tests:
|
||||||
|
|
||||||
|
```
|
||||||
|
=========================== 3 failed, 4 passed, 1 skipped in 0.09s ===========================
|
||||||
|
```
|
||||||
|
|
||||||
|
Another way to think about this is to mark this test case as _expected to fail_
|
||||||
|
with `@pytest.mark.xfail`. That will display as an `x` and show up in the summary as:
|
||||||
|
|
||||||
|
```
|
||||||
|
=========================== 3 failed, 4 passed, 1 xfailed in 0.11s ===========================
|
||||||
|
```
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Sort A List Of Dataclass Instances
|
||||||
|
|
||||||
|
Sorting lists of scalar values (integers, strings, floats, even booleans) in
|
||||||
|
Python is simple because the natural ordering of the list elements will be used.
|
||||||
|
We can call `sorted` on the list and it _just works_.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> items = ["orange", "apple", "banana", "mango"]
|
||||||
|
>>> sorted(items)
|
||||||
|
['apple', 'banana', 'mango', 'orange']
|
||||||
|
```
|
||||||
|
|
||||||
|
However, if we have a list of non-scalar values, it is a little more complex. We
|
||||||
|
have to give `sorted` some help with knowing how to sort things that don't have
|
||||||
|
a natural ordering.
|
||||||
|
|
||||||
|
Let's take this `dataclass` that represents a time-based `Session` as an
|
||||||
|
example.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Session:
|
||||||
|
start_time: datetime
|
||||||
|
project_name: str
|
||||||
|
end_time: datetime | None = None
|
||||||
|
|
||||||
|
# plus several methods ...
|
||||||
|
```
|
||||||
|
|
||||||
|
If I have a list of `Session` instances that I want to sort, I have to give
|
||||||
|
`sorted` a `key` to sort on. In the case of these `Session` instances, we'll
|
||||||
|
pass a `lambda` that can be evaluated to determine the sort value (which needs
|
||||||
|
to be sortable). `datetime` instances are sortable and I want to sort these
|
||||||
|
sessions based on their `start_time` values.
|
||||||
|
|
||||||
|
Here is a snippet from my `py_vmt` CLI where I make sure that each list of
|
||||||
|
sessions in this day-by-day `dict` is sorted based on the `start_time`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
for date in sessions_grouped_by_day.keys():
|
||||||
|
sessions_grouped_by_day[date].sort(
|
||||||
|
key=lambda session: session.start_time.time()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`sort` (and `sorted`) translates each item in the list to the values produced
|
||||||
|
by the lambda and then sorts them by those values.
|
||||||
|
|
||||||
|
[source](https://docs.python.org/3/howto/sorting.html)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Sort Normalized Version Of Data
|
||||||
|
|
||||||
|
Let's say I have a list of names that I want to sort. However, because of
|
||||||
|
inconsistency in how the data was entered, sometimes those names are capitalized
|
||||||
|
and other times they are not. Using
|
||||||
|
[`methodcaller`](https://docs.python.org/3/library/operator.html#operator.methodcaller),
|
||||||
|
I can normalize the sorting `key` used when comparing list items.
|
||||||
|
|
||||||
|
First, let's look at calling `sorted` with the list and no `key`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> sorted(["butler", "Jemisin", "le guin", "Erdrich"])
|
||||||
|
['Erdrich', 'Jemisin', 'butler', 'le guin']
|
||||||
|
```
|
||||||
|
|
||||||
|
`butler` which starts with a `b` gets moved to the 3rd position because it is
|
||||||
|
lowercase.
|
||||||
|
|
||||||
|
To sort this list using a normalized comparison, we will use `methodcaller` to
|
||||||
|
create a callable out of `lower` which is then passed as the sort `key`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> from operator import methodcaller
|
||||||
|
>>> sorted(["butler", "Jemisin", "le guin", "Erdrich"], key=methodcaller("lower"))
|
||||||
|
['butler', 'Erdrich', 'Jemisin', 'le guin']
|
||||||
|
```
|
||||||
|
|
||||||
|
That's the sort order I was originally hoping for.
|
||||||
|
|
||||||
|
What `methodcaller` is doing is creating a callable function that will invoke
|
||||||
|
`lower` with each string instance as the target. Conceptually similar to
|
||||||
|
`"Erdrich".lower()` or even `getattr("Erdrich", "lower")()` (notice this needs
|
||||||
|
to be immediately invoked).
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Start The Debugger When A Test Errors
|
||||||
|
|
||||||
|
While working on [some
|
||||||
|
tests](https://github.com/jbranchaud/build-an-llm-from-scratch/blob/main/tests/chapter_02/test_bpe_tokenizer.py)
|
||||||
|
for my Byte Pair Encoding tokenizer, I was running into an unexpected test
|
||||||
|
failure. To better understand what was going on, I needed to inspect the state
|
||||||
|
of the program around the time the code raised an exception.
|
||||||
|
|
||||||
|
Instead of needing to manually set a breakpoint at the correct spot to begin
|
||||||
|
debugging, I can run the test with the Pytest-supported `--pdb` flag. That's
|
||||||
|
short for _python debugger_.
|
||||||
|
|
||||||
|
> Start the interactive Python debugger on errors or KeyboardInterrupt
|
||||||
|
|
||||||
|
What this does during a test run is opens you up to the interactive Python
|
||||||
|
debugger at the exact moment an exception is raised. This gives you the ability
|
||||||
|
to inspect values of the program state at that point in execution which could
|
||||||
|
help inform the needed fix.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest -vv --pdb -k "test_train_bpe"
|
||||||
|
```
|
||||||
|
|
||||||
|
There I am running a specific test that matches against `-k "test_train_bpe"`
|
||||||
|
and the python debugger will start up if there is an error.
|
||||||
|
|
||||||
|
See `uv run pytest --help` for more details.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Turn Method Into Cached Property On Class Instance
|
||||||
|
|
||||||
|
I have a class that encapsulates a few things including a somewhat expensive
|
||||||
|
data lookup from a file on disk. When this class is instantiated, it is
|
||||||
|
short-lived and the data that gets pulled from the file on disk is considered
|
||||||
|
fresh for the life of the instance.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CliContext:
|
||||||
|
def __init__(self, verbose: bool) -> None:
|
||||||
|
# ...
|
||||||
|
self.repo = JsonRepository()
|
||||||
|
# ...
|
||||||
|
|
||||||
|
def session_log(self) -> list[Session]:
|
||||||
|
return self.repo.load_session_log()
|
||||||
|
```
|
||||||
|
|
||||||
|
Because this method gets called from a couple places during a single lifecycle,
|
||||||
|
this class would benefit from caching it via the [`@cached_property`
|
||||||
|
decorator](https://docs.python.org/3/library/functools.html#functools.cached_property).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
class CliContext:
|
||||||
|
def __init__(self, verbose: bool) -> None:
|
||||||
|
# ...
|
||||||
|
self.repo = JsonRepository()
|
||||||
|
# ...
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def session_log(self) -> list[Session]:
|
||||||
|
return self.repo.load_session_log()
|
||||||
|
```
|
||||||
|
|
||||||
|
Now `session_log` can be treated like a property instead of a method. That means
|
||||||
|
when I want to load and access the session log, I can do `self.session_log` (no
|
||||||
|
parentheses) like I would any other property. The first time I reference it, the
|
||||||
|
method will run. Then that value will be cached and all subsequent references
|
||||||
|
will use that cache.
|
||||||
|
|
||||||
|
> Transform a method of a class into a property whose value is computed once and
|
||||||
|
> then cached as a normal attribute for the life of the instance.
|
||||||
|
|
||||||
|
Of course, anytime we use caching, we can create a footgun for ourselves. We
|
||||||
|
have to be careful that our program doesn't evolve in such a way where the
|
||||||
|
caching will create a subtle bug due to stale data.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Use `__post_init__` For `dataclass` Validations
|
||||||
|
|
||||||
|
The [`dataclass`](https://docs.python.org/3/library/dataclasses.html) construct
|
||||||
|
is a handy stdlib way of modeling some data with many improvements over a `dict`
|
||||||
|
such as named attributes and type visibility.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BPEConfig:
|
||||||
|
BASE_VOCAB_SIZE: ClassVar[int] = 256
|
||||||
|
|
||||||
|
vocab_size: int
|
||||||
|
special_tokens: list[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
I want to enhance `BPEConfig` a little by validating the `vocab_size` which
|
||||||
|
cannot be less than the `BASE_VOCAB_SIZE`. The
|
||||||
|
[`__post_init__`](https://docs.python.org/3/library/dataclasses.html#dataclasses.__post_init__)
|
||||||
|
method is a good place for this kind of validation.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BPEConfig:
|
||||||
|
BASE_VOCAB_SIZE: ClassVar[int] = 256
|
||||||
|
|
||||||
|
vocab_size: int
|
||||||
|
special_tokens: list[str]
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.vocab_size < self.BASE_VOCAB_SIZE:
|
||||||
|
msg = f"vocab_size ({self.vocab_size}) must be greater than or equal to BASE_VOCAB_SIZE ({self.BASE_VOCAB_SIZE})"
|
||||||
|
raise ValueError(msg)
|
||||||
|
```
|
||||||
|
|
||||||
|
With this in place, my program will fail fast if I try to use an invalid
|
||||||
|
`vocab_size`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> BPEConfig(22, [])
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
File "<string>", line 5, in __init__
|
||||||
|
File "/Users/lastword/dev/misc/build-an-llm/chapter_02/bpe_tokenizer.py", line 24, in __post_init__
|
||||||
|
raise ValueError(msg)
|
||||||
|
ValueError: vocab_size (22) must be greater than or equal to BASE_VOCAB_SIZE (256)
|
||||||
|
```
|
||||||
|
|
||||||
|
This example is pulled directly from [the `BPETokenizer` I'm building](https://github.com/jbranchaud/build-an-llm-from-scratch/blob/d3fd0acd65c3e7419b2d15a64c8d74266d0488f6/chapter_02/bpe_tokenizer.py#L14-L24).
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Validate Click Option With Callback
|
||||||
|
|
||||||
|
I have a [click](https://click.palletsprojects.com/en/stable/) subcommand in my
|
||||||
|
[`py-vmt` project](https://github.com/jbranchaud/py-vmt) that includes an
|
||||||
|
`option` specified with the `--at` flag. This is what it originally looked like:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# define `start` subcommand
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("project-name")
|
||||||
|
@click.option("--at", help='Relative time in past to start the time, e.g. "2 hours ago", "33 minutes ago"')
|
||||||
|
@pass_cli
|
||||||
|
def start(cli_ctx: CliContext, project_name: str, at: str | None) -> None:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The value of `at` needs to be in the past. I need a way validate that it is or
|
||||||
|
otherwise bail early with a useful error message. The optional
|
||||||
|
[`callback`](https://click.palletsprojects.com/en/stable/advanced/#callbacks-for-validation)
|
||||||
|
to `@click.option` plus `click.BadParameter` are a good way to handle that.
|
||||||
|
|
||||||
|
First, I define a callback handler that does the validation. I even take it a
|
||||||
|
step further and have it return the transformed value (`datetime`) that the
|
||||||
|
subcommand logic will need.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def validate_past_time(_ctx, _param, value: str | None) -> datetime:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
if value == None:
|
||||||
|
return now
|
||||||
|
|
||||||
|
start_at = time_helpers.parse_to_datetime(value)
|
||||||
|
|
||||||
|
if start_time == None or start_at > now:
|
||||||
|
raise click.BadParameter("must be a relative time in the past")
|
||||||
|
|
||||||
|
return start_at
|
||||||
|
```
|
||||||
|
|
||||||
|
I ignore the first two arguments because I only need to work with `value`. Value
|
||||||
|
might be something like `"33 minutes ago"` and I attempt to transform that with
|
||||||
|
`dateparser` into a `datetime` instance. If it can't be parsed or it isn't in
|
||||||
|
the past, then I raise `click.BadParameter` which presents the user with useful
|
||||||
|
usage details.
|
||||||
|
|
||||||
|
This callback can then be incorporated into the subcommand like so:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# define `start` subcommand
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("project-name")
|
||||||
|
@click.option(
|
||||||
|
"--at",
|
||||||
|
help='Relative time in past to start the time, e.g. "2 hours ago", "33 minutes ago"',
|
||||||
|
callback=validate_past_time
|
||||||
|
)
|
||||||
|
@pass_cli
|
||||||
|
def start(cli_ctx: CliContext, project_name: str, at: datetime) -> None:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now I can expect the incoming `at` option to be a `datetime` which helps
|
||||||
|
simplify several lines of logic in the `start` implementation.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Define Conditional Routing Logic In Routes File
|
||||||
|
|
||||||
|
I ran into a situation recently where I needed to intercept the behavior a
|
||||||
|
common public-facing route in an app. Broadly, the route is for company specific
|
||||||
|
rental pages with query parameters that correspond to their available inventory.
|
||||||
|
|
||||||
|
What I needed was a way to display a demo version of that rental page ignoring
|
||||||
|
everything else about how the request would otherwise be processed, validated,
|
||||||
|
and rendered.
|
||||||
|
|
||||||
|
Instead of introducing a bunch of weird conditional logic into this already
|
||||||
|
complex rental controller, I was able to intercept the request at the routing
|
||||||
|
layer when `demo=true` is set and send it to a different controller.
|
||||||
|
|
||||||
|
Here is what that section of `config/routes.rb` looks like:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
get "rentals/new", to: "rental_demos#show",
|
||||||
|
as: :rental_demo,
|
||||||
|
constraints: ->(request) { request.params[:demo] == "true" }
|
||||||
|
|
||||||
|
resources :rentals, only: %i[new create] do
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This specifies a `constraint` on the `get` handler matching for a given request.
|
||||||
|
If the constraint isn't met, then the route handling logic proceeds where it
|
||||||
|
will instead find a match with the original new rentals resource routing.
|
||||||
|
|
||||||
|
Now I can reference a version of this URL that includes `demo=true` as a way of
|
||||||
|
having an always-available realistic-looking version of the rental page even if
|
||||||
|
one of these companies doesn't actively have available inventory.
|
||||||
|
|
||||||
|
Those requests will get intercepted by the first matching route handler which
|
||||||
|
will send them to the `RentalDemosController` instead of the
|
||||||
|
`RentalsController`.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Halt ActionMailer Delivery With Callback
|
||||||
|
|
||||||
|
`ActionMailer` supports callbacks, similar to `ActiveRecord`, like
|
||||||
|
`before_deliver` and `after_delivery`. We can hook into the `before_deliver`
|
||||||
|
callback to interrupt the delivery of an email that shouldn't go out.
|
||||||
|
|
||||||
|
Here's the scenario: you schedule a bunch of payment reminders to go out to your
|
||||||
|
customers that still need to make their latest payment. Let's say the daily job
|
||||||
|
that schedules all of these reminders runs in the middle of the night, but
|
||||||
|
schedules the emails to land in inboxes at a more reasonable time, like 10am.
|
||||||
|
Between the time that the email is scheduled and it gets processed for delivery,
|
||||||
|
a customer makes their payment. In that case, we no longer want to send that
|
||||||
|
person an email reminder.
|
||||||
|
|
||||||
|
To handle this scenario, we can have a `before_deliver` callback that checks the
|
||||||
|
user's balance and raises `:abort` to halt the callback execution chain,
|
||||||
|
effectively preventing the email from going out. We can even scope the callback
|
||||||
|
to just the actions we care about using the `if` option and checking the
|
||||||
|
`action_name`.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class UserMailer < ApplicationMailer
|
||||||
|
before_deliver :abort_if_payment_is_current,
|
||||||
|
if: -> { action_name.in?(%w[payment_reminder past_due_invoice]) }
|
||||||
|
|
||||||
|
def payment_reminder
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
|
||||||
|
def past_due_invoice
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def abort_if_payment_is_current
|
||||||
|
if @user.check_latest_balance.zero?
|
||||||
|
raise :abort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Action Mailer
|
||||||
|
Callbacks](https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-callbacks)
|
||||||
|
for more details.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Define A Set Of Class Methods
|
||||||
|
|
||||||
|
The most common way to define class methods is by defining them directly with
|
||||||
|
`self` (the class in the current context) on a method by method basis:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class User
|
||||||
|
def self.find_by(attrs)
|
||||||
|
# lookup logic ...
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have a group of class methods you want to define, you can stick them all
|
||||||
|
within a `class << self` block which does similarly defines each of them as
|
||||||
|
singleton methods of that class (`User` in this case):
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class User
|
||||||
|
class << self
|
||||||
|
def find_by_email(email)
|
||||||
|
# lookup logic ...
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_by_last_name(last_name)
|
||||||
|
# lookup logic ...
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This opens the singleton class of `User` for modification, adding these two new
|
||||||
|
methods.
|
||||||
|
|
||||||
|
We can see those defined alongside all other direct and inherited class methods:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> User.methods
|
||||||
|
=>
|
||||||
|
[:find_by_email,
|
||||||
|
:find_by_last_name,
|
||||||
|
:yaml_tag,
|
||||||
|
:allocate,
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Use Rescue As Part Of Inline Statement
|
||||||
|
|
||||||
|
In Ruby I typically think of `rescue` as block syntax that I can use to handle
|
||||||
|
exceptions.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
begin
|
||||||
|
User.update!(password:)
|
||||||
|
rescue
|
||||||
|
puts "There was an issue updating the password"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The `rescue` keyword can also be used as part of an inline statement as a way of
|
||||||
|
providing a _fallback_ value when the first part of the statement raises.
|
||||||
|
|
||||||
|
For instance, if I'm trying to access some value on an array that happens to be
|
||||||
|
`nil`, it is going to raise:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> scores.first
|
||||||
|
(irb):7:in '<main>': undefined method 'first' for nil (NoMethodError)
|
||||||
|
```
|
||||||
|
|
||||||
|
I can instead tack on a `rescue 0` which will give it `0` as a fallback value:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> scores.first rescue 0
|
||||||
|
=> 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Of course, there are more idiomatic ways to handle this kind of situation in
|
||||||
|
Ruby. Maybe something like this:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> Array(scores).first || 0
|
||||||
|
=> 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Another way I've seen this inline rescue used is to print out the exception
|
||||||
|
caused by that line of code, using `$!` (the global variable for the most
|
||||||
|
recently raised exception).
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
> scores.first rescue puts $!
|
||||||
|
undefined method 'first' for nil
|
||||||
|
=> nil
|
||||||
|
```
|
||||||
|
|
||||||
|
That is a one-liner for the following:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
begin
|
||||||
|
scores.first
|
||||||
|
rescue => e
|
||||||
|
puts e
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The big caveat that goes with this is the same one that goes with any other
|
||||||
|
blanket `rescue` block. If you are indiscriminately rescuing exceptions without
|
||||||
|
being intentional about what you are rescuing and why, you could be potentially
|
||||||
|
burying exceptions that you need to know about.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Add Default Task To List All Tasks
|
||||||
|
|
||||||
|
One thing I like about [`just`](https://github.com/casey/just) is that if you
|
||||||
|
run `just` by itself, the default behavior is to list out all the commands it
|
||||||
|
can run.
|
||||||
|
|
||||||
|
[Taskfile](https://github.com/go-task/task) technically does this as well, but
|
||||||
|
with a warning at the end:
|
||||||
|
|
||||||
|
```
|
||||||
|
❯ task
|
||||||
|
task: Available tasks for this project:
|
||||||
|
* notes: Interactive picker for notes tasks
|
||||||
|
* notes:diff: Show uncommitted changes in notes
|
||||||
|
* notes:edit: All-in-one edit, commit, and push notes
|
||||||
|
* notes:log: Show recent commit history for notes
|
||||||
|
* notes:open: Opens NOTES.md (syncs latest changes first) in default editor
|
||||||
|
* notes:push: Commit and push changes to notes submodule
|
||||||
|
* notes:status: Check status of notes submodule
|
||||||
|
* notes:sync: Sync latest changes from the notes submodule
|
||||||
|
task: Task "default" does not exist
|
||||||
|
```
|
||||||
|
|
||||||
|
I prefer to tidy this up a little by adding `task --list` as the _default_ in my
|
||||||
|
`Taskfile.yml`.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
default:
|
||||||
|
desc: Show available commands
|
||||||
|
cmds:
|
||||||
|
- task --list
|
||||||
|
```
|
||||||
|
|
||||||
|
Now when I run `task` with no arguments, I get this minutely nicer version:
|
||||||
|
|
||||||
|
```
|
||||||
|
❯ task
|
||||||
|
Alias tip: t
|
||||||
|
task: [default] task --list
|
||||||
|
task: Available tasks for this project:
|
||||||
|
* default: Show available commands
|
||||||
|
* notes: Interactive picker for notes tasks
|
||||||
|
* notes:diff: Show uncommitted changes in notes
|
||||||
|
* notes:edit: All-in-one edit, commit, and push notes
|
||||||
|
* notes:log: Show recent commit history for notes
|
||||||
|
* notes:open: Opens NOTES.md (syncs latest changes first) in default editor
|
||||||
|
* notes:push: Commit and push changes to notes submodule
|
||||||
|
* notes:status: Check status of notes submodule
|
||||||
|
* notes:sync: Sync latest changes from the notes submodule
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice there is no `task: Task "default" does not exist` warning at the end.
|
||||||
@@ -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,64 @@
|
|||||||
|
# Reverse Each Line Of A File
|
||||||
|
|
||||||
|
The [`rev` command](https://man7.org/linux/man-pages/man1/rev.1.html) can be
|
||||||
|
used to reverse each line in a file. Every line is left where it is relative to
|
||||||
|
other lines, but the contents of each line is reversed.
|
||||||
|
|
||||||
|
So a file that contains the following text:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ cat stuff.md
|
||||||
|
Three
|
||||||
|
Two
|
||||||
|
One
|
||||||
|
go racecar go
|
||||||
|
```
|
||||||
|
|
||||||
|
can be piped to `rev` to get the following output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ rev stuff.md
|
||||||
|
eerhT
|
||||||
|
owT
|
||||||
|
enO
|
||||||
|
og racecar og
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an odd utility that doesn't have too much use that I can imagine. After
|
||||||
|
a brief chat with Claude where I asked for some practical use cases, the one
|
||||||
|
that stood out the most to me is to reverse a list of filenames, sort them, and
|
||||||
|
then reverse them again (putting them back in readable order). This can shuffle
|
||||||
|
filenames with similar endings near each other like source and test files.
|
||||||
|
|
||||||
|
Here is a list of files for me [`py-vmt`
|
||||||
|
project](https://github.com/jbranchaud/py-vmt):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ fd -t f .
|
||||||
|
README.md
|
||||||
|
pyproject.toml
|
||||||
|
src/py_vmt/__init__.py
|
||||||
|
src/py_vmt/cli.py
|
||||||
|
src/py_vmt/session.py
|
||||||
|
src/py_vmt/time_helpers.py
|
||||||
|
tests/src/py_vmt/test_cli.py
|
||||||
|
tests/src/py_vmt/test_session.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Now I can pipe the output of that `fd` command through `rev | sort | rev` to get
|
||||||
|
my files organized in a different way.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ fd -t f . | rev | sort | rev
|
||||||
|
README.md
|
||||||
|
pyproject.toml
|
||||||
|
src/py_vmt/__init__.py
|
||||||
|
tests/src/py_vmt/test_cli.py
|
||||||
|
src/py_vmt/cli.py
|
||||||
|
tests/src/py_vmt/test_session.py
|
||||||
|
src/py_vmt/session.py
|
||||||
|
src/py_vmt/time_helpers.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Again the value of doing something like this is a bit tenuous. At the very least
|
||||||
|
it is fun to know about.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Use The Readline Keybindings Anywhere
|
||||||
|
|
||||||
|
There are these features of the "shell" that I've often heard called _emac
|
||||||
|
keybindings_. These are things like `ctrl-a` (move the cursor to the beginning
|
||||||
|
of the line) and `ctrl-e` (move the cursor to the end of the line) that I use
|
||||||
|
every single day. There are several others that are in my heavy rotation,
|
||||||
|
however, I learned about a couple more reading through [Shell Tricks That
|
||||||
|
Actually Make Life Easier (And Save Your
|
||||||
|
Sanity)](https://blog.hofstede.it/shell-tricks-that-actually-make-life-easier-and-save-your-sanity/).
|
||||||
|
|
||||||
|
These are [Readline commands](https://www.gnu.org/software/bash/manual/html_node/Bindable-Readline-Commands.html)
|
||||||
|
(or keybindings) which means they are supported by anything that uses Readline
|
||||||
|
under the hood. So while you might be using these to great effect in `bash` and
|
||||||
|
`zsh`, you should look for other places they are available.
|
||||||
|
|
||||||
|
A non-exhaustive list includes:
|
||||||
|
|
||||||
|
- Ruby's `irb`
|
||||||
|
- Python's `python`
|
||||||
|
- Node.js' `node`
|
||||||
|
- PostgreSQL's `psql`
|
||||||
|
- Claude Code
|
||||||
|
|
||||||
|
And many more similar REPLs and command line tools.
|
||||||
|
|
||||||
|
Try these keybindings out in one of your favorites and when you're done hit
|
||||||
|
`ctrl-c` to exit out of it.
|
||||||
|
|
||||||
|
PS. subsets of these keybindings are sometimes supported in unexpected places
|
||||||
|
like the Chrome URL bar.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Show All Linear Keyboard Shortcuts
|
||||||
|
|
||||||
|
Linear, the project management software, puts an incredible amount of attention
|
||||||
|
to detail into the UX and UI of their app. This includes making the app a power
|
||||||
|
tool for power users with tons of keyboard shortcuts.
|
||||||
|
|
||||||
|
I'm aware of some of Linear's keyboard shortcuts, but the discoverability of
|
||||||
|
many of them is tough.
|
||||||
|
|
||||||
|
A great way to list and browse through all of them right in the app is with
|
||||||
|
`Cmd+/`.
|
||||||
|
|
||||||
|
They are organized into sections that I can scroll through. There is also a
|
||||||
|
search box at the top of this _Keyboard Shortcuts_ panel where I can narrow down
|
||||||
|
the results to those that match a term.
|
||||||
|
|
||||||
|
A few that I'm finding immediately useful are:
|
||||||
|
|
||||||
|
- `gi` to go to my _Inbox_ in the current workspace
|
||||||
|
- `gm` to go to _My Issues_ in the current workspace
|
||||||
|
- `ow` to open a picker to switch between workspaces
|
||||||
|
|
||||||
|
Note: the _Keyboard Shortcuts_ panel lists many of the letter-based shortcuts as
|
||||||
|
being capitalized. I've found that these don't work when I hold shift. For that
|
||||||
|
reason, I've listed the above shortcuts with lowercase letters.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# View Nicely Formatted Markdown From Terminal
|
||||||
|
|
||||||
|
The [`glow`](https://github.com/charmbracelet/glow) utility is CLI markdown
|
||||||
|
renderer written in Go. It is part of the CCU
|
||||||
|
([charmbraclet](https://github.com/charmbracelet) CLI universe). And yes, I just
|
||||||
|
made up _CCU_.
|
||||||
|
|
||||||
|
`glow` is great because it processes and outputs a markdown file with some
|
||||||
|
styling tailored to a terminal including:
|
||||||
|
|
||||||
|
- colors to emphasize things like headings
|
||||||
|
- styling of inline code snippets
|
||||||
|
- syntax highlighting for fenced code blocks
|
||||||
|
- rendering of markdown tables
|
||||||
|
- and a lot more that I'm not thinking to mention
|
||||||
|
|
||||||
|
In the past I've installed this with `brew`, but I currently manage my `glow`
|
||||||
|
install with [this mise config](https://github.com/jbranchaud/dotfiles/blob/main/config/mise/config.toml?plain=1#L66).
|
||||||
|
|
||||||
|
To view a nicely rendered markdown file, I can run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ glow README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
For long markdown files like [this `README.md`](https://github.com/jbranchaud/til/blob/master/README.md), this
|
||||||
|
doesn't work too well because it renders until the end and spits you at at the
|
||||||
|
bottom.
|
||||||
|
|
||||||
|
Fortunately, `glow` has a built-in pager that maintains all the styling while
|
||||||
|
allowing you to navigate and search similar to `less`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ glow -p README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
There is also a TUI version (`-t`), but I find that less intuitive and useful
|
||||||
|
than the pager.
|
||||||
|
|
||||||
|
See `glow --help` for more details.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# List Available Zle Keybindings
|
||||||
|
|
||||||
|
Unlike `bash` which uses `readline`, `zsh` has its own implementation of a line
|
||||||
|
editing library -- `zle`. A lot of the core bindings between the two are the
|
||||||
|
same, e.g. `Ctrl-a` and `Ctrl-e` to go the beginning and end of the command line
|
||||||
|
prompt, respectively.
|
||||||
|
|
||||||
|
All available `zle` keybindings can be listed out by running `bindkey` without
|
||||||
|
any arguments.
|
||||||
|
|
||||||
|
The best way to check out an unaltered version of this list is by starting a
|
||||||
|
fresh `zsh` process with no RCS files loaded in. The `-f` flag does that. Note
|
||||||
|
though that when `zsh` is starting fresh, it has to decide whether to start in
|
||||||
|
_Emacs_ mode or _Vi_ mode. If it sees that your default editor is something like
|
||||||
|
`vi`, `vim` or `nvim`, then it will start you in _Vi_ mode.
|
||||||
|
|
||||||
|
Starting in _Vi_ mode can be confusing because none of the standard _Emacs_
|
||||||
|
keybindings like `Ctrl-a` and `Ctrl-e` are available in that context. So first
|
||||||
|
ensure you're in _Emacs_ mode by running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
❯ zsh -f
|
||||||
|
lastword% bindkey -e
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can list out all the keybindings:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
lastword% bindkey
|
||||||
|
"^@" set-mark-command
|
||||||
|
"^A" beginning-of-line
|
||||||
|
"^B" backward-char
|
||||||
|
"^D" delete-char-or-list
|
||||||
|
"^E" end-of-line
|
||||||
|
"^F" forward-char
|
||||||
|
"^G" send-break
|
||||||
|
"^H" backward-delete-char
|
||||||
|
"^I" expand-or-complete
|
||||||
|
"^J" accept-line
|
||||||
|
"^K" kill-line
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
See `man zshzle` for more details on `zle` and `bindkey`.
|
||||||
Reference in New Issue
Block a user