okf-tui 1.0.0 → 1.1.0
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.
- checksums.yaml +4 -4
- data/.okf/capabilities/index.md +13 -0
- data/.okf/capabilities/okf-surface.md +48 -0
- data/.okf/capabilities/views.md +48 -0
- data/.okf/decisions/invents-no-analysis.md +8 -7
- data/.okf/decisions/no-version-ceilings.md +8 -9
- data/.okf/decisions/okf-capability-drift.md +16 -18
- data/.okf/decisions/one-door-the-plugin-seam.md +15 -14
- data/.okf/decisions/registry-write-boundary.md +43 -15
- data/.okf/decisions/ruby-floor.md +11 -9
- data/.okf/decisions/search-facade-coupling.md +22 -24
- data/.okf/decisions/undeclared-width-dependency.md +12 -10
- data/.okf/index.md +13 -5
- data/.okf/interaction/cross-bundle-scope.md +8 -8
- data/.okf/interaction/deferred-search.md +8 -6
- data/.okf/interaction/esc-peels-one-layer.md +10 -9
- data/.okf/interaction/filter-escalates-to-search.md +12 -11
- data/.okf/interaction/following-links.md +10 -8
- data/.okf/interaction/key-routing.md +6 -5
- data/.okf/interaction/which-registry.md +39 -19
- data/.okf/log.md +56 -0
- data/.okf/rendering/ansi-aware-width.md +8 -7
- data/.okf/rendering/fit-or-say-so.md +34 -0
- data/.okf/rendering/index.md +1 -0
- data/.okf/rendering/markdown-rendering-trap.md +8 -7
- data/.okf/rendering/status-vocabulary.md +6 -5
- data/.okf/rendering/whole-frame-painting.md +8 -6
- data/.okf/structure/doors.md +61 -0
- data/.okf/structure/index.md +16 -0
- data/.okf/structure/rendering.md +58 -0
- data/.okf/structure/the-app.md +55 -0
- data/.okf/structure/the-workspace.md +69 -0
- data/.okf/testing/adding-a-view.md +58 -0
- data/.okf/testing/ci-matrix.md +8 -6
- data/.okf/testing/headless-frames.md +6 -6
- data/.okf/testing/index.md +2 -0
- data/.okf/testing/pty-test.md +6 -5
- data/.okf/testing/the-suite.md +120 -0
- data/CHANGELOG.md +95 -1
- data/README.md +12 -3
- data/lib/okf/tui/app.rb +28 -7
- data/lib/okf/tui/cli.rb +2 -2
- data/lib/okf/tui/refs.rb +1 -1
- data/lib/okf/tui/version.rb +1 -1
- data/lib/okf/tui/views.rb +15 -0
- data/lib/okf/tui/workspace.rb +34 -9
- metadata +18 -7
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Playbook
|
|
3
|
+
title: Adding a View, a Key, or a Panel
|
|
4
|
+
description: The steps a new screen owes, in order — where the builder goes, which tables it joins, what the frame test must assert, and the catalogue entry the pin will demand.
|
|
5
|
+
tags: [testing, playbook, views]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Before you add a view, try not to
|
|
12
|
+
|
|
13
|
+
Six is the whole tab bar, and a seventh is a real cost on every screen. One view
|
|
14
|
+
has already been withdrawn for not earning its tab: the `index` map, because
|
|
15
|
+
browse answered the same question with a key the reader already had. Check
|
|
16
|
+
[the catalogue](/capabilities/views.md) first.
|
|
17
|
+
|
|
18
|
+
The same instinct applies one level down: a new *number* on an existing screen
|
|
19
|
+
usually belongs in okf, not here — [invents-no-analysis](/decisions/invents-no-analysis.md).
|
|
20
|
+
|
|
21
|
+
# The walk
|
|
22
|
+
|
|
23
|
+
1. **Write the failing test first**, in `test/integration/`, headless. It renders
|
|
24
|
+
a frame and asserts what is in it. Run it: it must fail for the reason you
|
|
25
|
+
predicted. And prove the assertion can fail at all — a frame test that passes
|
|
26
|
+
against the *old* code is asserting nothing. See
|
|
27
|
+
[headless-frames](/testing/headless-frames.md).
|
|
28
|
+
2. **Build rows, not output.** The builder goes in `views.rb` and returns an
|
|
29
|
+
array of strings; it may not touch the terminal. Anything that measures or
|
|
30
|
+
cuts a string goes through `ui.rb`, never `String#length` —
|
|
31
|
+
[ansi-aware-width](/rendering/ansi-aware-width.md).
|
|
32
|
+
3. **Join the tables in `app.rb`.** `TABS` (order matters — `help` stays last)
|
|
33
|
+
and `KEY_VIEWS`. Then decide the two behaviours: is it a
|
|
34
|
+
`SINGLE_BUNDLE_VIEW` (follows the active bundle) or scope-wide? Is it a
|
|
35
|
+
`CONTENT_VIEW` (a scrolling page) or a list? Is it a `FILTERABLE_VIEW`?
|
|
36
|
+
4. **Ask the reader, don't compute.** `Model` is memoized and already answers
|
|
37
|
+
most of it — [the-workspace](/structure/the-workspace.md) has the table. If
|
|
38
|
+
the answer isn't there, the question is probably okf's.
|
|
39
|
+
5. **A destructive action gets a `Prompt`.** Every registry write asks first,
|
|
40
|
+
and the write itself goes through `Workspace` and nowhere else —
|
|
41
|
+
[registry-write-boundary](/decisions/registry-write-boundary.md).
|
|
42
|
+
6. **Esc must peel exactly one layer.** A new mode adds a layer to that stack;
|
|
43
|
+
[esc-peels-one-layer](/interaction/esc-peels-one-layer.md) is the rule it has
|
|
44
|
+
to obey.
|
|
45
|
+
7. **Update the catalogue** — [capabilities/views](/capabilities/views.md). This
|
|
46
|
+
is not etiquette: `test/unit/bundle_catalog_test.rb` compares that table
|
|
47
|
+
against `App::TABS`, so the suite is red until you do. A new file under
|
|
48
|
+
`lib/` needs its line in [structure/](/structure/) for the same reason.
|
|
49
|
+
8. **Run the same test unedited**, then the geometry suite — it runs twice, at
|
|
50
|
+
two widths, and that is where a row that fits in ASCII and overflows in colour
|
|
51
|
+
shows up.
|
|
52
|
+
|
|
53
|
+
# What the suite structurally cannot catch
|
|
54
|
+
|
|
55
|
+
One pty test proves the binary boots and nothing else ([pty-test](/testing/pty-test.md)),
|
|
56
|
+
and a local run cannot see the dependency resolution that only the matrix
|
|
57
|
+
exercises ([ci-matrix](/testing/ci-matrix.md)). Neither is a gap to fill with
|
|
58
|
+
more headless tests; they are different questions.
|
data/.okf/testing/ci-matrix.md
CHANGED
|
@@ -3,7 +3,14 @@ type: Runbook
|
|
|
3
3
|
title: The CI Matrix and What Only It Catches
|
|
4
4
|
description: Ten Rubies on every push, what only the 2.4 container proves, and the resolution gap the matrix cannot see because every run resolves the sibling checkout.
|
|
5
5
|
tags: [testing, ruby-floor, dependencies]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-19
|
|
9
|
+
sources:
|
|
10
|
+
- title: "The repository's `.github/workflows/main.yml`, `okf-tui` job."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/.github/workflows/main.yml
|
|
12
|
+
- title: "`AGENTS.md` — the scripted `Gemfile.ci-check` run against the published okf."
|
|
13
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/AGENTS.md
|
|
7
14
|
---
|
|
8
15
|
|
|
9
16
|
# The matrix
|
|
@@ -73,8 +80,3 @@ Two details keep that reproduction honest:
|
|
|
73
80
|
something no user has. Drop the lockfile in the copy, exactly as the Docker
|
|
74
81
|
command above drops it for its own reason (a lockfile written by a modern
|
|
75
82
|
Bundler is one 2.4's cannot read).
|
|
76
|
-
|
|
77
|
-
# Citations
|
|
78
|
-
|
|
79
|
-
[1] The repository's `.github/workflows/main.yml`, `okf-tui` job.
|
|
80
|
-
[2] `AGENTS.md` — the scripted `Gemfile.ci-check` run against the published okf.
|
|
@@ -3,7 +3,12 @@ type: Playbook
|
|
|
3
3
|
title: Testing Frames Without a Terminal
|
|
4
4
|
description: How the suite drives real interactions and asserts on real frames with no pty — and the discipline of proving each check can actually fail.
|
|
5
5
|
tags: [testing, rendering]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-19
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`test/test_helper.rb` — `FixedScreen`, `with_registry`, `app_for`, `frame_for`, `keystrokes`."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/test_helper.rb
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
# The shape
|
|
@@ -67,8 +72,3 @@ Two things, each covered elsewhere because no headless frame can reach them:
|
|
|
67
72
|
[the pty test](/testing/pty-test.md);
|
|
68
73
|
- anything that depends on which okf actually resolves, since the Gemfile prefers
|
|
69
74
|
the sibling checkout locally — that is [the CI matrix](/testing/ci-matrix.md).
|
|
70
|
-
|
|
71
|
-
# Citations
|
|
72
|
-
|
|
73
|
-
[1] `test/test_helper.rb` — `FixedScreen`, `with_registry`, `app_for`,
|
|
74
|
-
`frame_for`, `keystrokes`.
|
data/.okf/testing/index.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
How a full-screen terminal app is proven, and what each layer can and cannot
|
|
4
4
|
catch.
|
|
5
5
|
|
|
6
|
+
* [The Suite — Its Layers, and the Two Fixtures That Exist to Bite](the-suite.md) - What each test file proves, and the two fixtures built to reach a branch nothing else could.
|
|
6
7
|
* [Testing Frames Without a Terminal](headless-frames.md) - Driving real interactions headlessly, and the discipline of proving a check can fail.
|
|
7
8
|
* [The One Test That Opens a Terminal](pty-test.md) - The single pty walk, and the three timing traps that made it flake on the floor.
|
|
8
9
|
* [The CI Matrix and What Only It Catches](ci-matrix.md) - Ten Rubies, the Docker floor check, and the dependency bug a local run cannot see.
|
|
10
|
+
* [Adding a View, a Key, or a Panel](adding-a-view.md) - The steps a new screen owes, and the catalogue entry the pin will demand.
|
data/.okf/testing/pty-test.md
CHANGED
|
@@ -3,7 +3,12 @@ type: Runbook
|
|
|
3
3
|
title: The One Test That Opens a Terminal
|
|
4
4
|
description: A single pty test boots `okf tui` in a real process and walks every view, and the three timing traps that made it flake on the Ruby floor.
|
|
5
5
|
tags: [testing, terminal, scar-tissue]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-18
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`test/integration/terminal_test.rb` — `SCRIPT`, `settle`, `reap`."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/terminal_test.rb
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
# Why it exists
|
|
@@ -67,7 +72,3 @@ the escape sequences before it do not.
|
|
|
67
72
|
`"\t\r"` in a single write can reach the reader as a *single* keypress. Split
|
|
68
73
|
control keys into separate steps — a race the test loses only sometimes is worse
|
|
69
74
|
than one it loses always.
|
|
70
|
-
|
|
71
|
-
# Citations
|
|
72
|
-
|
|
73
|
-
[1] `test/integration/terminal_test.rb` — `SCRIPT`, `settle`, `reap`.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: The Suite — Its Layers, and the Two Fixtures That Exist to Bite
|
|
4
|
+
description: "What each test file proves, why `nested` and `provenance` were built when twelve fixtures already existed, and the two gaps between this suite and the okf a user actually resolves."
|
|
5
|
+
tags: [testing, fixtures, structure]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Integration first
|
|
12
|
+
|
|
13
|
+
`test/integration/` is the critical layer: it drives the app the way a user does
|
|
14
|
+
— real keys, real frames, real exit codes. A unit test proves a method behaves;
|
|
15
|
+
an integration test proves the *product* behaves.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
test/
|
|
19
|
+
test_helper.rb OKF::TUI::TestCase: app_for, render, with_registry,
|
|
20
|
+
with_local_registry
|
|
21
|
+
fixtures/ bundles chosen for their standing, not their size
|
|
22
|
+
nested/ the only one whose directories nest — see below
|
|
23
|
+
provenance/ the only v0.2 one, §5 declared and withheld — below
|
|
24
|
+
integration/
|
|
25
|
+
geometry_test.rb every row is exactly the terminal width
|
|
26
|
+
browse_test.rb reading order, rendering, find-in-body
|
|
27
|
+
search_test.rb deferred search, focus, escalation, the held corpus
|
|
28
|
+
graph_test.rb facets, and following a concept out
|
|
29
|
+
dirs_test.rb okf's directory set, and the dir facet
|
|
30
|
+
structure_test.rb hubs and dir traffic, against okf's own numbers,
|
|
31
|
+
and health's two panes
|
|
32
|
+
groups_test.rb registry groups, and scoping a search to one
|
|
33
|
+
refs_test.rb @slug / @group, and which registry resolves them
|
|
34
|
+
provenance_test.rb §5 on screen, and what a v0.1 bundle is spared
|
|
35
|
+
signals_test.rb health colours, the tab flag, the filters
|
|
36
|
+
cli_test.rb argv, streams, exit codes — CLI.run driven straight
|
|
37
|
+
plugin_test.rb `okf tui` through okf's registry, and that the
|
|
38
|
+
dispatcher adds nothing but the streams
|
|
39
|
+
terminal_test.rb `okf tui` in a real process, through a real pty —
|
|
40
|
+
the only test that walks process boot + discovery
|
|
41
|
+
unit/
|
|
42
|
+
gemspec_test.rb the declared okf floor tracks the kernel next door
|
|
43
|
+
packaging_test.rb LICENSE.txt and NOTICE ship, real files, unchanged
|
|
44
|
+
bundle_catalog_test.rb this bundle against the code it describes
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The three unit tests are there because their claims are about the *package* and
|
|
48
|
+
the *documentation*, not the app — no integration test can reach them.
|
|
49
|
+
|
|
50
|
+
# The two fixtures built to reach a branch nothing else could
|
|
51
|
+
|
|
52
|
+
**`fixtures/nested` exists because every other fixture is one level deep**, and
|
|
53
|
+
at one level `dir` and `top_dir` are the same string — so an assertion against
|
|
54
|
+
them passes whichever field the code reads, which is exactly how the `area`
|
|
55
|
+
break survived. It carries a real tree, an intermediate directory holding no
|
|
56
|
+
concepts of its own (`platform/`), and a directory whose only file is a `log.md`
|
|
57
|
+
(`history/`) — the two shapes okf 1.13.0 had to fix its own directory set for.
|
|
58
|
+
Six directories against three top-level ones: reach for it for anything about
|
|
59
|
+
`dir`, depth, or traffic.
|
|
60
|
+
|
|
61
|
+
**`fixtures/provenance` is the only v0.2 bundle**, and it carries a concept that
|
|
62
|
+
declares no §5 family (`untouched.md`) beside four that do — deliberately, so
|
|
63
|
+
the suppression rule is a property of the *concept* rather than of the fixture,
|
|
64
|
+
and one test can assert both halves against one bundle. Its numbers are chosen
|
|
65
|
+
to bite: three of its four `unverified` concepts are claimable, so a trust facet
|
|
66
|
+
counting the whole tally would say 4 and narrow to 3. Reach for it for anything
|
|
67
|
+
about trust, status, `generated`, `stale_after` or `sources`; reach for a v0.1
|
|
68
|
+
fixture to prove the same surface stays *absent*.
|
|
69
|
+
|
|
70
|
+
Keep both conformant and lint-clean, so they stay usable by the health tests.
|
|
71
|
+
Provenance's one `info` is the legacy `timestamp:` on `untouched.md`, which is
|
|
72
|
+
the §13.1 lift under test.
|
|
73
|
+
|
|
74
|
+
# Run with colour on, not just off
|
|
75
|
+
|
|
76
|
+
Pastel disables colour when stdout is not a terminal, so a piped test exercises
|
|
77
|
+
none of the ANSI-aware width, clipping and wrapping code — the paths most likely
|
|
78
|
+
to be wrong are exactly the ones a naive capture cannot see. `geometry_test` runs
|
|
79
|
+
both modes; `browse_test` forces colour for the render sweep.
|
|
80
|
+
|
|
81
|
+
That is not thoroughness for its own sake: the `IndexError` in
|
|
82
|
+
[markdown-rendering-trap](/rendering/markdown-rendering-trap.md) rendered
|
|
83
|
+
perfectly in every uncoloured test.
|
|
84
|
+
|
|
85
|
+
# No suite here runs the okf a user gets
|
|
86
|
+
|
|
87
|
+
The `Gemfile` resolves okf from `../okf`, and in the monorepo that checkout is
|
|
88
|
+
always there — so the local run, CI, and the 2.4 container all exercise
|
|
89
|
+
*unreleased* okf. That is the right default: it lets a kernel change be driven
|
|
90
|
+
from the UI without a release. It also leaves a checkout-versus-RubyGems gap.
|
|
91
|
+
|
|
92
|
+
Two things stand in that gap, covering different halves.
|
|
93
|
+
`test/unit/gemspec_test.rb` is the standing one: it fails the moment okf bumps
|
|
94
|
+
and the gemspec floor does not follow. And a run against the *published* okf
|
|
95
|
+
catches the rest — a released kernel resolves different analysis output, which
|
|
96
|
+
is a difference no floor expresses.
|
|
97
|
+
|
|
98
|
+
The disagreement it catches is silent: okf's lint findings on
|
|
99
|
+
`fixtures/okf-docs` have changed between releases before, which is enough to
|
|
100
|
+
change how many rows the health view has. **A test whose premise depends on
|
|
101
|
+
okf's analysis output can pass here and fail there** — the health scroll tests
|
|
102
|
+
did exactly that, proving a pane overflowed by leaning on a lint count. Prove a
|
|
103
|
+
rendering property from geometry (a terminal too short to fit) and let the
|
|
104
|
+
agreement tests be the place okf's numbers are asserted.
|
|
105
|
+
|
|
106
|
+
# Two assertion traps this suite has already hit
|
|
107
|
+
|
|
108
|
+
* **A check that crashes tells you less than one that fails.** When an
|
|
109
|
+
assertion's subject can be nil because the thing under test broke, report that
|
|
110
|
+
and skip the dependents rather than raising `NoMethodError` from the middle.
|
|
111
|
+
* **Judge a rendered offset against the window the view actually used.** An
|
|
112
|
+
earlier version compared a scroll against a different window size and reported
|
|
113
|
+
a failure that was not one.
|
|
114
|
+
|
|
115
|
+
And one naming rule: **name things, do not count them.** The browse list holds
|
|
116
|
+
reserved files as well as concepts, so `<down><down><down>` is a guess about
|
|
117
|
+
ordering. `open_concept("overview")` is a statement about which concept is open.
|
|
118
|
+
|
|
119
|
+
For the 2.4 container, **read its output, not its exit status** — piping it
|
|
120
|
+
through `tail` returns `tail`'s status, which is zero however the run went.
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,99 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Linked bundles are marked read-only, and the config keys refuse before the
|
|
13
|
+
prompt.** okf's global registry can now `link` another registry file, and that
|
|
14
|
+
file's bundles and groups resolve into the bundles view. Browsing one is
|
|
15
|
+
browsing a bundle, so they list, load, scope and search like any other — but
|
|
16
|
+
the file that owns them is elsewhere and okf refuses every write against one.
|
|
17
|
+
|
|
18
|
+
`d`, `n`, `x` and `+` now say so *before* asking. Without this the ask went
|
|
19
|
+
through, okf raised, and the message reached the status line only after the
|
|
20
|
+
user had typed a new name or confirmed a removal. The bundle detail carries
|
|
21
|
+
`read-only — linked from @onm` and the owning file underneath; the registry
|
|
22
|
+
pane is left alone, because 42 columns cannot take another marker without
|
|
23
|
+
clipping.
|
|
24
|
+
|
|
25
|
+
Nothing else changed to show linked bundles at all: the entries come from
|
|
26
|
+
okf's `listing` and the groups from its `groups_listing`, both of which fold
|
|
27
|
+
the linked half in. `Entry#link` and `Group#link` are read straight off those
|
|
28
|
+
rows, and the suite asserts that agreement against okf rather than a fixture.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **The project-local registry is now `.okf.json`.** The TUI reads whichever file
|
|
33
|
+
okf resolves, so this needed no code change — but it is what the header prints
|
|
34
|
+
and what the "nothing to show" message names, so the file you see identified
|
|
35
|
+
there has a new name. The older `.okf-registry.json` is still discovered, both
|
|
36
|
+
names checked in each directory on the way up, so a repository carrying one
|
|
37
|
+
keeps working untouched. `okf registry init` writes the short name.
|
|
38
|
+
|
|
39
|
+
- **The `okf` floor moves to 2.2.0**, the kernel that ships `registry link`
|
|
40
|
+
(`Registry#links_listing`, and the `link` key on listing and group rows).
|
|
41
|
+
Against 2.1.1 those read as *nil* rather than raising, so a linked bundle would
|
|
42
|
+
simply never be marked read-only and the four config keys would stop refusing —
|
|
43
|
+
silent, which is the drift this gem's floor exists to turn into a resolution
|
|
44
|
+
failure.
|
|
45
|
+
|
|
46
|
+
## [1.0.1] - 2026-08-20
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **The package metadata follows the `gems/` move and the repository rename.**
|
|
51
|
+
Two URLs on the package page were about to be wrong at once. `changelog_uri`
|
|
52
|
+
named `blob/main/okf-tui/CHANGELOG.md`, a path that stopped existing when the
|
|
53
|
+
four gems moved under `gems/`; `homepage` named `serradura/okf-gem`, and the
|
|
54
|
+
repository is now **`serradura/okf`** — the name the command has had all
|
|
55
|
+
along. rubygems.org serves whatever the last release published, so neither
|
|
56
|
+
corrects itself: only a release republishes metadata, and this is that
|
|
57
|
+
release. Both land in one round rather than two, which is the whole reason
|
|
58
|
+
the rename waited for the paths to stop moving.
|
|
59
|
+
|
|
60
|
+
`homepage_uri`, `source_code_uri` and `changelog_uri` are all derived from
|
|
61
|
+
`homepage`, so one line moves four pieces of this gem's metadata. GitHub
|
|
62
|
+
redirects the old URLs permanently, so anything already published keeps
|
|
63
|
+
resolving.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **The gem's own bundle is now OKF v0.2.** It was the last one in the tree on
|
|
68
|
+
v0.1. Its 30 concepts moved `timestamp:` under `generated: { by, at }`, and
|
|
69
|
+
the 22 carrying a body `# Citations` list moved that provenance into
|
|
70
|
+
`sources:` — every entry keeping its text, with a GitHub URL where it named a
|
|
71
|
+
real file. The three concepts using positional `[1]` markers were rekeyed to
|
|
72
|
+
`[^1]` footnotes, which is what §5.1 asks for. `okf validate` and `okf lint`
|
|
73
|
+
are both clean on it.
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
- **The bundle carries the structure and the catalogue, and a test pins them.**
|
|
78
|
+
`.okf/structure/` names every file under `lib/`, grouped by the layer that
|
|
79
|
+
owns it; `.okf/capabilities/` is the catalogue of the six views and the kernel
|
|
80
|
+
surface behind them; `.okf/testing/the-suite.md` is what each test file proves
|
|
81
|
+
and why the two special fixtures exist; `.okf/testing/adding-a-view.md` is the
|
|
82
|
+
walk a new screen owes. `test/unit/bundle_catalog_test.rb` fails when a file
|
|
83
|
+
under `lib/` is named by no concept, when a concept names a file that is gone,
|
|
84
|
+
or when the view catalogue and `App::TABS` disagree. `AGENTS.md` kept the
|
|
85
|
+
contracts and the commands and now routes here for the rest — its
|
|
86
|
+
hand-maintained Map of `lib/**` was the thing nothing checked.
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
- **Three `AGENTS.md` paths pointed at directories that no longer exist** —
|
|
91
|
+
`cd /build/okf-tui`, `cd okf-tui`, and `working-directory: okf-tui`, all from
|
|
92
|
+
before the gems moved under `gems/`.
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
|
|
96
|
+
- **The okf floor moves to `>= 2.1.1, < 3`** — the kernel this gem develops
|
|
97
|
+
against released 2.1.1, and the floor tracks what the suite proves against
|
|
98
|
+
(the gemspec drill enforces equality as the normal state). Nothing here calls
|
|
99
|
+
a 2.1-only surface; the ceiling is unchanged.
|
|
100
|
+
|
|
8
101
|
## [1.0.0] - 2026-08-15
|
|
9
102
|
|
|
10
103
|
First release.
|
|
@@ -236,4 +329,5 @@ First release.
|
|
|
236
329
|
turns the next one into a resolution failure a maintainer sees instead of a
|
|
237
330
|
wrong number a reader believes.
|
|
238
331
|
|
|
239
|
-
[1.0.
|
|
332
|
+
[1.0.1]: https://github.com/serradura/okf/compare/okf-tui/v1.0.0...okf-tui/v1.0.1
|
|
333
|
+
[1.0.0]: https://github.com/serradura/okf/releases/tag/okf-tui/v1.0.0
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# okf-tui
|
|
2
2
|
|
|
3
|
-
A full-screen terminal UI for [Open Knowledge Format](https://github.com/serradura/okf
|
|
3
|
+
A full-screen terminal UI for [Open Knowledge Format](https://github.com/serradura/okf)
|
|
4
4
|
bundles: read one, switch between many, configure the registry, and search
|
|
5
5
|
across all of them at once. Built on the [TTY toolkit](https://ttytoolkit.org/components/).
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ This gem ships **no executable of its own** — deliberately. A second binary th
|
|
|
21
21
|
only aliased `okf tui` would be one more name to install, document and keep
|
|
22
22
|
working, and two front ends is two argument grammars waiting to drift. `okf`
|
|
23
23
|
finds this gem because it ships `okf/plugin.rb`, the convention any gem can use
|
|
24
|
-
to add a verb — see [extension points](https://github.com/serradura/okf
|
|
24
|
+
to add a verb — see [extension points](https://github.com/serradura/okf/blob/main/.okf/design/extension-points.md).
|
|
25
25
|
|
|
26
26
|
The argument shape mirrors `okf server` exactly, because it is the same grammar:
|
|
27
27
|
`@slug` names a registered bundle, bare `@` the registry default, `@group` fans out
|
|
@@ -124,6 +124,10 @@ In the bundles view: `d` default, `a` add, `n` rename, `x` remove (asks first),
|
|
|
124
124
|
shows what the file now says. Removing a bundle never touches the bundle on
|
|
125
125
|
disk — the registry is a list of references.
|
|
126
126
|
|
|
127
|
+
A bundle that arrived through an `okf registry link` is read-only — the registry
|
|
128
|
+
that owns it is another file — so those keys say so instead of asking, and the
|
|
129
|
+
detail pane names the link and the file to edit.
|
|
130
|
+
|
|
127
131
|
It edits your *configuration*, never your knowledge: there is no bundle writing
|
|
128
132
|
anywhere in this gem. Authoring belongs to `okf` and its companion skill.
|
|
129
133
|
|
|
@@ -151,7 +155,7 @@ Anything that can lose configuration asks first and names what it is about to do
|
|
|
151
155
|
group goes with it. `+`, `c` and `d` add or reorder, and do not ask.
|
|
152
156
|
|
|
153
157
|
**Which registry.** The same one every other `okf` verb run from the same
|
|
154
|
-
directory resolves: a project-local `.okf
|
|
158
|
+
directory resolves: a project-local `.okf.json` when one is on the path
|
|
155
159
|
up from here, and `$OKF_HOME` (default `~/.okf`) otherwise. `OKF_NO_DISCOVERY=1`
|
|
156
160
|
forces the global one. The header names the file it read, so there is never a
|
|
157
161
|
question which is in force. A workspace of directories named on the command line
|
|
@@ -189,6 +193,11 @@ silently rather than loudly.
|
|
|
189
193
|
the verb resolves the same way it does for a user. See [AGENTS.md](AGENTS.md)
|
|
190
194
|
for the contracts a change has to keep.
|
|
191
195
|
|
|
196
|
+
This gem ships its own knowledge bundle at `.okf/`, and it is the gem's own
|
|
197
|
+
documentation rather than a sample: what every file under `lib/` does, what each
|
|
198
|
+
of the six views answers, and why the interaction model is what it is. `okf tui
|
|
199
|
+
.okf` reads it in this program, which is the shortest honest demo there is.
|
|
200
|
+
|
|
192
201
|
## License
|
|
193
202
|
|
|
194
203
|
Apache-2.0.
|
data/lib/okf/tui/app.rb
CHANGED
|
@@ -996,6 +996,10 @@ module OKF::TUI
|
|
|
996
996
|
return (@message = "no bundle here to add") if entry.nil?
|
|
997
997
|
return (@message = "no group to add it to — c names the bundles in scope as one") if group.nil?
|
|
998
998
|
return (@message = "@#{group.slug} already names @#{entry.slug}") if group.members.include?(entry.slug)
|
|
999
|
+
# Both halves: a linked bundle cannot be stored as a member (its name lives
|
|
1000
|
+
# only while its link resolves), and a linked group cannot be written to.
|
|
1001
|
+
return if refused_linked?(entry, "grouping")
|
|
1002
|
+
return if refused_linked?(group, "adding to")
|
|
999
1003
|
|
|
1000
1004
|
apply_group_edit_keeping_focus do
|
|
1001
1005
|
keeping_group_scoped(group.slug) { workspace.add_to_group(group.slug, [ entry.slug ]) }
|
|
@@ -1434,19 +1438,35 @@ module OKF::TUI
|
|
|
1434
1438
|
# cascades across every member list, and one `del` cascade-drops the slug and
|
|
1435
1439
|
# deletes any group it empties. Nothing here has to know that — it just has to
|
|
1436
1440
|
# pass the slug the cursor is on rather than assuming it is a bundle's.
|
|
1441
|
+
# A bundle or group an okf registry *link* brought in is read-only: the file
|
|
1442
|
+
# that owns it is another registry, and okf refuses every config write against
|
|
1443
|
+
# one. The refusal belongs here, before the prompt — letting the ask through
|
|
1444
|
+
# makes a user type a new name and confirm it before learning it could never
|
|
1445
|
+
# land, and this view's own rule is that a key which stops working says so.
|
|
1446
|
+
# True when it refused, so a caller reads as `return if refused_linked?(…)`.
|
|
1447
|
+
def refused_linked?(subject, action)
|
|
1448
|
+
return false if subject.nil? || !subject.linked?
|
|
1449
|
+
|
|
1450
|
+
@message = "#{action} @#{subject.slug} is read-only — it comes from the linked registry " \
|
|
1451
|
+
"@#{subject.link}, so edit it there"
|
|
1452
|
+
true
|
|
1453
|
+
end
|
|
1454
|
+
|
|
1437
1455
|
def ask_rename
|
|
1438
|
-
|
|
1439
|
-
return if
|
|
1456
|
+
subject = detailing_group? ? selected_group : selected_entry
|
|
1457
|
+
return if subject.nil?
|
|
1458
|
+
return if refused_linked?(subject, "renaming")
|
|
1440
1459
|
|
|
1441
|
-
ask(:rename, "rename @#{slug} to:", free_text: true, subject: slug)
|
|
1460
|
+
ask(:rename, "rename @#{subject.slug} to:", free_text: true, subject: subject.slug)
|
|
1442
1461
|
end
|
|
1443
1462
|
|
|
1444
1463
|
def ask_remove
|
|
1445
|
-
|
|
1446
|
-
return if
|
|
1464
|
+
subject = detailing_group? ? selected_group : selected_entry
|
|
1465
|
+
return if subject.nil?
|
|
1466
|
+
return if refused_linked?(subject, "removing")
|
|
1447
1467
|
|
|
1448
|
-
what = detailing_group? ? "group @#{slug}" : "@#{slug}"
|
|
1449
|
-
ask(:remove, "remove #{what} from the registry? (y/n)", free_text: false, subject: slug)
|
|
1468
|
+
what = detailing_group? ? "group @#{subject.slug}" : "@#{subject.slug}"
|
|
1469
|
+
ask(:remove, "remove #{what} from the registry? (y/n)", free_text: false, subject: subject.slug)
|
|
1450
1470
|
end
|
|
1451
1471
|
|
|
1452
1472
|
# The slug the focused pane has selected — a group's when the groups or member
|
|
@@ -1458,6 +1478,7 @@ module OKF::TUI
|
|
|
1458
1478
|
def set_default
|
|
1459
1479
|
entry = selected_entry
|
|
1460
1480
|
return if entry.nil?
|
|
1481
|
+
return if refused_linked?(entry, "defaulting to")
|
|
1461
1482
|
|
|
1462
1483
|
@message = workspace.make_default(entry.slug)
|
|
1463
1484
|
# Making a bundle the default moves it to the front, so the list reorders
|
data/lib/okf/tui/cli.rb
CHANGED
|
@@ -32,7 +32,7 @@ module OKF
|
|
|
32
32
|
-v, --version print the version
|
|
33
33
|
-h, --help print this message
|
|
34
34
|
|
|
35
|
-
The registry is the project-local .okf
|
|
35
|
+
The registry is the project-local .okf.json when one is on the
|
|
36
36
|
path up from here, and $OKF_HOME (default ~/.okf) otherwise — whichever
|
|
37
37
|
one every other `okf` verb run from here resolves to. OKF_NO_DISCOVERY=1
|
|
38
38
|
forces the global one.
|
|
@@ -87,7 +87,7 @@ module OKF
|
|
|
87
87
|
# `cwd: Dir.pwd` is the other half of that: it is what opts this run into
|
|
88
88
|
# registry discovery, mirroring okf's own rule that only its CLI passes a
|
|
89
89
|
# cwd while a library caller stays global-only. Without it the TUI would be
|
|
90
|
-
# the one okf verb that ignores a project-local `.okf
|
|
90
|
+
# the one okf verb that ignores a project-local `.okf.json` sitting
|
|
91
91
|
# right beside the bundles it is being asked about.
|
|
92
92
|
def start(refs)
|
|
93
93
|
return incompatible_okf("cannot answer a search — OKF::Bundle::Search is missing across/prepare/with") unless
|
data/lib/okf/tui/refs.rb
CHANGED
|
@@ -28,7 +28,7 @@ module OKF
|
|
|
28
28
|
# Resolution is also what opts the TUI into registry discovery: Command's
|
|
29
29
|
# `open_registry` is `OKF::Registry.load(cwd: Dir.pwd)`, so a `@slug` here
|
|
30
30
|
# means the same bundle it means to every other okf verb run from the same
|
|
31
|
-
# directory — the project-local `.okf
|
|
31
|
+
# directory — the project-local `.okf.json` when there is one on the
|
|
32
32
|
# path up, the global `$OKF_HOME` registry otherwise.
|
|
33
33
|
class Refs < OKF::CLI::Command
|
|
34
34
|
def self.id
|
data/lib/okf/tui/version.rb
CHANGED
data/lib/okf/tui/views.rb
CHANGED
|
@@ -439,6 +439,7 @@ module OKF::TUI
|
|
|
439
439
|
r.add(" default", :magenta) if entry.default?
|
|
440
440
|
end
|
|
441
441
|
rows << Ui.line(width) { |r| r.add(entry.dir.to_s, :bright_black) }
|
|
442
|
+
rows.concat(linked_rows(app, entry, width))
|
|
442
443
|
rows.concat(membership_rows(app, entry, width))
|
|
443
444
|
rows << Ui.blank_line(width)
|
|
444
445
|
|
|
@@ -500,6 +501,20 @@ module OKF::TUI
|
|
|
500
501
|
# can show one membership, and a bundle can have several. This is the line that
|
|
501
502
|
# answers "did that + land" when the group being edited is scrolled out of the
|
|
502
503
|
# groups pane.
|
|
504
|
+
# Where a linked bundle comes from, and that the config keys will not touch
|
|
505
|
+
# it. On the detail rather than the registry row: that pane is 42 columns and
|
|
506
|
+
# already carries slug, count and `default`, so a marker there would clip on a
|
|
507
|
+
# narrow terminal — and this is the pane where the other "what can I do with
|
|
508
|
+
# this one" lines already live.
|
|
509
|
+
def linked_rows(app, entry, width)
|
|
510
|
+
return [] unless entry.linked?
|
|
511
|
+
|
|
512
|
+
target = app.workspace.link_target(entry.link)
|
|
513
|
+
rows = [ Ui.line(width) { |r| r.add("read-only — linked from @#{entry.link}", :yellow) } ]
|
|
514
|
+
rows << Ui.line(width) { |r| r.add(target.to_s, :bright_black) } if target
|
|
515
|
+
rows
|
|
516
|
+
end
|
|
517
|
+
|
|
503
518
|
def membership_rows(app, entry, width)
|
|
504
519
|
groups = app.workspace.groups.select { |group| group.members.include?(entry.slug) }
|
|
505
520
|
return [] if groups.empty?
|
data/lib/okf/tui/workspace.rb
CHANGED
|
@@ -16,14 +16,22 @@ module OKF::TUI
|
|
|
16
16
|
# or unreadable, and `error` says which — so a row explains itself instead of
|
|
17
17
|
# silently vanishing from the list.
|
|
18
18
|
class Entry
|
|
19
|
-
attr_reader :slug, :dir
|
|
19
|
+
attr_reader :slug, :dir, :link
|
|
20
20
|
attr_accessor :model, :error
|
|
21
21
|
|
|
22
|
-
def initialize(slug:, dir:, default:, registered:)
|
|
22
|
+
def initialize(slug:, dir:, default:, registered:, link: nil)
|
|
23
23
|
@slug = slug
|
|
24
24
|
@dir = dir
|
|
25
25
|
@default = default
|
|
26
26
|
@registered = registered
|
|
27
|
+
@link = link
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Did this bundle arrive through an okf registry link? Browsing one is
|
|
31
|
+
# browsing a bundle, so it reads like any other — but the registry that
|
|
32
|
+
# owns it is another file, and okf refuses every config write against it.
|
|
33
|
+
def linked?
|
|
34
|
+
!@link.nil?
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
def default?
|
|
@@ -52,13 +60,21 @@ module OKF::TUI
|
|
|
52
60
|
# the members are what an edit would change, the leaves are what a search
|
|
53
61
|
# would cover, and for a nested group those are not the same list.
|
|
54
62
|
class Group
|
|
55
|
-
attr_reader :slug, :members, :bundles
|
|
63
|
+
attr_reader :slug, :members, :bundles, :link
|
|
56
64
|
|
|
57
|
-
def initialize(slug:, members:, bundles:, cyclic:)
|
|
65
|
+
def initialize(slug:, members:, bundles:, cyclic:, link: nil)
|
|
58
66
|
@slug = slug
|
|
59
67
|
@members = members
|
|
60
68
|
@bundles = bundles
|
|
61
69
|
@cyclic = cyclic
|
|
70
|
+
@link = link
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A group an okf registry link brought in — the link's own set, or one the
|
|
74
|
+
# linked file curates. Scopable and searchable like any other; writable by
|
|
75
|
+
# nobody here, since okf persists only the groups this registry owns.
|
|
76
|
+
def linked?
|
|
77
|
+
!@link.nil?
|
|
62
78
|
end
|
|
63
79
|
|
|
64
80
|
# A hand-edited registry can name a cycle, which okf reports by declining to
|
|
@@ -81,7 +97,7 @@ module OKF::TUI
|
|
|
81
97
|
# its CLI passes a cwd and a library caller stays on the global registry. A
|
|
82
98
|
# default here would make the registry an embedding app reads depend on the
|
|
83
99
|
# directory its process happens to be in, and would let the suite discover a
|
|
84
|
-
# `.okf
|
|
100
|
+
# `.okf.json` from wherever `rake` was run.
|
|
85
101
|
#
|
|
86
102
|
# +ref_slugs+ maps a resolved directory to the slug the @ref named it by, so a
|
|
87
103
|
# session built from refs keeps the names the user typed. See Refs#slugs.
|
|
@@ -108,6 +124,15 @@ module OKF::TUI
|
|
|
108
124
|
(registry || open_registry).path
|
|
109
125
|
end
|
|
110
126
|
|
|
127
|
+
# The registry file a link points at — what a refusal has to name, since
|
|
128
|
+
# "edit it there" is useless without saying where there is. nil when nothing
|
|
129
|
+
# is linked under that name.
|
|
130
|
+
def link_target(name)
|
|
131
|
+
return nil unless registry_backed?
|
|
132
|
+
|
|
133
|
+
registry.links_listing.find { |row| row[:slug] == name }&.fetch(:registry)
|
|
134
|
+
end
|
|
135
|
+
|
|
111
136
|
def empty?
|
|
112
137
|
entries.empty?
|
|
113
138
|
end
|
|
@@ -465,7 +490,7 @@ module OKF::TUI
|
|
|
465
490
|
def registry_entries
|
|
466
491
|
registry.listing.map do |row|
|
|
467
492
|
build_entry(slug: row[:slug], dir: row[:dir], default: row[:default],
|
|
468
|
-
registered: true, missing: row[:missing])
|
|
493
|
+
registered: true, missing: row[:missing], link: row[:link])
|
|
469
494
|
end
|
|
470
495
|
end
|
|
471
496
|
|
|
@@ -482,7 +507,7 @@ module OKF::TUI
|
|
|
482
507
|
end
|
|
483
508
|
|
|
484
509
|
Group.new(slug: row[:slug], members: row[:members], bundles: bundles || [],
|
|
485
|
-
cyclic: bundles.nil
|
|
510
|
+
cyclic: bundles.nil?, link: row[:link])
|
|
486
511
|
end
|
|
487
512
|
end
|
|
488
513
|
|
|
@@ -508,8 +533,8 @@ module OKF::TUI
|
|
|
508
533
|
end
|
|
509
534
|
end
|
|
510
535
|
|
|
511
|
-
def build_entry(slug:, dir:, default:, registered:, missing:)
|
|
512
|
-
entry = Entry.new(slug: slug, dir: dir, default: default, registered: registered)
|
|
536
|
+
def build_entry(slug:, dir:, default:, registered:, missing:, link: nil)
|
|
537
|
+
entry = Entry.new(slug: slug, dir: dir, default: default, registered: registered, link: link)
|
|
513
538
|
|
|
514
539
|
if missing
|
|
515
540
|
entry.error = "directory is gone"
|