mirror of
https://github.com/jbranchaud/til
synced 2026-09-02 17:51:47 +00:00
Compare commits
10
Commits
8191e9303b
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee23956d2 | ||
|
|
7e4a02e66f | ||
|
|
965bade9d4 | ||
|
|
2a2a962864 | ||
|
|
a1708116bb | ||
|
|
85a6dd3644 | ||
|
|
453e9edbdf | ||
|
|
bcf957cabd | ||
|
|
fe019c3338 | ||
|
|
5773961c93 |
@@ -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).
|
||||
|
||||
_1868 TILs and counting..._
|
||||
_1877 TILs and counting..._
|
||||
|
||||
See some of the other learning resources I work on:
|
||||
|
||||
@@ -172,6 +172,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Monitor Usage Limits From CLI](claude-code/monitor-usage-limits-from-cli.md)
|
||||
- [Open Current Prompt In Default Editor](claude-code/open-current-prompt-in-default-editor.md)
|
||||
- [Resume Specific Session](claude-code/resume-specific-session.md)
|
||||
- [Set Permission Mode When Starting Session](claude-code/set-permission-mode-when-starting-session.md)
|
||||
- [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md)
|
||||
|
||||
### Clojure
|
||||
@@ -1105,6 +1106,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Escape Curly Braces Within Formatted String](python/escape-curly-braces-within-formatted-string.md)
|
||||
- [Experiment With SQLite Queries In Memory](python/experiment-with-sqlite-queries-in-memory.md)
|
||||
- [Force Remaining Arguments To Be Named](python/force-remaining-arguments-to-be-named.md)
|
||||
- [Generate Sample PDFs With ReportLab](python/generate-sample-pdfs-with-reportlab.md)
|
||||
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md)
|
||||
- [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
|
||||
- [Globally Install CLI Tool With UV](python/globally-install-cli-tool-with-uv.md)
|
||||
@@ -1124,12 +1126,14 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
|
||||
- [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md)
|
||||
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
|
||||
- [Register SQLite Adapter To Serialize Datetimes](python/register-sqlite-adapter-to-serialize-datetimes.md)
|
||||
- [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md)
|
||||
- [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md)
|
||||
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
|
||||
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
|
||||
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
|
||||
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
|
||||
- [Specify Parameter Name For Click Option](python/specify-parameter-name-for-click-option.md)
|
||||
- [Start Jupyter Notebook With Extra Packages](python/start-jupyter-notebook-with-extra-packages.md)
|
||||
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
|
||||
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
|
||||
@@ -1648,9 +1652,11 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
|
||||
### SQLite
|
||||
|
||||
- [Add Check Constraint To Existing Column](sqlite/add-check-constraint-to-existing-column.md)
|
||||
- [Display Results In Readable Column Format](sqlite/display-results-in-readable-column-format.md)
|
||||
- [Explore The Database Schema](sqlite/explore-the-database-schema.md)
|
||||
- [Manage Lightweight Schema Migrations With `user_version`](sqlite/manage-lightweight-schema-migrations-with-user-version.md)
|
||||
- [Output Query Result In Nicely Formatted Table](sqlite/output-query-result-in-nicely-formatted-table.md)
|
||||
|
||||
### Streaming
|
||||
|
||||
@@ -1680,6 +1686,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Change Base Directory Of Existing Session](tmux/change-base-directory-of-existing-session.md)
|
||||
- [Change Base Directory Without Detaching](tmux/change-base-directory-without-detaching.md)
|
||||
- [Change The Default Prefix Key](tmux/change-the-default-prefix-key.md)
|
||||
- [Connect To Individual Overmind Processes Via tmux](tmux/connect-to-individual-overmind-processes-via-tmux.md)
|
||||
- [Create A Named tmux Session](tmux/create-a-named-tmux-session.md)
|
||||
- [Create A New Session In A New Server](tmux/create-a-new-session-in-a-new-server.md)
|
||||
- [Cycle Through Layouts](tmux/cycle-through-layouts.md)
|
||||
@@ -1752,6 +1759,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Check SSH Key Fingerprints Of Known Hosts](unix/check-ssh-key-fingerprints-of-known-hosts.md)
|
||||
- [Check The Current Working Directory](unix/check-the-current-working-directory.md)
|
||||
- [Check The Installed OpenSSL Version](unix/check-the-installed-openssl-version.md)
|
||||
- [Check What Is Inside A Zip File](unix/check-what-is-inside-a-zip-file.md)
|
||||
- [Clear The Screen](unix/clear-the-screen.md)
|
||||
- [Combine All My TILs Into A Single File](unix/combine-all-my-tils-into-a-single-file.md)
|
||||
- [Command Line Length Limitations](unix/command-line-length-limitations.md)
|
||||
@@ -2143,6 +2151,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Open Slack's Keyboard Shortcuts Reference Panel](workflow/open-slacks-keyboard-shortcuts-reference-panel.md)
|
||||
- [Pop Videos Out As Picture-in-Picture](workflow/pop-videos-out-as-picture-in-picture.md)
|
||||
- [Prune The Excess From node_modules](workflow/prune-the-excess-from-node-modules.md)
|
||||
- [Remove Pages From A PDF](workflow/remove-pages-from-a-pdf.md)
|
||||
- [Rotate An Image To Be Oriented Upright](workflow/rotate-an-image-to-be-oriented-upright.md)
|
||||
- [See Overlaps For A Set Of Time Zones](workflow/see-overlaps-for-a-set-of-time-zones.md)
|
||||
- [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Set Permission Mode When Starting Session
|
||||
|
||||
The way I typically use Claude Code day-to-day is with a couple long-running
|
||||
sessions for one to two clones of the project. I start a session with `claude`
|
||||
and then hit `shift+tab` until I've toggled it to _auto_ mode. I do tightly
|
||||
scoped features and `/clear` the context in between each.
|
||||
|
||||
I get used to being in _auto_ mode, so whenever I start a new `claude` session I
|
||||
forget to first toggle from _manual_ to _auto_ mode.
|
||||
|
||||
This is where the
|
||||
[`--permission-mode`](https://code.claude.com/docs/en/permission-modes) flag can
|
||||
help. I can start a session directly in _auto_ mode like so:
|
||||
|
||||
```bash
|
||||
❯ claude --permission-mode auto
|
||||
```
|
||||
|
||||
Or if I know I want to generate a plan first, I can start it in _plan_ mode.
|
||||
|
||||
```bash
|
||||
❯ claude --permission-mode plan
|
||||
```
|
||||
|
||||
There is also the `--dangerously-skip-permissions` flag which is equivalent to
|
||||
`--permission-mode bypassPermissions`. I tend to stay away from those unless I'm
|
||||
working from a sandboxed dev container.
|
||||
|
||||
See `claude --help` for more details.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generate Sample PDFs With ReportLab
|
||||
|
||||
The `reportlab` package has a `pdfgen` module that provides a `Canvas` API for
|
||||
programmatically constructing a PDF.
|
||||
|
||||
I recently needed the most basic use of this to quickly construct a sample PDF
|
||||
that I could use as I experimented with [different `qpdf`
|
||||
commands](/workflow/remove-pages-from-a-pdf.md). I wanted to generate a 10-page
|
||||
PDF where each page included a large font number in the center. This way I could
|
||||
clearly see what was the first, second, third, and so on pages. Then I could
|
||||
quickly verify that the `qpdf` manipulations I was doing were working as
|
||||
expected.
|
||||
|
||||
I was able to do this without any setup or install. Using a `uv` one-liner (does
|
||||
a 10-line heredoc count as a one-liner?), I generated a PDF with some basic
|
||||
canvas rendering on each page.
|
||||
|
||||
```python
|
||||
❯ uv run --with reportlab python - <<'EOF'
|
||||
from reportlab.pdfgen import canvas
|
||||
N, W, H = 10, 612, 792
|
||||
c = canvas.Canvas("test.pdf", pagesize=(W, H))
|
||||
for i in range(1, N + 1):
|
||||
c.setFont("Helvetica-Bold", 220)
|
||||
c.drawCentredString(W / 2, H / 2 - 80, str(i))
|
||||
c.setFont("Helvetica", 28)
|
||||
c.drawCentredString(W / 2, 100, f"page {i} of {N}")
|
||||
c.showPage()
|
||||
c.save()
|
||||
EOF
|
||||
```
|
||||
|
||||
The `--with` makes the `reportlab` package available to the command being run.
|
||||
The command being run is `python` which evaluates that block of heredoc text.
|
||||
|
||||
Within the heredoc, I import `canvas` from the `reportlab` package that has been
|
||||
made available. I then create an in-memory PDF of a standard size. Then for-loop
|
||||
10 times to build each page of the PDF with a big number in the middle and a
|
||||
smaller `page x of N` lower down on the page. Finally, I save the PDF which
|
||||
writes it to the directory I executed this whole command from.
|
||||
|
||||
I had Claude help with the `canvas` API, however there are some decent
|
||||
[`reportlab.pdfgen` docs
|
||||
here](https://docs.reportlab.com/reportlab/userguide/ch2_graphics/).
|
||||
@@ -0,0 +1,93 @@
|
||||
# Register SQLite Adapter To Serialize Datetimes
|
||||
|
||||
SQLite doesn't have a datetime or timestamp data type, so that information has
|
||||
to be stored as `text` or a unix epoch `int`. That means when using Python's
|
||||
`sqlite3` module to perform writes I need to tell it what shape to write a
|
||||
datetime value.
|
||||
|
||||
I could manually convert `datetime` values everywhere they are involved in
|
||||
_write_ operations.
|
||||
|
||||
```python
|
||||
# Prepare `sessions` insert payload
|
||||
session_data = {
|
||||
"active": 1 if active else 0,
|
||||
"project_id": project_id,
|
||||
"start_time": datetime.isoformat(session.start_time),
|
||||
"end_time": None,
|
||||
}
|
||||
|
||||
if session.end_time:
|
||||
session_data["end_time"] = datetime.isoformat(session.end_time)
|
||||
|
||||
# Insert the new active session
|
||||
cursor = self.conn.execute(
|
||||
"""
|
||||
insert into sessions (active, project_id, start_time, end_time)
|
||||
values (:active, :project_id, :start_time, :end_time)
|
||||
returning id;
|
||||
""",
|
||||
session_data,
|
||||
)
|
||||
```
|
||||
|
||||
I've used `datetime.isoformat` above which formats `datetime` objects like so:
|
||||
|
||||
```python
|
||||
>>> datetime.now().isoformat()
|
||||
'2026-08-28T11:52:04.709907'
|
||||
```
|
||||
|
||||
I'd like to make two improvements.
|
||||
|
||||
1. I want these `datetime` values to be formatted instead like `2026-08-28T18:15:27.213Z`.
|
||||
2. I want `datetime` values to be serialized automatically in the specific shape
|
||||
without having to manually convert them everywhere.
|
||||
|
||||
I can achieve both of these things by [registering an adapter with `sqlite3`](https://docs.python.org/3/library/sqlite3.html#how-to-register-adapter-callables)
|
||||
that handles the serialize of `datetime` objects.
|
||||
|
||||
First, I define a function that can perform the `datetime` to `str` conversion.
|
||||
I decided to put this in `db.py` where I have some other database-specific
|
||||
functions.
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
|
||||
def to_db(dt: datetime) -> str:
|
||||
if dt.tzinfo is None or dt.utcoffset() is None:
|
||||
raise ValueError(f"Unable to store naive datetime: {dt!r}")
|
||||
dt = dt.astimezone(timezone.utc)
|
||||
return f"{dt:%Y-%m-%dT%H:%M:%S}.{dt.microsecond // 1000:03d}Z"
|
||||
```
|
||||
|
||||
Then I register the adapter before creating the connection that gets used for
|
||||
database interactions.
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from sqlite3 import Connection
|
||||
|
||||
def initialize_conn(db_file: Path) -> Connection:
|
||||
# register adapters
|
||||
sqlite3.register_adapter(datetime, to_db)
|
||||
|
||||
conn: Connection = sqlite3.connect(db_file)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
return conn
|
||||
```
|
||||
|
||||
Then I can run write operations with `datetime` objects knowing they will be
|
||||
correctly serialized.
|
||||
|
||||
```python
|
||||
with self.conn:
|
||||
query = "update sessions set active = :active, end_time = :end_time where active = 1;"
|
||||
self.conn.execute(
|
||||
query,
|
||||
{"active": 0, "end_time": session.end_time},
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
# Specify Parameter Name For Click Option
|
||||
|
||||
[Click](https://click.palletsprojects.com/en/stable/)'s option decorator
|
||||
provides a versatile way to define flags for a command. It has good defaults
|
||||
that minimize the aspects of a flag that I need to be explicit about.
|
||||
|
||||
For example, a boolean `--init` flag for the `config` command could be specified
|
||||
like so:
|
||||
|
||||
```python
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--init",
|
||||
help="Initialize a config file with minimal defaults",
|
||||
is_flag=True,
|
||||
)
|
||||
@pass_cli
|
||||
def config(cli_ctx: CliContext, init: bool):
|
||||
# ...
|
||||
```
|
||||
|
||||
Notice, in particular, that the flag string (`--init`) that I pass as the first
|
||||
argument to `@click.option` has to correspond to the name of the parameter
|
||||
`init`. Click passes all the defined options as keyword arguments when invoking
|
||||
`config`. If the `init` parameter was changed to `initial`, there would be a
|
||||
runtime error like this: `TypeError: config() got an unexpected keyword argument 'init'`.
|
||||
|
||||
Like I said though, Click is flexible when I need it to be. I can leave the flag
|
||||
name as it is, but specify a different name to be used for the function
|
||||
parameter. I found this useful when I realized that as I added support for a
|
||||
`--json` flag I was inadvertently superseding the `json` import.
|
||||
|
||||
The second positional argument to `@click.option` can be included to rename that
|
||||
parameter:
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--json",
|
||||
"use_json",
|
||||
help="Output all info details in JSON format",
|
||||
is_flag=True,
|
||||
)
|
||||
@pass_cli
|
||||
def info(cli_ctx: CliContext, use_json: bool):
|
||||
# ...
|
||||
|
||||
if use_json:
|
||||
click.echo(json.dumps(info_details, indent=2))
|
||||
```
|
||||
|
||||
Both of these code blocks are excerpts from my [`py-vmt` time tracker project](https://github.com/jbranchaud/py-vmt).
|
||||
@@ -0,0 +1,74 @@
|
||||
# Add Check Constraint To Existing Column
|
||||
|
||||
To not bury the lede, SQLite does not support adding a check constraint to an
|
||||
existing column. Instead, if I want to add one after the fact, I can reconstruct
|
||||
the table with the check constraint specified up front and then migrate the
|
||||
data.
|
||||
|
||||
Here is a `sessions` table that includes `start_time` and `end_time` columns
|
||||
that use the `text` data type to represent points in time.
|
||||
|
||||
```sql
|
||||
sqlite> .schema sessions
|
||||
CREATE TABLE sessions (
|
||||
id integer primary key,
|
||||
active integer not null check (active in (0, 1)),
|
||||
project_id integer not null references projects(id) on delete cascade,
|
||||
start_time text not null,
|
||||
end_time text,
|
||||
created_at text not null default (datetime('now')),
|
||||
updated_at text not null default (datetime('now'))
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_sessions_single_active
|
||||
on sessions(active)
|
||||
where active = 1;
|
||||
```
|
||||
|
||||
I want `start_time` and `end_time` to both enforce the shape of the timestamp
|
||||
strings with `check` constraints. To do that, I need to start a transaction,
|
||||
create a new version of the table with the check constraints, migrate the data,
|
||||
rename `sessions` to `sessions_old`, rename `sessions_new` to `sessions` (that's
|
||||
the in-place swap), and then commit the transaction. `sessions_old` can be
|
||||
dropped later once I feel good about the migration.
|
||||
|
||||
```sql
|
||||
begin transaction;
|
||||
|
||||
create table sessions_new (
|
||||
id integer primary key,
|
||||
active integer not null check (active in (0, 1)),
|
||||
project_id integer not null references projects(id) on delete cascade,
|
||||
start_time text not null,
|
||||
end_time text,
|
||||
created_at text not null default (datetime('now')),
|
||||
updated_at text not null default (datetime('now')),
|
||||
check(start_time is strftime('%Y-%m-%dT%H:%M:%fZ', start_time)),
|
||||
check(end_time is strftime('%Y-%m-%dT%H:%M:%fZ', end_time))
|
||||
);
|
||||
|
||||
insert into sessions_new (
|
||||
id, active, project_id, start_time, end_time, created_at, updated_at
|
||||
)
|
||||
select id, active, project_id, start_time, end_time, created_at, updated_at
|
||||
from sessions;
|
||||
|
||||
alter table sessions rename to sessions_old;
|
||||
|
||||
alter table sessions_new rename to sessions;
|
||||
|
||||
drop index if exists idx_sessions_single_active; -- on sessions_old
|
||||
|
||||
create unique index idx_sessions_single_active
|
||||
on sessions(active)
|
||||
where active = 1;
|
||||
|
||||
commit;
|
||||
```
|
||||
|
||||
Notice that after the table renames I also drop index (now pointing to
|
||||
`sessions_old`) and recreate it for the _new_ `sessions` table.
|
||||
|
||||
This approach worked well for my situation, but may not be a one-size-fits-all
|
||||
solution. Depending on how the database is deployed, the size of the tables, and
|
||||
usage, this approach may not scale. Always do a dry-run of database migrations
|
||||
like this.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Output Query Result In Nicely Formatted Table
|
||||
|
||||
When I start a fresh SQLite connection and run a query, all the results are
|
||||
squished together in a way that is poorly formatted, hard to read, and missing
|
||||
column headers.
|
||||
|
||||
```sql
|
||||
sqlite> select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3;
|
||||
1|2026-07-26T21:15:50.062936+00:00|2026-07-26T21:53:13.990Z|taco
|
||||
2|2026-07-26T21:53:40.019946+00:00|2026-07-26T22:14:09.168Z|TIL
|
||||
3|2026-08-02T17:00:16.119169+00:00|2026-08-02T17:30:16.119Z|py-vmt
|
||||
```
|
||||
|
||||
I can drastically improve the look of this by turning _headers_ on and switching
|
||||
to _box_ mode.
|
||||
|
||||
```sql
|
||||
sqlite> .headers on
|
||||
sqlite> .mode box
|
||||
sqlite> select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3;
|
||||
┌────┬──────────────────────────────────┬──────────────────────────┬────────┐
|
||||
│ id │ start_time │ end_time │ name │
|
||||
├────┼──────────────────────────────────┼──────────────────────────┼────────┤
|
||||
│ 1 │ 2026-07-26T21:15:50.062936+00:00 │ 2026-07-26T21:53:13.990Z │ taco │
|
||||
│ 2 │ 2026-07-26T21:53:40.019946+00:00 │ 2026-07-26T22:14:09.168Z │ TIL │
|
||||
│ 3 │ 2026-08-02T17:00:16.119169+00:00 │ 2026-08-02T17:30:16.119Z │ py-vmt │
|
||||
└────┴──────────────────────────────────┴──────────────────────────┴────────┘
|
||||
```
|
||||
|
||||
I personally find that much easier on the eyes. It is also a nicer format to
|
||||
copy and paste into a post like this or a formatted code block that I'm sharing
|
||||
with a colleague.
|
||||
|
||||
I can also do this directly from the CLI with a one-liner using `-header` and
|
||||
`-box` like so:
|
||||
|
||||
```bash
|
||||
❯ sqlite3 /Users/lastword/.local/share/vmt/sessions.db -header -box "select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3"
|
||||
┌────┬──────────────────────────────────┬──────────────────────────┬────────┐
|
||||
│ id │ start_time │ end_time │ name │
|
||||
├────┼──────────────────────────────────┼──────────────────────────┼────────┤
|
||||
│ 1 │ 2026-07-26T21:15:50.062936+00:00 │ 2026-07-26T21:53:13.990Z │ taco │
|
||||
│ 2 │ 2026-07-26T21:53:40.019946+00:00 │ 2026-07-26T22:14:09.168Z │ TIL │
|
||||
│ 3 │ 2026-08-02T17:00:16.119169+00:00 │ 2026-08-02T17:30:16.119Z │ py-vmt │
|
||||
└────┴──────────────────────────────────┴──────────────────────────┴────────┘
|
||||
```
|
||||
|
||||
Run the `.help` dot-command from a SQLite prompt for a full listing of these
|
||||
commands. See also `sqlite3 --help` from the CLI for usage details about all
|
||||
flags.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Connect To Individual Overmind Processes Via tmux
|
||||
|
||||
A common `Procfile.dev` in a Rails project might look something like this.
|
||||
|
||||
```
|
||||
web: bin/rails server -p $PORT
|
||||
vite: bin/vite dev
|
||||
worker: bundle exec good_job start
|
||||
```
|
||||
|
||||
Instead of starting up each process that needs to be running for development to
|
||||
work, I can instead run a tool that reads the procfile and sets it all up for me
|
||||
-- like `overmind`.
|
||||
|
||||
```bash
|
||||
❯ overmind start -f Procfile.dev
|
||||
```
|
||||
|
||||
What's cool about `overmind` is that it starts its own `tmux` session and then
|
||||
runs each of these processes in its own window.
|
||||
|
||||
I can connect to any one of them by name with `overmind connect <name>`. Or I
|
||||
can connect to the session defaulting to the first window with just `overmind
|
||||
connect`.
|
||||
|
||||
If I need to see what is going on with my background jobs, I'll run:
|
||||
|
||||
```bash
|
||||
❯ overmind connect worker
|
||||
```
|
||||
|
||||
This behaves like any other tmux session, so I can use my prefix key (`ctrl-z`
|
||||
in my case) to access tmux-specific keybindings. Most notably, once I'm done
|
||||
looking, I'll want to hit `ctrl-z d` to detach from the session.
|
||||
|
||||
I'm already using tmux as my daily driver which means its easy for me to end up
|
||||
in a nested tmux session if I connect while already in my development session.
|
||||
To help with that, I set up [a forwarding
|
||||
prefix](set-up-forwarding-prefix-for-nested-session.md).
|
||||
@@ -0,0 +1,31 @@
|
||||
# Check What Is Inside A Zip File
|
||||
|
||||
Before unzipping a file, it can be useful to check the contents. You can learn a
|
||||
few things from doing this. First off, are all the files nested under some extra
|
||||
top-level directory? How many files are in there? How big are certain files? Etc.
|
||||
|
||||
The `-l` flag to `unzip` can help answer these questions.
|
||||
|
||||
```bash
|
||||
❯ unzip -l zipped-files.zip
|
||||
Archive: zipped-files.zip
|
||||
Length Date Time Name
|
||||
--------- ---------- ----- ----
|
||||
0 08-22-2026 08:54 files-to-zip/
|
||||
14 08-22-2026 08:54 files-to-zip/data.json
|
||||
38 08-22-2026 08:53 files-to-zip/notes.md
|
||||
14 08-22-2026 08:53 files-to-zip/hello.txt
|
||||
--------- -------
|
||||
66 4 files
|
||||
```
|
||||
|
||||
Notice that the first line is for `files-to-zip/` which tells us that everything
|
||||
is nested under the top-level directory that was probably the target of the
|
||||
`zip` command. Everything is organized into a table with labeled columns which
|
||||
allows us to answer our questions and get a good idea of what is in there.
|
||||
|
||||
In my experience, zip files tend to have dozens to hundreds of files, so it is
|
||||
even better to pipe the above to a pager like `less`. That way you can search
|
||||
and navigate through everything that's in there.
|
||||
|
||||
See `man unzip` for more details.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Remove Pages From A PDF
|
||||
|
||||
I have a two page PDF. I really only need to second page though. I want to send
|
||||
that second page to someone and including the first page with it will only cause
|
||||
confusion. I couldn't figure out a sane way to do this on MacOS with either
|
||||
Preview or Notes (where the PDF originated).
|
||||
|
||||
So, I looked into a tool I'd heard of recently for manipulating PDFs called
|
||||
[`qpdf`](https://github.com/qpdf/qpdf).
|
||||
|
||||
> qpdf is a command-line tool and C++ library that performs content-preserving
|
||||
> transformations on PDF files.
|
||||
|
||||
I installed it with `homebrew` and then looked up how "removing" pages works.
|
||||
Conceptually, `qpdf` is used to transform a PDF. That means there are a couple
|
||||
ways I can think about approaching this.
|
||||
|
||||
If I want to preserve every page except for the first page, then I produce a PDF
|
||||
that starts at page `2` and goes to the end (`z`) by specifying a range of
|
||||
`2-z`.
|
||||
|
||||
```bash
|
||||
qpdf in.pdf --pages . 2-z -- out.pdf
|
||||
```
|
||||
|
||||
If I truly only want the 2nd page from the PDF and nothing else, then I can
|
||||
produce a PDF made up of _only_ page `2`.
|
||||
|
||||
```bash
|
||||
qpdf in.pdf --pages . 2 -- out.pdf
|
||||
```
|
||||
|
||||
Yet another way to think about this is to produce a PDF that includes all pages,
|
||||
but then explicitly mark page `1` for exclusion after the range with `x1`.
|
||||
|
||||
```bash
|
||||
qpdf in.pdf --pages . 1-z,x1 -- out.pdf
|
||||
```
|
||||
|
||||
And one last way, assuming that I am dealing specifically with a two-page PDF,
|
||||
is to use the `rN` syntax where `N` is the number of pages from the back of the
|
||||
list. `r1` is the last page, `r2` is the second to last page, and so forth. In a
|
||||
two-page PDF, `r2` would then refer to the _first_page.
|
||||
|
||||
```bash
|
||||
qpdf in.pdf --pages . 1-z,xr2 -- out.pdf
|
||||
```
|
||||
|
||||
I know I said that was the last, but I'll also throw in that, again assuming a
|
||||
two-page PDF, I could produce a PDF made up of only `:even` numbered pages
|
||||
relative to the defined range.
|
||||
|
||||
```bash
|
||||
qpdf in.pdf --pages . 1-z:even -- out.pdf
|
||||
```
|
||||
|
||||
I'll stick with the first approach, but it was fun to experiment with all the
|
||||
other ways this can be done with `qpdf`'s flexible syntax.
|
||||
Reference in New Issue
Block a user