keymap-align 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {keymap_align-0.2.0 → keymap_align-0.2.1}/.github/actionlint.yaml +3 -3
  2. {keymap_align-0.2.0 → keymap_align-0.2.1}/.github/workflows/validate.yml +48 -12
  3. keymap_align-0.2.1/.markdownlintignore +4 -0
  4. {keymap_align-0.2.0 → keymap_align-0.2.1}/.pre-commit-config.yaml +33 -10
  5. {keymap_align-0.2.0 → keymap_align-0.2.1}/CLAUDE.md +4 -4
  6. {keymap_align-0.2.0 → keymap_align-0.2.1}/PKG-INFO +1 -1
  7. {keymap_align-0.2.0 → keymap_align-0.2.1}/pyproject.toml +13 -14
  8. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/align.py +2 -2
  9. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/cli.py +25 -2
  10. {keymap_align-0.2.0 → keymap_align-0.2.1}/uv.lock +20 -20
  11. keymap_align-0.2.0/.markdownlintignore +0 -4
  12. {keymap_align-0.2.0 → keymap_align-0.2.1}/.editorconfig +0 -0
  13. {keymap_align-0.2.0 → keymap_align-0.2.1}/.github/workflows/release.yml +0 -0
  14. {keymap_align-0.2.0 → keymap_align-0.2.1}/.gitignore +0 -0
  15. {keymap_align-0.2.0 → keymap_align-0.2.1}/.markdownlint.yaml +0 -0
  16. {keymap_align-0.2.0 → keymap_align-0.2.1}/.python-version +0 -0
  17. {keymap_align-0.2.0 → keymap_align-0.2.1}/.shellcheckrc +0 -0
  18. {keymap_align-0.2.0 → keymap_align-0.2.1}/LICENSE +0 -0
  19. {keymap_align-0.2.0 → keymap_align-0.2.1}/README.md +0 -0
  20. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/__init__.py +0 -0
  21. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/config.py +0 -0
  22. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/layout_resolver.py +0 -0
  23. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/layouts/__init__.py +0 -0
  24. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/layouts/corne42.json +0 -0
  25. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/layouts/glove80.json +0 -0
  26. {keymap_align-0.2.0 → keymap_align-0.2.1}/src/keymap_align/layouts/piantor.json +0 -0
  27. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/layouts/corne_6key_simple_layout.json +0 -0
  28. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/layouts/glove80_80key_layout.json +0 -0
  29. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/simple_tests/corne_6key_simple_test.keymap +0 -0
  30. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_align_keymap.py +0 -0
  31. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_config.py +0 -0
  32. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_display_name_preservation.py +0 -0
  33. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_keymaps/correct/corne_6key_reference_comments_and_properties.keymap +0 -0
  34. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_keymaps/correct/glove80_reference_properly_aligned.keymap +0 -0
  35. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_keymaps/correct/glove80_reference_reverse_key_order.keymap +0 -0
  36. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_keymaps/misaligned/corne_6key_input_comments_and_properties.keymap +0 -0
  37. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_keymaps/misaligned/glove80_input_badly_aligned.keymap +0 -0
  38. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_keymaps/misaligned/glove80_input_cramped_no_spacing.keymap +0 -0
  39. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_keymaps/misaligned/glove80_input_excess_bindings.keymap +0 -0
  40. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_keymaps/misaligned/glove80_input_insufficient_bindings.keymap +0 -0
  41. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_layouts.py +0 -0
  42. {keymap_align-0.2.0 → keymap_align-0.2.1}/tests/test_update.py +0 -0
@@ -1,8 +1,8 @@
1
- # forge-toolchain: 46
2
- # Labels actionlint cannot discover. Its list of GitHub's hosted images is the
1
+ # forge-toolchain: 56
2
+ # Labels jactionlint cannot discover. Its list of GitHub's hosted images is the
3
3
  # one its release shipped with, and a self-hosted runner's labels are on no
4
4
  # list. Without this every runs-on naming one is reported as a typo and the
5
- # actionlint hook fails.
5
+ # jactionlint hook fails.
6
6
  self-hosted-runner:
7
7
  labels:
8
8
  - ubuntu-26.04
@@ -1,15 +1,18 @@
1
- # forge-toolchain: 46
1
+ # forge-toolchain: 56
2
2
  name: CI
3
3
 
4
4
  on:
5
- push:
6
- branches: [main]
7
5
  pull_request:
8
6
  workflow_call:
9
7
 
10
8
  permissions:
11
9
  contents: read
12
10
 
11
+ # uv runs on this Python rather than whatever the runner happens to
12
+ # have. It is the highest requires-python floor among the components.
13
+ env:
14
+ UV_PYTHON: "3.13"
15
+
13
16
  jobs:
14
17
 
15
18
  python:
@@ -56,8 +59,8 @@ jobs:
56
59
  - uses: actions/checkout@v7
57
60
 
58
61
  # generated:hooks
59
- # Every hook a standard block put in the repo's committed config, which
60
- # forge lists below. A push from a checkout that never ran `pre-commit
62
+ # Every hook in the repo's committed pre-commit config. A push from a
63
+ # checkout that never ran `pre-commit
61
64
  # install`, or a merge in the web UI, passes every hook by not running it,
62
65
  # and this is the gate that catches both. CI runs the hooks themselves
63
66
  # rather than copies of their checks, because a copy is a second spelling
@@ -66,6 +69,14 @@ jobs:
66
69
  with:
67
70
  version: "0.12.24"
68
71
 
72
+ # pre-commit takes a `language: node` hook's Node from PATH where node and
73
+ # npm are there, and otherwise downloads the newest release. That release
74
+ # can need libraries a runner image lacks, as Node 25 needs libatomic. So
75
+ # the job puts the declared Node on PATH, on every runner alike.
76
+ - uses: actions/setup-node@v7
77
+ with:
78
+ node-version: "24"
79
+
69
80
  # A self-hosted runner keeps ~/.cache/pre-commit between jobs, so there
70
81
  # the archive is a download of what is already on disk.
71
82
  - uses: actions/cache@v6
@@ -92,8 +103,8 @@ jobs:
92
103
  # name that path literally, and `load` resolves nothing, so BATS_LIB_PATH
93
104
  # cannot move the libraries anywhere cheaper.
94
105
  #
95
- # On the self-hosted pool that $HOME outlives the job and is shared by
96
- # every repo's runner, so concurrent jobs meet at ~/.local/lib. Each
106
+ # On a self-hosted runner, $HOME can outlive the job and be shared by
107
+ # several repos' runners, so concurrent jobs meet at ~/.local/lib. Each
97
108
  # release is cloned once, into a directory named for its version, and
98
109
  # nothing that has been loaded from is ever removed. The clone is staged
99
110
  # beside its target and renamed into place, so a reader never sees a
@@ -106,19 +117,26 @@ jobs:
106
117
  # The hook runs tests/*.bats, flat. Recursing would sweep an integration
107
118
  # tier in beside the unit one, and only the repo knows which of its tiers
108
119
  # is runner-safe. A nested layout is named in a notice rather than passed
109
- # over in silence, which is how one went unnoticed before.
120
+ # over in silence.
110
121
  #
111
122
  # jq is installed ahead of the runner's own, because the scripts under
112
123
  # test evaluate jq programs and jq's grammar moves between releases. 1.8
113
124
  # accepts an object value piped into `//` without parentheses, and 1.7
114
- # rejects the whole program. The version is the release the development
115
- # machines run, so a program parses here exactly when it parses there.
125
+ # rejects the whole program. The version is pinned to the jq developers
126
+ # run locally, so a program parses here exactly when it parses there.
127
+ #
128
+ # yq and rg are installed for the same scripts, which read YAML and
129
+ # search with them. A self-hosted runner image ships neither, so every
130
+ # test reaching one exits 127. Each is pinned to the release developers
131
+ # run, as jq is.
116
132
  - name: Set up bats
117
133
  run: |
118
134
  bats_version="1.14.0"
119
135
  bats_support_version="0.3.0"
120
136
  bats_assert_version="2.2.4"
121
137
  jq_version="1.8.2"
138
+ yq_version="4.54.1"
139
+ rg_version="15.2.0"
122
140
 
123
141
  if ! compgen -G "tests/*.bats" > /dev/null; then
124
142
  if [ -n "$(find tests -name '*.bats' -print -quit 2>/dev/null)" ]; then
@@ -154,6 +172,20 @@ jobs:
154
172
  echo "$RUNNER_TEMP/jq" >> "$GITHUB_PATH"
155
173
  "$RUNNER_TEMP/jq/jq" --version
156
174
 
175
+ mkdir -p "$RUNNER_TEMP/yq"
176
+ curl -fsSL --retry 3 -o "$RUNNER_TEMP/yq/yq" \
177
+ "https://github.com/mikefarah/yq/releases/download/v${yq_version}/yq_linux_amd64"
178
+ chmod +x "$RUNNER_TEMP/yq/yq"
179
+ echo "$RUNNER_TEMP/yq" >> "$GITHUB_PATH"
180
+ "$RUNNER_TEMP/yq/yq" --version
181
+
182
+ rg_release="ripgrep-${rg_version}-x86_64-unknown-linux-musl"
183
+ mkdir -p "$RUNNER_TEMP/rg"
184
+ curl -fsSL --retry 3 "https://github.com/BurntSushi/ripgrep/releases/download/${rg_version}/${rg_release}.tar.gz" |
185
+ tar -xzf - -C "$RUNNER_TEMP/rg" --strip-components=1 "${rg_release}/rg"
186
+ echo "$RUNNER_TEMP/rg" >> "$GITHUB_PATH"
187
+ "$RUNNER_TEMP/rg/rg" --version
188
+
157
189
  # Over what the push or pull request changed, which is what the hooks
158
190
  # would have seen at commit time. --all-files would fail a push on every
159
191
  # finding in a file it never touched, so it is the scope only where no
@@ -238,12 +270,16 @@ jobs:
238
270
  ruff-format
239
271
  ruff-check
240
272
  mypy
241
- actionlint
273
+ jactionlint
242
274
  )
243
275
  # Every hook runs whatever the one before it found, so one push
244
276
  # reports every failing hook rather than the first.
277
+ #
278
+ # pre-commit builds each `language: python` hook on the interpreter
279
+ # running it. Left to uv, that is whatever Python the runner ships,
280
+ # so pre-commit runs on the declared floor instead.
245
281
  status=0
246
282
  for hook in "${hooks[@]}"; do
247
- uvx pre-commit run "$hook" "${scope[@]}" --show-diff-on-failure || status=1
283
+ uvx --python 3.13 pre-commit run "$hook" "${scope[@]}" --show-diff-on-failure || status=1
248
284
  done
249
285
  exit "$status"
@@ -0,0 +1,4 @@
1
+ # No release here writes a changelog. If a generated one arrives, it is never
2
+ # linted: a release would undo any normalization and it would return as a
3
+ # conflict.
4
+ CHANGELOG.md
@@ -1,4 +1,4 @@
1
- # forge-toolchain: 46
1
+ # forge-toolchain: 56
2
2
  fail_fast: true
3
3
  default_stages: [pre-commit]
4
4
  repos:
@@ -62,8 +62,12 @@ repos:
62
62
  # worse than either erroring. identify already tags these files zsh,
63
63
  # so this is the native exclusion rather than a path regex that would have to
64
64
  # enumerate .zshrc, .zprofile, .zlogin and the rest.
65
- - repo: https://github.com/koalaman/shellcheck-precommit
66
- rev: v0.10.0
65
+ #
66
+ # shellcheck-py installs shellcheck's own release binary through pip and
67
+ # checks its sha256, so the hook needs no Docker daemon and no system
68
+ # package. Its tag is the shellcheck release it wraps, plus a packaging digit.
69
+ - repo: https://github.com/shellcheck-py/shellcheck-py
70
+ rev: v0.10.0.1
67
71
  hooks:
68
72
  - id: shellcheck
69
73
  exclude_types: [zsh]
@@ -94,11 +98,19 @@ repos:
94
98
  # from a linked worktree exports GIT_DIR and GIT_INDEX_FILE into each hook,
95
99
  # and GIT_DIR outranks `git -C`, so a test running `git config` in its own
96
100
  # scratch repo writes into the repository being committed instead.
101
+ #
102
+ # The suite runs one job per online core. Most of a bats test's cost is the
103
+ # framework re-sourcing its file and loading the helper libraries for every
104
+ # test, which no change to the tests removes, so wall time falls roughly by
105
+ # the core count. --jobs needs GNU parallel, and where it is not on PATH the
106
+ # suite runs serially rather than failing. getconf rather than nproc, which
107
+ # macOS lacks. A suite that cannot share a file across jobs sets
108
+ # BATS_NO_PARALLELIZE_WITHIN_FILE=true in its setup_file.
97
109
  - repo: local
98
110
  hooks:
99
111
  - id: bats
100
112
  name: bats
101
- entry: bash -c 'unset $(git rev-parse --local-env-vars); compgen -G "tests/*.bats" > /dev/null || exit 0; exec bats tests/'
113
+ entry: bash -c 'unset $(git rev-parse --local-env-vars); compgen -G "tests/*.bats" > /dev/null || exit 0; command -v parallel > /dev/null || exec bats tests/; exec bats --jobs "$(getconf _NPROCESSORS_ONLN)" tests/'
102
114
  language: system
103
115
  pass_filenames: false
104
116
 
@@ -157,7 +169,12 @@ repos:
157
169
  # nobody may respell — which is why the class is excluded rather than any
158
170
  # one repo's data. `.tsv` is named beside `.csv` as the same format with
159
171
  # a different delimiter.
160
- exclude: '(^|/)(.*-lock\.json|.*\.lock|uv\.lock|package-lock\.json|go\.sum|go\.mod|secrets?[^/]*\.(ya?ml|env|json)|.*\.enc\.[^/]+|.*\.[ct]sv)$'
172
+ #
173
+ # `gotk-components.yaml` is `flux install --export`, under the name
174
+ # `flux bootstrap` gives it. Its CRD descriptions are Flux's prose, and
175
+ # the file says DO NOT EDIT, so a hit there is fixed by no edit: the next
176
+ # export writes it back, as `go mod tidy` writes back a module path.
177
+ exclude: '(^|/)(.*-lock\.json|.*\.lock|uv\.lock|package-lock\.json|go\.sum|go\.mod|gotk-components\.yaml|secrets?[^/]*\.(ya?ml|env|json)|.*\.enc\.[^/]+|.*\.[ct]sv)$'
161
178
 
162
179
  # generated:refcheck - Reference checking
163
180
  #
@@ -192,14 +209,20 @@ repos:
192
209
  - id: uv-lock
193
210
 
194
211
  # Python: formatting
212
+ #
213
+ # types_or leaves out markdown, which ruff-pre-commit's ruff-format selects.
214
+ # ruff formats a Python fence in Markdown as source, and a fence here is often
215
+ # a figure: aligned comments and right-aligned paths, which the reflow
216
+ # destroys.
195
217
  - repo: https://github.com/astral-sh/ruff-pre-commit
196
- rev: v0.12.5
218
+ rev: v0.17.1
197
219
  hooks:
198
220
  - id: ruff-format
221
+ types_or: [python, pyi, jupyter]
199
222
 
200
223
  # generated:python-lint - Python: linting
201
224
  - repo: https://github.com/astral-sh/ruff-pre-commit
202
- rev: v0.12.5
225
+ rev: v0.17.1
203
226
  hooks:
204
227
  - id: ruff-check
205
228
  args: [--fix]
@@ -230,8 +253,8 @@ repos:
230
253
  pass_filenames: false
231
254
 
232
255
  # generated:github-actions - GitHub Actions
233
- - repo: https://github.com/rhysd/actionlint
234
- rev: v1.7.7
256
+ - repo: https://github.com/jdx/jactionlint
257
+ rev: v1.8.2
235
258
  hooks:
236
- - id: actionlint
259
+ - id: jactionlint
237
260
  args: ["-color", "-shellcheck="]
@@ -19,15 +19,15 @@ holds each of those beside the layers.
19
19
 
20
20
  Before anything is written, each array's whitespace-separated tokens are compared with those of
21
21
  its realigned rows. A token the rows would not reproduce is an error, and the run writes nothing.
22
- That catches a comment inside an array, an undefined macro name before the first `&`, and a parameter past a
23
- multi-parameter behavior's count. The run also refuses, and writes nothing, when:
22
+ That catches a comment inside an array, an undefined macro name before the first `&`, and a
23
+ parameter past a multi-parameter behavior's count. The run also refuses, and writes nothing, when:
24
24
 
25
25
  - a layer holds more bindings than the layout has keys;
26
26
  - two layer nodes share a name, because the layers are keyed by name;
27
27
  - a `bindings` property holds more than one `< >` array.
28
28
 
29
- A layer with too few bindings gets a blank cell for each one missing. `--debug` prints its grids
30
- and then runs the same checks, so it exits 1 wherever a real run would.
29
+ A layer with too few bindings gets a blank cell for each one missing. `--debug` prints the layout
30
+ and each layer's grid, then runs the same checks, so it exits 1 wherever a real run would.
31
31
 
32
32
  ## A binding ends where the next one starts, so a behavior-valued parameter must be declared
33
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: keymap-align
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: ZMK keymap alignment tool for custom mechanical keyboards
5
5
  Project-URL: Repository, https://github.com/datapointchris/keymap-align
6
6
  Author-email: Chris Birch <datapointchris@gmail.com>
@@ -29,7 +29,7 @@ keymap-align = "keymap_align.cli:app"
29
29
  dev = [
30
30
  "bandit>=1.7.8",
31
31
  "mypy>=1.10.0",
32
- "ruff==0.12.5",
32
+ "ruff==0.17.1",
33
33
  "pre-commit>=4.3.0",
34
34
  ]
35
35
  test = [
@@ -77,6 +77,7 @@ exclude = [
77
77
  "build",
78
78
  "dist",
79
79
  ]
80
+ extend-exclude = ["*.md"]
80
81
 
81
82
  [tool.ruff.format]
82
83
  quote-style = "single"
@@ -131,12 +132,23 @@ dataclasses = "dc"
131
132
  typeCheckingMode = "standard"
132
133
  analyzeUnannotatedFunctions = true # or pylance stops analyzing in vscode
133
134
 
135
+ [tool.hatch.version]
136
+ source = "uv-dynamic-versioning"
137
+
138
+ [tool.uv-dynamic-versioning]
139
+ style = "semver"
140
+
141
+ # A new commit or tag changes the version, so it has to invalidate uv's cache.
142
+ [tool.uv]
143
+ cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }]
144
+
134
145
  [tool.forge]
135
146
  # Keys the shared toolchain standard owns, written by its generator.
136
147
  # Dropping one from the template removes it here on the next sync; a key absent
137
148
  # from this list belongs to the project and is never touched. Do not hand-edit.
138
149
  managed = [
139
150
  ["ruff", "line-length"],
151
+ ["ruff", "extend-exclude"],
140
152
  ["ruff", "format", "quote-style"],
141
153
  ["ruff", "format", "indent-style"],
142
154
  ["ruff", "format", "skip-magic-trailing-comma"],
@@ -170,19 +182,6 @@ managed = [
170
182
  # and extra that names them, so a local run and CI cannot disagree.
171
183
  pinned = ["ruff"]
172
184
 
173
- # The release tag is the version. python-semantic-release finds no file to
174
- # stamp, so it tags the merge commit itself and writes no release commit.
175
- # uv_build cannot do this: its metadata has no dynamic version field.
176
- [tool.hatch.version]
177
- source = "uv-dynamic-versioning"
178
-
179
- [tool.uv-dynamic-versioning]
180
- style = "semver"
181
-
182
- # A new commit or tag changes the version, so it has to invalidate uv's cache.
183
- [tool.uv]
184
- cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }]
185
-
186
185
  [build-system]
187
186
  requires = ["hatchling", "uv-dynamic-versioning"]
188
187
  build-backend = "hatchling.build"
@@ -151,7 +151,6 @@ def handle_multi_param_behavior(tokens, i, binding_parts, binding_macros):
151
151
 
152
152
  def handle_standard_behavior(tokens, i, binding_parts, binding_macros):
153
153
  """Handle standard behaviors with simple parameters."""
154
- # Collect parameters until the next binding starts or the tokens run out
155
154
  while i < len(tokens) and not tokens[i].startswith('&') and tokens[i] not in binding_macros:
156
155
  binding_parts.append(tokens[i])
157
156
  i += 1
@@ -614,7 +613,8 @@ def align_keymap_with_layout(
614
613
  """Align keymap bindings according to keyboard layout.
615
614
 
616
615
  Only the inside of each layer's `bindings` array is rewritten. A keymap that cannot be
617
- rewritten without losing text is reported, and nothing is written.
616
+ rewritten without losing text gets each problem printed to stderr, nothing written, and a
617
+ return of False.
618
618
 
619
619
  Args:
620
620
  keymap_file: Path to the keymap file
@@ -54,6 +54,28 @@ Examples:
54
54
  [b]keymap-align format -k corne.keymap -o aligned.keymap[/b] — write elsewhere, leave the original
55
55
  """
56
56
 
57
+ LAYOUTS_EPILOG = """
58
+ Examples:
59
+
60
+ [b]keymap-align layouts list[/b] — the boards --layout accepts
61
+ """
62
+
63
+ LAYOUTS_LIST_EPILOG = """
64
+ Examples:
65
+
66
+ [b]keymap-align layouts list[/b] — every name --layout accepts, one per line
67
+
68
+ [b]keymap-align layouts list | grep -x corne42[/b] — whether a board ships with the tool, exit 0 if it does
69
+ """
70
+
71
+ UPDATE_EPILOG = """
72
+ Examples:
73
+
74
+ [b]keymap-align update --check[/b] — whether a newer release exists, without installing it
75
+
76
+ [b]keymap-align update[/b] — install the latest release
77
+ """
78
+
57
79
  # One config for `update` and the notice, so the notice never names a release `update` would not install.
58
80
  UPDATE_CONFIG = Config(tool='keymap-align', owner='datapointchris')
59
81
 
@@ -69,6 +91,7 @@ layouts_app = typer.Typer(
69
91
  name='layouts',
70
92
  no_args_is_help=True,
71
93
  help='The keyboard layouts that ship with the tool, each one a name `format --layout` accepts.',
94
+ epilog=LAYOUTS_EPILOG,
72
95
  )
73
96
  app.add_typer(layouts_app, name='layouts')
74
97
 
@@ -149,14 +172,14 @@ def format_keymap(
149
172
  raise typer.Exit(0 if success else 1)
150
173
 
151
174
 
152
- @layouts_app.command('list')
175
+ @layouts_app.command('list', epilog=LAYOUTS_LIST_EPILOG)
153
176
  def list_layouts() -> None:
154
177
  """Print the name of every bundled layout, one per line."""
155
178
  for name in get_bundled_layout_names():
156
179
  print(name)
157
180
 
158
181
 
159
- @app.command(rich_help_panel='Manage')
182
+ @app.command(rich_help_panel='Manage', epilog=UPDATE_EPILOG)
160
183
  def update(
161
184
  check_only: Annotated[bool, typer.Option('--check', help='Report whether an update is available without installing it.')] = False,
162
185
  ) -> None:
@@ -110,7 +110,7 @@ dev = [
110
110
  { name = "bandit", specifier = ">=1.7.8" },
111
111
  { name = "mypy", specifier = ">=1.10.0" },
112
112
  { name = "pre-commit", specifier = ">=4.3.0" },
113
- { name = "ruff", specifier = "==0.12.5" },
113
+ { name = "ruff", specifier = "==0.17.1" },
114
114
  ]
115
115
  test = [{ name = "pytest", specifier = ">=8.4.1" }]
116
116
 
@@ -363,27 +363,27 @@ wheels = [
363
363
 
364
364
  [[package]]
365
365
  name = "ruff"
366
- version = "0.12.5"
366
+ version = "0.17.1"
367
367
  source = { registry = "https://pypi.org/simple" }
368
- sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" }
368
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" }
369
369
  wheels = [
370
- { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" },
371
- { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" },
372
- { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" },
373
- { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" },
374
- { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" },
375
- { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" },
376
- { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" },
377
- { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" },
378
- { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" },
379
- { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" },
380
- { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" },
381
- { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" },
382
- { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" },
383
- { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" },
384
- { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" },
385
- { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" },
386
- { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" },
370
+ { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" },
371
+ { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" },
372
+ { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" },
373
+ { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" },
374
+ { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" },
375
+ { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" },
376
+ { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" },
377
+ { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" },
378
+ { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" },
379
+ { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" },
380
+ { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" },
381
+ { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" },
382
+ { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" },
383
+ { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" },
384
+ { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" },
385
+ { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" },
386
+ { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" },
387
387
  ]
388
388
 
389
389
  [[package]]
@@ -1,4 +0,0 @@
1
- # Preventive: no release here writes a changelog. The entry stands so that one
2
- # arriving generated is never linted, since normalization of a generated file
3
- # is undone on the next release and returns as a conflict.
4
- CHANGELOG.md
File without changes
File without changes
File without changes
File without changes
File without changes