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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.okf/capabilities/index.md +13 -0
  3. data/.okf/capabilities/okf-surface.md +48 -0
  4. data/.okf/capabilities/views.md +48 -0
  5. data/.okf/decisions/invents-no-analysis.md +8 -7
  6. data/.okf/decisions/no-version-ceilings.md +8 -9
  7. data/.okf/decisions/okf-capability-drift.md +16 -18
  8. data/.okf/decisions/one-door-the-plugin-seam.md +15 -14
  9. data/.okf/decisions/registry-write-boundary.md +43 -15
  10. data/.okf/decisions/ruby-floor.md +11 -9
  11. data/.okf/decisions/search-facade-coupling.md +22 -24
  12. data/.okf/decisions/undeclared-width-dependency.md +12 -10
  13. data/.okf/index.md +13 -5
  14. data/.okf/interaction/cross-bundle-scope.md +8 -8
  15. data/.okf/interaction/deferred-search.md +8 -6
  16. data/.okf/interaction/esc-peels-one-layer.md +10 -9
  17. data/.okf/interaction/filter-escalates-to-search.md +12 -11
  18. data/.okf/interaction/following-links.md +10 -8
  19. data/.okf/interaction/key-routing.md +6 -5
  20. data/.okf/interaction/which-registry.md +39 -19
  21. data/.okf/log.md +56 -0
  22. data/.okf/rendering/ansi-aware-width.md +8 -7
  23. data/.okf/rendering/fit-or-say-so.md +34 -0
  24. data/.okf/rendering/index.md +1 -0
  25. data/.okf/rendering/markdown-rendering-trap.md +8 -7
  26. data/.okf/rendering/status-vocabulary.md +6 -5
  27. data/.okf/rendering/whole-frame-painting.md +8 -6
  28. data/.okf/structure/doors.md +61 -0
  29. data/.okf/structure/index.md +16 -0
  30. data/.okf/structure/rendering.md +58 -0
  31. data/.okf/structure/the-app.md +55 -0
  32. data/.okf/structure/the-workspace.md +69 -0
  33. data/.okf/testing/adding-a-view.md +58 -0
  34. data/.okf/testing/ci-matrix.md +8 -6
  35. data/.okf/testing/headless-frames.md +6 -6
  36. data/.okf/testing/index.md +2 -0
  37. data/.okf/testing/pty-test.md +6 -5
  38. data/.okf/testing/the-suite.md +120 -0
  39. data/CHANGELOG.md +95 -1
  40. data/README.md +12 -3
  41. data/lib/okf/tui/app.rb +28 -7
  42. data/lib/okf/tui/cli.rb +2 -2
  43. data/lib/okf/tui/refs.rb +1 -1
  44. data/lib/okf/tui/version.rb +1 -1
  45. data/lib/okf/tui/views.rb +15 -0
  46. data/lib/okf/tui/workspace.rb +34 -9
  47. 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.
@@ -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
- timestamp: 2026-07-19
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
- timestamp: 2026-07-19
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`.
@@ -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.
@@ -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
- timestamp: 2026-07-18
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.0]: https://github.com/serradura/okf-gem/releases/tag/okf-tui/v1.0.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-gem)
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-gem/blob/main/.okf/design/extension-points.md).
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-registry.json` when one is on the path
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
- slug = selected_slug
1439
- return if slug.nil?
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
- slug = selected_slug
1446
- return if slug.nil?
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-registry.json when one is on the
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-registry.json` sitting
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-registry.json` when there is one on the
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OKF
4
4
  module TUI
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
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?
@@ -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-registry.json` from wherever `rake` was run.
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"