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
|
@@ -3,7 +3,18 @@ type: Concept
|
|
|
3
3
|
title: A Dead Filter Offers the Wider Search
|
|
4
4
|
description: Filtering reads metadata in one bundle and searching reads bodies across all of them, so a filter that matches nothing offers the search rather than leaving a dead end.
|
|
5
5
|
tags: [ux, search]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-13
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`lib/okf/tui/views.rb` — `escalation_panel`."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/views.rb
|
|
12
|
+
- title: "`lib/okf/tui/app.rb` — `filter_found_nothing?`, which is where the two views' conditions live side by side."
|
|
13
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
|
|
14
|
+
- title: "`test/integration/search_test.rb` — \"a filter that matches nothing offers the wider search\"."
|
|
15
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/search_test.rb
|
|
16
|
+
- title: "`test/integration/groups_test.rb` — \"a filter matching a group but no bundle has found something\"."
|
|
17
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/groups_test.rb
|
|
7
18
|
---
|
|
8
19
|
|
|
9
20
|
# Overview
|
|
@@ -45,13 +56,3 @@ group and no bundle has found something, and the first cut of this read only the
|
|
|
45
56
|
bundles pane — so `Enter` escalated on the keystroke that was accepting the
|
|
46
57
|
filter, taking the filter, the view and the group the reader was pointing at with
|
|
47
58
|
it. "The filter found nothing" is a claim about the whole view.
|
|
48
|
-
|
|
49
|
-
# Citations
|
|
50
|
-
|
|
51
|
-
[1] `lib/okf/tui/views.rb` — `escalation_panel`.
|
|
52
|
-
[2] `lib/okf/tui/app.rb` — `filter_found_nothing?`, which is where the two views'
|
|
53
|
-
conditions live side by side.
|
|
54
|
-
[3] `test/integration/search_test.rb` — "a filter that matches nothing offers
|
|
55
|
-
the wider search".
|
|
56
|
-
[4] `test/integration/groups_test.rb` — "a filter matching a group but no bundle
|
|
57
|
-
has found something".
|
|
@@ -3,7 +3,16 @@ type: Decision
|
|
|
3
3
|
title: Following a Link Out of the Page
|
|
4
4
|
description: The picker is a mode rather than inline hints, the directory link okf declines to resolve, and the count it deliberately disagrees with.
|
|
5
5
|
tags: [ux, keys, okf-coupling]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-19
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`lib/okf/tui/model.rb` — `links_for`, `resolve_target`, `describe_link`."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/model.rb
|
|
12
|
+
- title: "`lib/okf/tui/app.rb` — `handle_follow`, `follow_selected`, `push_trail`, `back`."
|
|
13
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
|
|
14
|
+
- title: "`test/integration/links_test.rb` — the area-link case is the one that would otherwise have been silently empty."
|
|
15
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/links_test.rb
|
|
7
16
|
---
|
|
8
17
|
|
|
9
18
|
# Overview
|
|
@@ -73,10 +82,3 @@ label rather than being reconciled to the header.
|
|
|
73
82
|
two functions every jump already went through. Opening a search hit and following
|
|
74
83
|
a concept out of the graph became reversible without either being touched, which
|
|
75
84
|
is why the stack lives there rather than in the picker.
|
|
76
|
-
|
|
77
|
-
# Citations
|
|
78
|
-
|
|
79
|
-
[1] `lib/okf/tui/model.rb` — `links_for`, `resolve_target`, `describe_link`.
|
|
80
|
-
[2] `lib/okf/tui/app.rb` — `handle_follow`, `follow_selected`, `push_trail`, `back`.
|
|
81
|
-
[3] `test/integration/links_test.rb` — the area-link case is the one that would
|
|
82
|
-
otherwise have been silently empty.
|
|
@@ -3,7 +3,12 @@ type: Reference
|
|
|
3
3
|
title: Key Routing and Its Modes
|
|
4
4
|
description: handle dispatches through modes before the global keys, which is what keeps digits navigating everywhere, and the guard-fallback trap that a Ruby case statement sets for shared letters.
|
|
5
5
|
tags: [ux, keys]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-18
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`lib/okf/tui/app.rb` — `handle`, `fallback`, `KEY_VIEWS`."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
# The order
|
|
@@ -78,7 +83,3 @@ later. That is the check worth keeping: an arming that never lets go renders
|
|
|
78
83
|
identically to the fixed behaviour until the pair is pressed apart.
|
|
79
84
|
|
|
80
85
|
`Ctrl-c` stays single. An escape hatch that needs confirming is not one.
|
|
81
|
-
|
|
82
|
-
# Citations
|
|
83
|
-
|
|
84
|
-
[1] `lib/okf/tui/app.rb` — `handle`, `fallback`, `KEY_VIEWS`.
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Constraint
|
|
3
3
|
title: Which Registry a Session Is On
|
|
4
|
-
description: okf resolves a project-local .okf
|
|
4
|
+
description: okf resolves a project-local .okf.json before the global $OKF_HOME one; the TUI did not, and being the single verb that disagreed was a silent wrong answer rather than an error.
|
|
5
5
|
tags: [registry, okf-coupling, discovery]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-13
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`lib/okf/tui/workspace.rb` — `open_registry`, and `registry_path` asking the registry rather than recomputing from `home`."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/workspace.rb
|
|
12
|
+
- title: "okf `lib/okf/registry.rb` — `Registry.load(home:, cwd:)`, `LOCAL_FILE`, `NO_DISCOVERY_ENV`, and `#reopen` with the comment recording what a bare `new` costs."
|
|
13
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/registry.rb
|
|
14
|
+
- title: "okf `CHANGELOG.md` 1.12.0 — `okf registry init`, relative path storage, and the hub bug that `#reopen` fixed."
|
|
15
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/CHANGELOG.md
|
|
16
|
+
- title: "`test/integration/refs_test.rb` — the local/global pair, `OKF_NO_DISCOVERY`, and the embedding-app case that must stay global-only."
|
|
17
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/refs_test.rb
|
|
18
|
+
- title: "Reproduced 2026-08-13 in a scratch project: `Registry.load(cwd: pwd).path` → `<project>/.okf-registry.json`, `Registry.load.path` → `<home>/registry.json`."
|
|
19
|
+
resource: "Reproduced 2026-08-13 in a scratch project: `Registry.load(cwd: pwd).path` → `<project>/.okf-registry.json`, `Registry.load.path` → `<home>/registry.json`"
|
|
7
20
|
---
|
|
8
21
|
|
|
9
22
|
# Overview
|
|
10
23
|
|
|
11
24
|
"Which bundles can I see?" has one right answer per directory, and okf decides it:
|
|
12
|
-
`OKF_NO_DISCOVERY` forces the global registry; otherwise a `.okf
|
|
25
|
+
`OKF_NO_DISCOVERY` forces the global registry; otherwise a `.okf.json` — or the
|
|
26
|
+
older `.okf-registry.json`, still discovered so no committed registry breaks —
|
|
13
27
|
found by walking up from the working directory wins; otherwise `$OKF_HOME`
|
|
14
|
-
(default `~/.okf`).
|
|
15
|
-
|
|
28
|
+
(default `~/.okf`). Both names are checked in each directory before climbing, so
|
|
29
|
+
"nearest local file wins" keeps meaning what it says, and a local registry stores
|
|
30
|
+
paths *relative* to itself so it can be committed and travel with the repo.
|
|
16
31
|
|
|
17
32
|
The TUI ignored all of that for a release. `Workspace` called
|
|
18
33
|
`OKF::Registry.load(home: home)` with no `cwd:`, and okf only discovers when it is
|
|
@@ -32,6 +47,25 @@ This is the shape [okf-capability-drift](/decisions/okf-capability-drift.md)
|
|
|
32
47
|
describes: okf added a resolution rule, the old call kept working, and "kept
|
|
33
48
|
working" meant "kept answering the wrong question".
|
|
34
49
|
|
|
50
|
+
# A third input arrived, and the answer stayed one method
|
|
51
|
+
|
|
52
|
+
okf later gained `okf registry link`: the global registry can point at another
|
|
53
|
+
registry file, and that file's bundles and groups resolve into the set. The TUI
|
|
54
|
+
needed no change to show them — `registry_entries` reads `listing` and
|
|
55
|
+
`registry_groups` reads `groups_listing`, and okf folds the linked half into
|
|
56
|
+
both. That is the point. The rule this concept states is that one question has
|
|
57
|
+
one answer per directory; a linked group listed by a *second* method would have
|
|
58
|
+
put the TUI back where it started, showing a smaller set than its own `@ref`
|
|
59
|
+
resolution could open, with nothing failing to say so.
|
|
60
|
+
|
|
61
|
+
What the TUI had to add for itself is that a linked bundle is **read-only**. The
|
|
62
|
+
config keys reached okf, which refused, and the message arrived on the status
|
|
63
|
+
line — correct, and too late: the user had already typed a new name or confirmed
|
|
64
|
+
a removal. `d`, `n`, `x` and `+` now refuse before the prompt, and the detail
|
|
65
|
+
pane says where the bundle comes from. That is
|
|
66
|
+
[registry-write-boundary](/decisions/registry-write-boundary.md)'s, not this
|
|
67
|
+
file's — what belongs here is only that the *set* stayed one answer.
|
|
68
|
+
|
|
35
69
|
# The library keeps okf's own line
|
|
36
70
|
|
|
37
71
|
`cwd:` is a parameter, not a default of `Dir.pwd`, and that mirrors okf exactly:
|
|
@@ -69,17 +103,3 @@ too. That is not decoration — it is the only way a user can tell which of the
|
|
|
69
103
|
registries is in force, and it is what `refs_test.rb` asserts on to prove the CLI
|
|
70
104
|
opts into discovery at all: an empty *local* registry reports the local path,
|
|
71
105
|
where a run that ignored discovery would name the global one.
|
|
72
|
-
|
|
73
|
-
# Citations
|
|
74
|
-
|
|
75
|
-
[1] `lib/okf/tui/workspace.rb` — `open_registry`, and `registry_path` asking the
|
|
76
|
-
registry rather than recomputing from `home`.
|
|
77
|
-
[2] okf `lib/okf/registry.rb` — `Registry.load(home:, cwd:)`, `LOCAL_FILE`,
|
|
78
|
-
`NO_DISCOVERY_ENV`, and `#reopen` with the comment recording what a bare `new`
|
|
79
|
-
costs.
|
|
80
|
-
[3] okf `CHANGELOG.md` 1.12.0 — `okf registry init`, relative path storage, and the
|
|
81
|
-
hub bug that `#reopen` fixed.
|
|
82
|
-
[4] `test/integration/refs_test.rb` — the local/global pair, `OKF_NO_DISCOVERY`, and
|
|
83
|
-
the embedding-app case that must stay global-only.
|
|
84
|
-
[5] Reproduced 2026-08-13 in a scratch project: `Registry.load(cwd: pwd).path` →
|
|
85
|
-
`<project>/.okf-registry.json`, `Registry.load.path` → `<home>/registry.json`.
|
data/.okf/log.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Update Log
|
|
2
2
|
|
|
3
|
+
## 2026-08-22
|
|
4
|
+
|
|
5
|
+
* **The project-local registry is `.okf.json`, and the TUI inherits that for
|
|
6
|
+
free** — [which-registry](interaction/which-registry.md),
|
|
7
|
+
[registry-write-boundary](decisions/registry-write-boundary.md). okf renamed the
|
|
8
|
+
file and kept discovering the old `.okf-registry.json`, checking both names in
|
|
9
|
+
each directory on the way up. `Workspace` needed no change, which is the point
|
|
10
|
+
of asking okf rather than recomputing: the one method that answers "which
|
|
11
|
+
registry am I on" answered the new question the moment the kernel did. What the
|
|
12
|
+
two concepts owed was accuracy — one stated the discovery rule and would have
|
|
13
|
+
stated half of it, and the other named the file it writes.
|
|
14
|
+
|
|
15
|
+
## 2026-08-21
|
|
16
|
+
|
|
17
|
+
* **A linked bundle is read-only, and the four config keys say so before they
|
|
18
|
+
ask** — [registry-write-boundary](decisions/registry-write-boundary.md),
|
|
19
|
+
[which-registry](interaction/which-registry.md). okf's registry can now `link`
|
|
20
|
+
another registry file, whose bundles and groups resolve into the bundles view.
|
|
21
|
+
They list, load, scope and search like any other — but the file that owns them
|
|
22
|
+
is elsewhere, so okf refuses every write. `d`, `n`, `x` and `+` reached okf,
|
|
23
|
+
okf raised, and the message landed on the status line *after* the user had
|
|
24
|
+
typed a new name or confirmed a removal. They now refuse first, naming the
|
|
25
|
+
link; the bundle detail carries `read-only — linked from @onm` and the owning
|
|
26
|
+
file underneath. The registry pane is left alone, because 42 columns cannot
|
|
27
|
+
take another marker without clipping.
|
|
28
|
+
|
|
29
|
+
* **Nothing was needed to *show* linked bundles at all.** The entries come from
|
|
30
|
+
okf's `listing` and the groups from its `groups_listing`, and okf folds the
|
|
31
|
+
linked half into both rather than into a second method — which is the whole
|
|
32
|
+
point of [which-registry](interaction/which-registry.md)'s rule, arriving from
|
|
33
|
+
the kernel's side this time. `Entry#link` and `Group#link` are read off those
|
|
34
|
+
rows, and an agreement test asserts the field against okf itself, since a
|
|
35
|
+
rename there would leave every refusal above quietly not firing.
|
|
36
|
+
|
|
37
|
+
## 2026-08-19
|
|
38
|
+
|
|
39
|
+
* **This bundle moved to OKF v0.2.** `timestamp:` became
|
|
40
|
+
`generated: { by, at }` on all 30 concepts, and the 22 body `# Citations`
|
|
41
|
+
lists became `sources:` — the text preserved, a GitHub URL where the entry
|
|
42
|
+
named a real file, a scope descriptor where it recorded a measurement or a
|
|
43
|
+
repro. Three concepts carried positional `[1]` markers in their bodies; those
|
|
44
|
+
are now `[^1]` footnotes keyed to `sources[].id`, and the sources nothing
|
|
45
|
+
cites lost the id rather than keeping a join half-made.
|
|
46
|
+
|
|
47
|
+
* **The structure and the catalogue moved into this bundle, and a test holds
|
|
48
|
+
them to the code.** `AGENTS.md` carried a hand-maintained Map of `lib/**` that
|
|
49
|
+
nothing checked, so a file could arrive, move or leave and the Map would keep
|
|
50
|
+
reading plausibly. [Structure](/structure/) now owns it — four concepts over
|
|
51
|
+
ten files — and [Capabilities](/capabilities/) owns the catalogue of the six
|
|
52
|
+
views and the kernel surface behind them, which is the list to read before
|
|
53
|
+
building a seventh. `test/unit/bundle_catalog_test.rb` is the pin, and it
|
|
54
|
+
bites in both directions: a file no concept names, a concept naming a file
|
|
55
|
+
that is gone, or a view catalogue that disagrees with `App::TABS`. The test
|
|
56
|
+
suite's own map moved too, to [the-suite](/testing/the-suite.md), and
|
|
57
|
+
[adding-a-view](/testing/adding-a-view.md) is the walk a new screen owes.
|
|
58
|
+
|
|
3
59
|
## 2026-08-15
|
|
4
60
|
|
|
5
61
|
* **Release**: **1.0.0**, the first. Six views over one bundle or many —
|
|
@@ -3,7 +3,14 @@ type: Component
|
|
|
3
3
|
title: ANSI-aware Width
|
|
4
4
|
description: Every layout primitive measures display width on colour-stripped text, because String#length counts escape bytes and a composed frame breaks the moment those two disagree.
|
|
5
5
|
tags: [rendering, terminal, ansi]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-19
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`lib/okf/tui/ui.rb` — the primitives."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/ui.rb
|
|
12
|
+
- title: "`test/integration/geometry_test.rb` — the two-colour matrix and `with_colour`."
|
|
13
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/geometry_test.rb
|
|
7
14
|
---
|
|
8
15
|
|
|
9
16
|
# Overview
|
|
@@ -66,9 +73,3 @@ the same broken measure.
|
|
|
66
73
|
|
|
67
74
|
Which matters because `Ui.width` depends on a gem nobody declared — see
|
|
68
75
|
[the undeclared width dependency](/decisions/undeclared-width-dependency.md).
|
|
69
|
-
|
|
70
|
-
# Citations
|
|
71
|
-
|
|
72
|
-
[1] `lib/okf/tui/ui.rb` — the primitives.
|
|
73
|
-
[2] `test/integration/geometry_test.rb` — the two-colour matrix and
|
|
74
|
-
`with_colour`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Constraint
|
|
3
|
+
title: A layout that cannot fit says so
|
|
4
|
+
description: Health is two panes above 112 columns and one at a time below it, reached by the same key either way — a narrow terminal changes what is shown rather than silently clipping it, and a new split is judged by its narrowest column.
|
|
5
|
+
tags: [rendering, layout, terminal, width]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19T12:00:00Z
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# The rule
|
|
12
|
+
|
|
13
|
+
A pane that cannot fit its content does not clip it. It stops claiming to show
|
|
14
|
+
both halves.
|
|
15
|
+
|
|
16
|
+
The health view is the instance: two panes above 112 columns, one pane at a time
|
|
17
|
+
below it, and the **same `Tab`** reaches the other one in both modes. The key
|
|
18
|
+
keeps its meaning across the breakpoint, so nothing a reader has learned stops
|
|
19
|
+
working when they narrow the window — the mode changes, the vocabulary does not.
|
|
20
|
+
|
|
21
|
+
# How to judge a new split
|
|
22
|
+
|
|
23
|
+
By what its *narrowest* column does to the longest row it must carry, not by how
|
|
24
|
+
it looks at the width you happen to be developing at.
|
|
25
|
+
|
|
26
|
+
Health passes that test by construction rather than by tuning: its right pane
|
|
27
|
+
holds a fixed width because every row in it is short by design, and every row
|
|
28
|
+
that carries a path — the ones with no bound on their length — is on the left.
|
|
29
|
+
A split that puts an unbounded row in a fixed column has already failed, and
|
|
30
|
+
will fail invisibly, because at a comfortable width it looks correct.
|
|
31
|
+
|
|
32
|
+
This is the layout-level form of the same discipline
|
|
33
|
+
[width measurement](ansi-aware-width.md) applies at the character level: a frame
|
|
34
|
+
is only correct if it is correct at the width it was not designed for.
|
data/.okf/rendering/index.md
CHANGED
|
@@ -6,3 +6,4 @@ How a frame is composed, and the arithmetic that breaks when colour is involved.
|
|
|
6
6
|
* [Whole-frame Painting](whole-frame-painting.md) - Repaint everything each keystroke; the purity that buys, and the constraints it imposes.
|
|
7
7
|
* [The tty-markdown Wrapping Trap](markdown-rendering-trap.md) - An `IndexError` that only appears with colour on, and the parse width that avoids it.
|
|
8
8
|
* [One Verdict, Worn Everywhere](status-vocabulary.md) - Collapsing `validate` and `lint` into one colour so a problem is visible from any view.
|
|
9
|
+
* [A layout that cannot fit says so](fit-or-say-so.md) - Two panes or one, the same key either way, and a new split judged by its narrowest column rather than by how it looks now.
|
|
@@ -3,7 +3,14 @@ type: Reference
|
|
|
3
3
|
title: The tty-markdown Wrapping Trap
|
|
4
4
|
description: tty-markdown raises IndexError on some documents when colour is on, so the renderer asks it never to wrap and does its own wrapping instead.
|
|
5
5
|
tags: [rendering, terminal, ansi, scar-tissue]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-18
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`lib/okf/tui/app.rb` — `PARSE_WIDTH` and the render."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
|
|
12
|
+
- title: "Reproduced by sweeping every concept × width with colour forced on: 50/192 raised `IndexError`; 0/192 with the fix."
|
|
13
|
+
resource: "Reproduced by sweeping every concept × width with colour forced on: 50/192 raised `IndexError`; 0/192 with the fix"
|
|
7
14
|
---
|
|
8
15
|
|
|
9
16
|
# The symptom
|
|
@@ -55,9 +62,3 @@ A rendering bug that only appears with colour on cannot be caught by any test
|
|
|
55
62
|
that captures output through a pipe. When a screen misbehaves in the terminal but
|
|
56
63
|
not in a test, **suspect the colour path first** — it is the one the harness
|
|
57
64
|
never walks.
|
|
58
|
-
|
|
59
|
-
# Citations
|
|
60
|
-
|
|
61
|
-
[1] `lib/okf/tui/app.rb` — `PARSE_WIDTH` and the render.
|
|
62
|
-
[2] Reproduced by sweeping every concept × width with colour forced on: 50/192
|
|
63
|
-
raised `IndexError`; 0/192 with the fix.
|
|
@@ -3,7 +3,12 @@ type: Concept
|
|
|
3
3
|
title: One Verdict, Worn Everywhere
|
|
4
4
|
description: A bundle is clean, warned, or not conformant, and that single judgement drives its colour in every place it is named — so a problem is visible without opening the health view.
|
|
5
5
|
tags: [rendering, ux]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-18
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`lib/okf/tui/views.rb` — `health_status`, `STATUS`, `status_style`."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/views.rb
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
# Overview
|
|
@@ -39,7 +44,3 @@ Because the colour is attached to the name rather than to a view, switching the
|
|
|
39
44
|
active bundle changes it everywhere at once, which is what makes it readable
|
|
40
45
|
while moving between bundles — see
|
|
41
46
|
[cross-bundle-scope](/interaction/cross-bundle-scope.md).
|
|
42
|
-
|
|
43
|
-
# Citations
|
|
44
|
-
|
|
45
|
-
[1] `lib/okf/tui/views.rb` — `health_status`, `STATUS`, `status_style`.
|
|
@@ -3,7 +3,14 @@ type: Component
|
|
|
3
3
|
title: Whole-frame Painting
|
|
4
4
|
description: Each keystroke repaints every row from cursor-home rather than diffing, which makes a frame a pure function of state and is what lets the tests render without a terminal.
|
|
5
5
|
tags: [rendering, terminal, testing]
|
|
6
|
-
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-07-18
|
|
9
|
+
sources:
|
|
10
|
+
- title: "`lib/okf/tui/app.rb` — `paint`."
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
|
|
12
|
+
- title: "`test/test_helper.rb` — `FixedScreen`, `frame_for`, `render`."
|
|
13
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/test_helper.rb
|
|
7
14
|
---
|
|
8
15
|
|
|
9
16
|
# Overview
|
|
@@ -45,8 +52,3 @@ differently on the machine running the suite.
|
|
|
45
52
|
earlier per-pane budget that could go negative on a short terminal and crash.
|
|
46
53
|
- **Views stay pure row builders.** No view writes to the terminal; they return
|
|
47
54
|
arrays of rows. The app is the only thing that prints.
|
|
48
|
-
|
|
49
|
-
# Citations
|
|
50
|
-
|
|
51
|
-
[1] `lib/okf/tui/app.rb` — `paint`.
|
|
52
|
-
[2] `test/test_helper.rb` — `FixedScreen`, `frame_for`, `render`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: The Doors, and What Loads When
|
|
4
|
+
description: One entry point through okf's plugin seam, an argv shell that loads on demand, and a ref grammar subclassed from okf rather than rewritten.
|
|
5
|
+
tags: [structure, cli, plugin, loading]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# The files
|
|
12
|
+
|
|
13
|
+
| file | what it owns |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `lib/okf/tui.rb` | `OKF::TUI`, `OKF::TUI::Error`, and the two capability probes |
|
|
16
|
+
| `lib/okf/plugin.rb` | `OKF::CLI::Tui` — registers `okf tui`, the gem's only entry point |
|
|
17
|
+
| `lib/okf/tui/cli.rb` | `OKF::TUI::CLI` — the only layer that parses argv, prints, and exits |
|
|
18
|
+
| `lib/okf/tui/refs.rb` | `OKF::TUI::Refs` — argv to bundle dirs, through okf's own ref grammar |
|
|
19
|
+
| `lib/okf/tui/version.rb` | `OKF::TUI::VERSION` |
|
|
20
|
+
|
|
21
|
+
# One door, and what that costs the dispatcher
|
|
22
|
+
|
|
23
|
+
There is **no executable**. `okf tui` is the whole surface, and the reasoning is
|
|
24
|
+
[one-door-the-plugin-seam](/decisions/one-door-the-plugin-seam.md).
|
|
25
|
+
|
|
26
|
+
The consequence to hold on to: **the dispatcher must add nothing but argv and
|
|
27
|
+
the streams.** `plugin_test.rb` pins it by driving the same run both through
|
|
28
|
+
`OKF::CLI.start` and straight into `CLI.run`, comparing the exit code and the
|
|
29
|
+
message.
|
|
30
|
+
|
|
31
|
+
`help_rows` is a promise as much as a help line. It read `tui [DIR|@slug…]` for
|
|
32
|
+
a whole release while the CLI rejected every `@slug` as "not a directory", so
|
|
33
|
+
the test now asserts the advertised form actually resolves.
|
|
34
|
+
|
|
35
|
+
# The ref grammar is okf's
|
|
36
|
+
|
|
37
|
+
`Refs` subclasses `OKF::CLI::Command`, so `@slug`, a bare `@`, an `@group` and
|
|
38
|
+
the refusal of `@all` mean exactly what they mean to `okf server` — same
|
|
39
|
+
messages, same exit codes. It reaches a *private* helper
|
|
40
|
+
(`resolve_ref_expanding`), which is a deliberate trade: one copy of the grammar,
|
|
41
|
+
at the cost of a coupling that `refs_test.rb` pins **by name**, so okf moving it
|
|
42
|
+
fails loudly rather than quietly restoring "not a directory".
|
|
43
|
+
|
|
44
|
+
The same call is what opts this gem into registry discovery, since okf's
|
|
45
|
+
`open_registry` is `Registry.load(cwd: Dir.pwd)` — which is why the TUI resolves
|
|
46
|
+
the same registry every other `okf` verb run from that directory resolves. See
|
|
47
|
+
[which-registry](/interaction/which-registry.md).
|
|
48
|
+
|
|
49
|
+
# What loads when
|
|
50
|
+
|
|
51
|
+
`require "okf/tui"` loads the library only. `cli.rb` arrives on demand — the
|
|
52
|
+
plugin's `#call` requires it, and so must any test that drives it.
|
|
53
|
+
|
|
54
|
+
That is not tidiness. okf reads `plugin.rb` whenever a verb misses or `okf help`
|
|
55
|
+
runs, so it must stay cheap: it registers a class and nothing else, and the TTY
|
|
56
|
+
toolkit is required inside `#call`. A subprocess test asserts that loading the
|
|
57
|
+
plugin leaves `TTY::Box` undefined.
|
|
58
|
+
|
|
59
|
+
`OKF::TUI.search_capable?` and `.spec_capable?` are the other half of that
|
|
60
|
+
caution — they ask the installed okf what it can do rather than assuming a
|
|
61
|
+
version, which is [okf-capability-drift](/decisions/okf-capability-drift.md).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Structure
|
|
2
|
+
|
|
3
|
+
Every file under `lib/`, grouped by the layer that owns it. One concept owns
|
|
4
|
+
each file, and `test/unit/bundle_catalog_test.rb` fails if that stops being true
|
|
5
|
+
in either direction — a file no concept names, or a concept naming a file that
|
|
6
|
+
is gone.
|
|
7
|
+
|
|
8
|
+
Ten files, four layers, and the dependency runs one way: the doors build a
|
|
9
|
+
workspace, the workspace answers questions about bundles, the app holds the
|
|
10
|
+
state and the key loop, and the rendering layer turns that state into rows.
|
|
11
|
+
Nothing below reaches back up.
|
|
12
|
+
|
|
13
|
+
* [The Doors](doors.md) - `lib/okf/tui.rb`, `lib/okf/plugin.rb`, `lib/okf/tui/cli.rb`, `lib/okf/tui/refs.rb`, `lib/okf/tui/version.rb` — the one entry point, the argv shell, and the ref grammar borrowed rather than rewritten.
|
|
14
|
+
* [The Workspace and the Model](the-workspace.md) - `lib/okf/tui/workspace.rb`, `lib/okf/tui/model.rb` — which bundles a session can see, and every answer about one of them.
|
|
15
|
+
* [The App](the-app.md) - `lib/okf/tui/app.rb` — state, the key loop, and the frame.
|
|
16
|
+
* [The Rendering Layer](rendering.md) - `lib/okf/tui/views.rb`, `lib/okf/tui/ui.rb` — the six screens as row builders, and the primitives that make a row fit.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: The Rendering Layer
|
|
4
|
+
description: Six screens built as rows of text with no terminal I/O, over primitives that measure and cut strings by display width rather than by character count.
|
|
5
|
+
tags: [structure, rendering, ansi, layout]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# The files
|
|
12
|
+
|
|
13
|
+
| file | pure? | what it owns |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `lib/okf/tui/views.rb` | pure | the six screens — row builders, no terminal I/O |
|
|
16
|
+
| `lib/okf/tui/ui.rb` | pure | layout primitives: width, clipping, wrapping, boxes |
|
|
17
|
+
|
|
18
|
+
Both are pure, and that is the property the test suite is built on: a view
|
|
19
|
+
returns an array of strings, so a test can assert a frame without a pty.
|
|
20
|
+
|
|
21
|
+
# Views: one builder per screen, plus the chrome
|
|
22
|
+
|
|
23
|
+
`Views` is a module of builders taking `(app, width, height)` and returning rows.
|
|
24
|
+
Beyond the six screens it owns the chrome every screen wears — `header`,
|
|
25
|
+
`workspace_header`, `tabs`, `footer`, `active_badge` — and the two vocabularies
|
|
26
|
+
that must be identical everywhere: `TYPE_COLOURS` / `type_colour`, and `STATUS` /
|
|
27
|
+
`health_status` / `status_style`.
|
|
28
|
+
|
|
29
|
+
That second one is the point of [status-vocabulary](/rendering/status-vocabulary.md):
|
|
30
|
+
`validate` and `lint` collapse into **one** verdict so a problem is visible from
|
|
31
|
+
any view, rather than only from the one that ran the check.
|
|
32
|
+
|
|
33
|
+
`pair` is the label/value row every detail panel is made of, and it is worth
|
|
34
|
+
reusing rather than re-aligning by hand.
|
|
35
|
+
|
|
36
|
+
# Ui: nothing here counts characters
|
|
37
|
+
|
|
38
|
+
`String#length` is the wrong answer twice over — a wide CJK glyph occupies two
|
|
39
|
+
columns, and an ANSI escape occupies none — so every primitive here measures
|
|
40
|
+
display width instead. `ANSI` is the escape pattern, `width` the measure, and
|
|
41
|
+
`clip` / `clip_ansi` / `wrap_ansi` / `reflow` the cuts that respect it.
|
|
42
|
+
[ansi-aware-width](/rendering/ansi-aware-width.md) is the arithmetic and why the
|
|
43
|
+
geometry suite runs twice.
|
|
44
|
+
|
|
45
|
+
`TABULAR` exists because a box-drawing run must not be reflowed as prose, and
|
|
46
|
+
`LIST_START` because a bullet's continuation lines have to hang under its text.
|
|
47
|
+
`reset_if_styled` closes a style at the end of a row so a colour cannot bleed
|
|
48
|
+
into the next one — a frame is printed whole, so one unterminated sequence
|
|
49
|
+
stains everything below it.
|
|
50
|
+
|
|
51
|
+
`Line` is the builder for a single row under a width budget; `blank_line`,
|
|
52
|
+
`fit_block`, `hjoin`, `box` and `title_label` compose them. `PASTEL` is
|
|
53
|
+
constructed once, with `FORCE_COLOR` honoured, because colour detection is a
|
|
54
|
+
terminal question and the tests need to answer it themselves.
|
|
55
|
+
|
|
56
|
+
The trap that only appears with colour on is
|
|
57
|
+
[markdown-rendering-trap](/rendering/markdown-rendering-trap.md); read it before
|
|
58
|
+
touching anything that hands a width to tty-markdown.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: The App — State, the Key Loop, and the Frame
|
|
4
|
+
description: "One class holding the whole session: which view, which selection, which prompt is open, and the loop that reads a key, changes that state, and repaints every row."
|
|
5
|
+
tags: [structure, keyboard, state, terminal]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# The file
|
|
12
|
+
|
|
13
|
+
| file | pure? | what it owns |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `lib/okf/tui/app.rb` | shell | state, the key loop, frame painting |
|
|
16
|
+
|
|
17
|
+
It is the largest file in the gem and the only one that reads a keystroke, and
|
|
18
|
+
those two facts are the same fact: everything that is *not* state or input has
|
|
19
|
+
been pushed down into `Views` and `Ui`, or out into `Workspace` and `Model`.
|
|
20
|
+
|
|
21
|
+
# The tables that define the surface
|
|
22
|
+
|
|
23
|
+
| constant | what it fixes |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `TABS` | the six views, in the order the tab bar shows them, `help` last |
|
|
26
|
+
| `KEY_VIEWS` | `1`–`6`, the direct jump to each |
|
|
27
|
+
| `SINGLE_BUNDLE_VIEWS` | `browse`, `health`, `graph` — the ones with nothing to show when no bundle can be read |
|
|
28
|
+
| `CONTENT_VIEWS` | `health`, `help` — a scrolling page rather than a selectable list |
|
|
29
|
+
| `FILTERABLE_VIEWS` | `bundles`, `browse`, `graph` — the ones `/` narrows |
|
|
30
|
+
| `PANES` | `bundles`, `groups`, `members` — what Tab cycles inside the bundles view |
|
|
31
|
+
|
|
32
|
+
There were seven views once. The `index` map went because browse answers the
|
|
33
|
+
same question with a key the reader already has, and what the map could show and
|
|
34
|
+
browse cannot is real but narrow. A seventh tab is a real cost on a six-tab bar.
|
|
35
|
+
|
|
36
|
+
`Prompt` is a pending question on the status line: `kind` decides what the answer
|
|
37
|
+
does, and `free_text?` says whether it collects a line or a single confirming
|
|
38
|
+
key. Every destructive registry action goes through one.
|
|
39
|
+
|
|
40
|
+
# The loop, and why the frame is a pure function
|
|
41
|
+
|
|
42
|
+
`run` reads a key, routes it, and calls `paint`. `paint` moves the cursor home
|
|
43
|
+
and prints exactly `height` rows — no damage tracking, no dirty regions. That is
|
|
44
|
+
what makes a frame a pure function of state, which is in turn what lets the whole
|
|
45
|
+
test suite render without a terminal
|
|
46
|
+
([headless-frames](/testing/headless-frames.md)).
|
|
47
|
+
|
|
48
|
+
Key routing has modes, and Esc peels one layer at a time rather than quitting
|
|
49
|
+
outright — both arrived at by getting them wrong first:
|
|
50
|
+
[key-routing](/interaction/key-routing.md),
|
|
51
|
+
[esc-peels-one-layer](/interaction/esc-peels-one-layer.md).
|
|
52
|
+
`/` starts a filter, and the filter belongs to the *view*, so switching away
|
|
53
|
+
drops it rather than carrying a bundle filter into the concept list; where the
|
|
54
|
+
filter runs out, [filter-escalates-to-search](/interaction/filter-escalates-to-search.md)
|
|
55
|
+
is what happens next.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Component
|
|
3
|
+
title: The Workspace and the Model
|
|
4
|
+
description: Two files answering two different questions — which bundles this session can see, and everything there is to know about one of them — with the memoization that makes a repaint free.
|
|
5
|
+
tags: [structure, registry, model, memoization]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-19
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# The files
|
|
12
|
+
|
|
13
|
+
| file | pure? | what it owns |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `lib/okf/tui/workspace.rb` | shell | the bundles a session can see; the **only** registry writes in the gem |
|
|
16
|
+
| `lib/okf/tui/model.rb` | pure | one bundle, and every answer about it, memoized |
|
|
17
|
+
|
|
18
|
+
# Workspace: which bundles, and which of them count
|
|
19
|
+
|
|
20
|
+
`Workspace` holds `Entry` (a bundle: slug, dir, default?, registered?, loaded?)
|
|
21
|
+
and `Group` (a registry group, with `cyclic?` for the one it cannot resolve).
|
|
22
|
+
Its two axes are independent and it is worth stating plainly, because conflating
|
|
23
|
+
them is the mistake the code is shaped to prevent:
|
|
24
|
+
|
|
25
|
+
* **active** — `switch`, `active`, `model`: the one bundle the single-bundle
|
|
26
|
+
views are about;
|
|
27
|
+
* **scope** — `scope`, `scoped?`, `toggle_scope`, `scope_all`, `scope_none`,
|
|
28
|
+
`scope_only`, `scope_group`: the set search runs across.
|
|
29
|
+
|
|
30
|
+
[cross-bundle-scope](/interaction/cross-bundle-scope.md) is why they are two
|
|
31
|
+
things and not one.
|
|
32
|
+
|
|
33
|
+
`add`, `remove` and the rest of the mutators are the **only** writes to the
|
|
34
|
+
registry anywhere in this gem, deliberately — the boundary and what it refuses
|
|
35
|
+
are [registry-write-boundary](/decisions/registry-write-boundary.md).
|
|
36
|
+
`registry_backed?` and `registry_path` are how a view says which registry it is
|
|
37
|
+
looking at without guessing.
|
|
38
|
+
|
|
39
|
+
`search` is the cross-bundle one, and it goes through okf's search facade rather
|
|
40
|
+
than reimplementing ranking — with the sharp edge recorded in
|
|
41
|
+
[search-facade-coupling](/decisions/search-facade-coupling.md).
|
|
42
|
+
|
|
43
|
+
# Model: every answer about one bundle, computed once
|
|
44
|
+
|
|
45
|
+
`Model` is pure and **memoized**, and both properties are load-bearing. Pure,
|
|
46
|
+
because the app repaints the entire frame on every keystroke
|
|
47
|
+
([whole-frame-painting](/rendering/whole-frame-painting.md)) and a view that
|
|
48
|
+
recomputed a lint on each one would be unusable. Memoized, for the same reason.
|
|
49
|
+
|
|
50
|
+
What it already answers — read this before computing anything in a view:
|
|
51
|
+
|
|
52
|
+
| question | reader |
|
|
53
|
+
|---|---|
|
|
54
|
+
| the bundle's concepts, as rows | `rows`, `row_by_id`, `concept_by_id` |
|
|
55
|
+
| the catalog and the graph | `catalog`, `graph` |
|
|
56
|
+
| conformance and curation | `validation`, `lint`, `skipped_checks` |
|
|
57
|
+
| the two v0.2 postures | `trust_posture`, `status_posture`, `shows_trust?` |
|
|
58
|
+
| size and shape | `concept_count`, `edge_count`, `orphan_ids`, `hubs` |
|
|
59
|
+
| the directory tree | `dirs`, `dir_traffic`, `dir_arcs`, `under_dir?` |
|
|
60
|
+
| the facets | `types`, `tags`, `types_of`, `tags_of`, `statuses_of`, `tiers_of` |
|
|
61
|
+
| what the bundle declares itself to be | `okf_version` |
|
|
62
|
+
|
|
63
|
+
Every one of those is okf's judgement, not this gem's. `Model` is a reader over
|
|
64
|
+
the kernel's analysers, and the rule that keeps it that way is
|
|
65
|
+
[invents-no-analysis](/decisions/invents-no-analysis.md): a number on screen
|
|
66
|
+
that okf did not compute is a number that will disagree with `okf lint`.
|
|
67
|
+
|
|
68
|
+
`UNTYPED` and `type_label` are the one exception, and they are labelling rather
|
|
69
|
+
than analysis — a concept with no `type` still needs a row.
|