@miadi/chronicle-tui 0.3.0 → 0.4.1
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.
- package/README.md +41 -21
- package/dist/App.d.ts +14 -5
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +595 -321
- package/dist/App.js.map +1 -1
- package/dist/actions.d.ts +31 -21
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +62 -44
- package/dist/actions.js.map +1 -1
- package/dist/api.d.ts +58 -30
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +129 -44
- package/dist/api.js.map +1 -1
- package/dist/attention.d.ts +50 -0
- package/dist/attention.d.ts.map +1 -0
- package/dist/attention.js +69 -0
- package/dist/attention.js.map +1 -0
- package/dist/bin.d.ts +2 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +19 -11
- package/dist/bin.js.map +1 -1
- package/dist/data.d.ts +48 -12
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +124 -24
- package/dist/data.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +10 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +47 -35
- package/dist/model.js.map +1 -1
- package/dist/text.d.ts +23 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +57 -0
- package/dist/text.js.map +1 -0
- package/package.json +5 -4
package/dist/App.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* The terminal browser's screen. It draws the panes and routes keys; every
|
|
4
|
-
* read goes through `data.ts`, every operation through the
|
|
4
|
+
* read goes through `data.ts` and `attention.ts`, every operation through the
|
|
5
|
+
* action registry, every printed string through `text.ts`.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* State lives in one ref and is read from there, so keys that arrive together
|
|
8
|
+
* (fast typing, a paste, ssh) each act on the state the previous one left, not
|
|
9
|
+
* on the last render (jgwill/Miadi#706 UI review, F5 and F13).
|
|
10
|
+
*
|
|
11
|
+
* Keys are plain letters, arrows, Tab, Enter and Esc. No Ctrl or Meta chord is
|
|
12
|
+
* taken, so tmux (C-b) and herdr (prefix+…) keep all of theirs.
|
|
8
13
|
*/
|
|
9
|
-
import { useEffect, useMemo,
|
|
10
|
-
import { Box, Text, useApp, useInput,
|
|
11
|
-
import {
|
|
12
|
-
import { DEFAULT_ACTIONS, actionFor,
|
|
14
|
+
import { useEffect, useMemo, useReducer, useRef } from "react";
|
|
15
|
+
import { Box, Text, useApp, useInput, useWindowSize } from "ink";
|
|
16
|
+
import { MAX_FILES } from "@miadi/episode-vessel";
|
|
17
|
+
import { DEFAULT_ACTIONS, actionFor, } from "./actions.js";
|
|
13
18
|
import { TOKEN_ENV, answerAttention, connectApi } from "./api.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
19
|
+
import { BOARD_LABELS, ROOM_LABELS, answeredMessage, checkAnswer, filterBoard, loadBoard, loadEpisodePanel, panelView, readOnlyLabel, } from "./attention.js";
|
|
20
|
+
import { VISITOR, loadChronicle, loadDetail, loadText, openUrl, qrLines, search, visibleRows, } from "./data.js";
|
|
21
|
+
import { ageOf, clamp, fileView, filterRows, formatDate, screenLineOf, windowOf, wrapSegments, } from "./model.js";
|
|
22
|
+
import { clean, cleanLine, link } from "./text.js";
|
|
16
23
|
const DIRECTION_MARK = { east: "E", south: "S", west: "W", north: "N" };
|
|
17
24
|
const DIRECTION_COLOR = { east: "yellow", south: "red", west: "blue", north: "white" };
|
|
18
25
|
function Segments({ segments, selectedRef }) {
|
|
@@ -30,341 +37,542 @@ function Segments({ segments, selectedRef }) {
|
|
|
30
37
|
return _jsx(Text, { children: segment.text }, index);
|
|
31
38
|
}) }));
|
|
32
39
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
40
|
+
/** Rows an episode argument names: the folder name, a number (all folders with it), or a slug fragment. */
|
|
41
|
+
function matchRows(rows, ref) {
|
|
42
|
+
const wanted = ref.trim().toLowerCase();
|
|
43
|
+
const exact = rows.findIndex((row) => row.name.toLowerCase() === wanted);
|
|
36
44
|
if (exact !== -1)
|
|
37
|
-
return exact;
|
|
38
|
-
const number = Number(
|
|
45
|
+
return [exact];
|
|
46
|
+
const number = Number(wanted.replace(/^ep/, ""));
|
|
39
47
|
if (Number.isInteger(number)) {
|
|
40
|
-
const byNumber = rows.
|
|
41
|
-
if (byNumber
|
|
48
|
+
const byNumber = rows.flatMap((row, index) => (row.number === number ? [index] : []));
|
|
49
|
+
if (byNumber.length)
|
|
42
50
|
return byNumber;
|
|
43
51
|
}
|
|
44
|
-
|
|
52
|
+
const bySlug = rows.findIndex((row) => row.name.toLowerCase().includes(wanted));
|
|
53
|
+
return bySlug === -1 ? [] : [bySlug];
|
|
54
|
+
}
|
|
55
|
+
/** A path cut in the middle, keeping its file name. */
|
|
56
|
+
function middle(text, width) {
|
|
57
|
+
if (text.length <= width)
|
|
58
|
+
return text;
|
|
59
|
+
const keep = Math.max(4, width - 1);
|
|
60
|
+
const tail = Math.ceil(keep * 0.6);
|
|
61
|
+
return `${text.slice(0, keep - tail)}…${text.slice(text.length - tail)}`;
|
|
45
62
|
}
|
|
46
|
-
|
|
63
|
+
const PLAIN_KEY = {
|
|
64
|
+
upArrow: false,
|
|
65
|
+
downArrow: false,
|
|
66
|
+
leftArrow: false,
|
|
67
|
+
rightArrow: false,
|
|
68
|
+
pageDown: false,
|
|
69
|
+
pageUp: false,
|
|
70
|
+
home: false,
|
|
71
|
+
end: false,
|
|
72
|
+
return: false,
|
|
73
|
+
escape: false,
|
|
74
|
+
ctrl: false,
|
|
75
|
+
shift: false,
|
|
76
|
+
tab: false,
|
|
77
|
+
backspace: false,
|
|
78
|
+
delete: false,
|
|
79
|
+
meta: false,
|
|
80
|
+
super: false,
|
|
81
|
+
hyper: false,
|
|
82
|
+
capsLock: false,
|
|
83
|
+
numLock: false,
|
|
84
|
+
};
|
|
85
|
+
export function App(props) {
|
|
86
|
+
const { root, actions: extra = [], url, withheld = [] } = props;
|
|
47
87
|
const { exit } = useApp();
|
|
48
|
-
const {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
88
|
+
const { columns, rows } = useWindowSize();
|
|
89
|
+
const [, render] = useReducer((n) => n + 1, 0);
|
|
90
|
+
const ui = useRef({
|
|
91
|
+
chronicle: null,
|
|
92
|
+
loadError: null,
|
|
93
|
+
query: props.search ?? props.query ?? "",
|
|
94
|
+
inputMode: false,
|
|
95
|
+
searchMode: props.search ? "full" : "title",
|
|
96
|
+
view: props.attention ? "attention" : props.search ? "hits" : "list",
|
|
97
|
+
listSel: 0,
|
|
98
|
+
fileSel: 0,
|
|
99
|
+
hits: null,
|
|
100
|
+
hitSel: 0,
|
|
101
|
+
searching: false,
|
|
102
|
+
open: null,
|
|
103
|
+
overlays: [],
|
|
104
|
+
status: "",
|
|
105
|
+
api: null,
|
|
106
|
+
reader: null,
|
|
107
|
+
access: VISITOR,
|
|
108
|
+
mayAnswer: false,
|
|
109
|
+
board: null,
|
|
110
|
+
boardSel: 0,
|
|
111
|
+
history: [],
|
|
112
|
+
pendingEpisode: props.episode,
|
|
113
|
+
});
|
|
67
114
|
const details = useRef(new Map());
|
|
115
|
+
const overlaySeq = useRef(0);
|
|
68
116
|
const searchTurn = useRef(0);
|
|
117
|
+
const searchTimer = useRef(null);
|
|
118
|
+
const searchAbort = useRef(null);
|
|
69
119
|
const actions = useMemo(() => [...DEFAULT_ACTIONS, ...extra], [extra]);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
/** Change state; a new view or episode clears a stale status line (UI review, F22). */
|
|
121
|
+
const set = (patch) => {
|
|
122
|
+
const before = ui.current;
|
|
123
|
+
const next = { ...before, ...patch };
|
|
124
|
+
if (patch.status === undefined && (next.view !== before.view || next.listSel !== before.listSel || next.open !== before.open))
|
|
125
|
+
next.status = "";
|
|
126
|
+
ui.current = next;
|
|
127
|
+
render();
|
|
128
|
+
};
|
|
129
|
+
// ── derived state, always from the ref ───────────────────────────────────
|
|
130
|
+
const derive = () => {
|
|
131
|
+
const s = ui.current;
|
|
132
|
+
const visible = s.chronicle ? visibleRows(s.chronicle.rows, s.access) : [];
|
|
133
|
+
const visibleNames = new Set(visible.map((row) => row.name));
|
|
134
|
+
const filtered = s.searchMode === "title" ? filterRows(visible, s.query) : visible;
|
|
135
|
+
const board = filterBoard((s.board ?? []).filter((item) => visibleNames.has(item.episode)), s.view === "attention" ? s.query : "");
|
|
136
|
+
const boardItem = board[clamp(s.boardSel, board.length)];
|
|
137
|
+
const rowByName = (name) => visible.find((row) => row.name === name);
|
|
138
|
+
const hit = s.view === "hits" || s.open?.from === "hits" ? s.hits?.hits[s.hitSel] : undefined;
|
|
139
|
+
const currentRow = s.view === "file" && s.open
|
|
140
|
+
? rowByName(s.open.name)
|
|
141
|
+
: s.view === "hits"
|
|
142
|
+
? hit && rowByName(hit.episodePath)
|
|
143
|
+
: s.view === "attention"
|
|
144
|
+
? boardItem && rowByName(boardItem.episode)
|
|
145
|
+
: filtered[clamp(s.listSel, filtered.length)];
|
|
146
|
+
let detail = null;
|
|
147
|
+
if (s.chronicle && currentRow && s.view !== "attention") {
|
|
148
|
+
const key = `${s.access.mayWrite}:${currentRow.name}`;
|
|
149
|
+
detail = details.current.get(key) ?? null;
|
|
150
|
+
if (!detail) {
|
|
151
|
+
detail = loadDetail(s.chronicle.root, currentRow.name, s.access);
|
|
152
|
+
details.current.set(key, detail);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const files = detail?.files ?? [];
|
|
156
|
+
const openCount = (s.board ?? []).filter((item) => visibleNames.has(item.episode)).length;
|
|
157
|
+
return { s, visible, filtered, board, boardItem, rowByName, hit, currentRow, detail, files, openCount };
|
|
158
|
+
};
|
|
159
|
+
const compact = columns < 90;
|
|
160
|
+
const bodyHeight = Math.max(3, rows - 4);
|
|
161
|
+
// ── loading: the chronicle, the board, the app ───────────────────────────
|
|
162
|
+
const resolvePending = () => {
|
|
163
|
+
const { s, filtered } = derive();
|
|
164
|
+
if (!s.pendingEpisode || !s.chronicle)
|
|
165
|
+
return;
|
|
166
|
+
const matches = matchRows(filtered, s.pendingEpisode);
|
|
167
|
+
if (matches.length === 0) {
|
|
168
|
+
// Not under the filter: look without it (UI review, F12).
|
|
169
|
+
const all = visibleRows(s.chronicle.rows, s.access);
|
|
170
|
+
const unfiltered = matchRows(all, s.pendingEpisode);
|
|
171
|
+
if (unfiltered.length === 0) {
|
|
172
|
+
if (s.api === null || s.access.signedIn)
|
|
173
|
+
set({ status: `No episode you can see matches ${cleanLine(s.pendingEpisode)}.`, pendingEpisode: undefined });
|
|
174
|
+
return;
|
|
81
175
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
176
|
+
set({ query: "", searchMode: "title" });
|
|
177
|
+
return resolvePending();
|
|
178
|
+
}
|
|
179
|
+
const names = matches.map((index) => filtered[index].number ?? filtered[index].name);
|
|
180
|
+
set({
|
|
181
|
+
listSel: matches[0],
|
|
182
|
+
fileSel: 0,
|
|
183
|
+
view: "files",
|
|
184
|
+
pendingEpisode: undefined,
|
|
185
|
+
status: matches.length > 1 ? `${cleanLine(s.pendingEpisode)} names ${matches.length} folders; showing the first of ${names.join(", ")}.` : "",
|
|
186
|
+
});
|
|
187
|
+
};
|
|
85
188
|
useEffect(() => {
|
|
189
|
+
loadChronicle(root).then((chronicle) => {
|
|
190
|
+
set({ chronicle });
|
|
191
|
+
resolvePending();
|
|
192
|
+
loadBoard(chronicle.root).then((board) => set({ board }), () => set({ board: [] }));
|
|
193
|
+
}, (error) => set({ loadError: error.message }));
|
|
86
194
|
let live = true;
|
|
87
195
|
connectApi(url)
|
|
88
|
-
.then(async (
|
|
196
|
+
.then(async ({ api, warning }) => {
|
|
89
197
|
if (!live)
|
|
90
198
|
return;
|
|
91
|
-
|
|
92
|
-
if (!
|
|
199
|
+
set({ api, status: warning ? cleanLine(warning) : ui.current.status });
|
|
200
|
+
if (!api?.signed)
|
|
93
201
|
return;
|
|
94
202
|
try {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
203
|
+
const reader = await api.me();
|
|
204
|
+
const mayAnswer = await api.mayAnswer();
|
|
205
|
+
if (!live)
|
|
206
|
+
return;
|
|
207
|
+
details.current.clear();
|
|
208
|
+
set({ reader, mayAnswer, access: { signedIn: Boolean(reader), mayWrite: mayAnswer } });
|
|
209
|
+
resolvePending();
|
|
98
210
|
}
|
|
99
211
|
catch (error) {
|
|
100
212
|
if (live)
|
|
101
|
-
|
|
213
|
+
set({ status: cleanLine(`The token was refused by ${api.base}: ${error.message}`) });
|
|
102
214
|
}
|
|
103
215
|
})
|
|
104
216
|
.catch(() => { });
|
|
105
217
|
return () => {
|
|
106
218
|
live = false;
|
|
107
219
|
};
|
|
108
|
-
}, [url]);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
if (!chronicle || searchMode !== "full")
|
|
113
|
-
return;
|
|
114
|
-
const text = query.trim();
|
|
115
|
-
if (text.length < 2)
|
|
116
|
-
return;
|
|
220
|
+
}, [root, url]);
|
|
221
|
+
// ── full-text search: debounced, cancelled when superseded ───────────────
|
|
222
|
+
const scheduleSearch = () => {
|
|
117
223
|
const turn = ++searchTurn.current;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
224
|
+
if (searchTimer.current)
|
|
225
|
+
clearTimeout(searchTimer.current);
|
|
226
|
+
searchAbort.current?.abort();
|
|
227
|
+
const s = ui.current;
|
|
228
|
+
if (!s.chronicle || s.searchMode !== "full" || s.query.trim().length < 2) {
|
|
229
|
+
if (s.searching)
|
|
230
|
+
set({ searching: false });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
searchTimer.current = setTimeout(() => {
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
searchAbort.current = controller;
|
|
236
|
+
set({ searching: true });
|
|
237
|
+
const now = ui.current;
|
|
238
|
+
search(now.chronicle.root, now.query.trim(), now.chronicle.rows, now.access, controller.signal).then((result) => {
|
|
239
|
+
if (turn !== searchTurn.current || ui.current.searchMode !== "full")
|
|
122
240
|
return;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
setSearching(false);
|
|
126
|
-
setView((current) => (current === "file" ? current : "hits"));
|
|
241
|
+
const view = ui.current.view === "file" || ui.current.view === "attention" ? ui.current.view : "hits";
|
|
242
|
+
set({ hits: result, hitSel: 0, searching: false, view });
|
|
127
243
|
}, (error) => {
|
|
128
|
-
if (turn !== searchTurn.current)
|
|
244
|
+
if (turn !== searchTurn.current || error.name === "AbortError")
|
|
129
245
|
return;
|
|
130
|
-
|
|
131
|
-
setStatus(error.message);
|
|
246
|
+
set({ searching: false, status: cleanLine(error.message) });
|
|
132
247
|
});
|
|
133
248
|
}, 250);
|
|
134
|
-
return () => clearTimeout(timer);
|
|
135
|
-
}, [chronicle, query, searchMode]);
|
|
136
|
-
const rowByName = (name) => chronicle?.rows.find((row) => row.name === name);
|
|
137
|
-
const hit = view === "hits" || open?.from === "hits" ? hits?.hits[hitSel] : undefined;
|
|
138
|
-
const currentRow = view === "file" && open
|
|
139
|
-
? rowByName(open.name)
|
|
140
|
-
: view === "hits"
|
|
141
|
-
? hit && rowByName(hit.episodePath)
|
|
142
|
-
: filtered[clamp(listSel, filtered.length)];
|
|
143
|
-
const detailOf = (name) => {
|
|
144
|
-
const cached = details.current.get(name);
|
|
145
|
-
if (cached)
|
|
146
|
-
return cached;
|
|
147
|
-
const loaded = loadDetail(chronicle.root, name);
|
|
148
|
-
details.current.set(name, loaded);
|
|
149
|
-
return loaded;
|
|
150
249
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (props.search)
|
|
252
|
+
scheduleSearch();
|
|
253
|
+
}, [ui.current.chronicle]);
|
|
254
|
+
// ── overlays: a stack, with ids so a late result never lands on the wrong one
|
|
255
|
+
const topId = () => ui.current.overlays[ui.current.overlays.length - 1]?.id ?? 0;
|
|
256
|
+
const openOverlay = (overlay) => {
|
|
257
|
+
const id = ++overlaySeq.current;
|
|
258
|
+
set({ overlays: [...ui.current.overlays, { id, overlay }] });
|
|
259
|
+
return id;
|
|
260
|
+
};
|
|
261
|
+
const replaceOverlay = (id, overlay) => {
|
|
262
|
+
const stack = ui.current.overlays;
|
|
263
|
+
if (stack[stack.length - 1]?.id !== id)
|
|
264
|
+
return false;
|
|
265
|
+
set({ overlays: [...stack.slice(0, -1), { id, overlay }] });
|
|
266
|
+
return true;
|
|
267
|
+
};
|
|
268
|
+
const updateTop = (overlay) => {
|
|
269
|
+
const stack = ui.current.overlays;
|
|
270
|
+
const top = stack[stack.length - 1];
|
|
271
|
+
if (top)
|
|
272
|
+
set({ overlays: [...stack.slice(0, -1), { id: top.id, overlay }] });
|
|
273
|
+
};
|
|
274
|
+
const closeTop = () => set({ overlays: ui.current.overlays.slice(0, -1) });
|
|
275
|
+
const refreshBoard = async () => {
|
|
276
|
+
const chronicle = ui.current.chronicle;
|
|
277
|
+
if (!chronicle)
|
|
278
|
+
return;
|
|
279
|
+
set({ board: await loadBoard(chronicle.root) });
|
|
280
|
+
};
|
|
281
|
+
const place = () => {
|
|
282
|
+
const s = ui.current;
|
|
283
|
+
return { view: s.view, open: s.open, listSel: s.listSel, fileSel: s.fileSel, query: s.query, searchMode: s.searchMode };
|
|
284
|
+
};
|
|
285
|
+
const jumpTo = (name) => {
|
|
286
|
+
const s = ui.current;
|
|
287
|
+
const history = [...s.history, place()];
|
|
288
|
+
let { filtered } = derive();
|
|
289
|
+
let index = filtered.findIndex((row) => row.name === name);
|
|
290
|
+
if (index === -1) {
|
|
291
|
+
ui.current = { ...ui.current, query: "", searchMode: "title" };
|
|
292
|
+
filtered = derive().filtered;
|
|
293
|
+
index = filtered.findIndex((row) => row.name === name);
|
|
294
|
+
}
|
|
295
|
+
details.current.delete(`${s.access.mayWrite}:${name}`);
|
|
296
|
+
set({ history, listSel: Math.max(0, index), fileSel: 0, open: null, view: "files", overlays: [] });
|
|
297
|
+
};
|
|
298
|
+
const contextFor = () => {
|
|
299
|
+
const d = derive();
|
|
300
|
+
const s = d.s;
|
|
301
|
+
if (!s.chronicle)
|
|
302
|
+
return null;
|
|
303
|
+
return {
|
|
304
|
+
view: s.view,
|
|
305
|
+
chronicle: s.chronicle,
|
|
306
|
+
rows: d.visible,
|
|
307
|
+
access: s.access,
|
|
308
|
+
episode: d.currentRow,
|
|
309
|
+
file: s.view === "file" ? s.open?.path : s.view === "files" ? d.files[clamp(s.fileSel, d.files.length)]?.relativePath : d.hit?.file,
|
|
310
|
+
reference: s.view === "file" && s.open && s.open.refSel >= 0 ? s.open.view.refs[s.open.refSel] : undefined,
|
|
311
|
+
hit: d.hit,
|
|
312
|
+
open: openOverlay,
|
|
313
|
+
replace: replaceOverlay,
|
|
314
|
+
topId,
|
|
315
|
+
status: (message) => set({ status: cleanLine(message) }),
|
|
316
|
+
jumpTo,
|
|
317
|
+
api: s.api,
|
|
318
|
+
reader: s.reader,
|
|
319
|
+
mayAnswer: s.mayAnswer,
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
const openFile = (name, file, from, line) => {
|
|
323
|
+
const s = ui.current;
|
|
324
|
+
const text = loadText(s.chronicle.root, name, file, s.access);
|
|
155
325
|
if ("error" in text)
|
|
156
|
-
return
|
|
326
|
+
return set({ status: cleanLine(text.error) });
|
|
157
327
|
const shaped = fileView(text.content, columns - 2);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
328
|
+
set({
|
|
329
|
+
open: { name, path: file, view: shaped, scroll: line ? Math.max(0, screenLineOf(shaped, line) - 2) : 0, refSel: -1, from, empty: text.content.length === 0 },
|
|
330
|
+
view: "file",
|
|
331
|
+
status: shaped.refs.length ? `${shaped.refs.length} reference(s): Tab moves between them, Enter follows.` : "",
|
|
332
|
+
});
|
|
161
333
|
};
|
|
162
|
-
const context = chronicle
|
|
163
|
-
? {
|
|
164
|
-
view,
|
|
165
|
-
chronicle,
|
|
166
|
-
episode: currentRow,
|
|
167
|
-
file: view === "file" ? open?.path : view === "files" ? files[clamp(fileSel, files.length)]?.relativePath : hit?.file,
|
|
168
|
-
reference: view === "file" && open && open.refSel >= 0 ? open.view.refs[open.refSel] : undefined,
|
|
169
|
-
hit,
|
|
170
|
-
show: setOverlay,
|
|
171
|
-
status: setStatus,
|
|
172
|
-
jumpTo: (name) => {
|
|
173
|
-
let index = filtered.findIndex((row) => row.name === name);
|
|
174
|
-
if (index === -1) {
|
|
175
|
-
setQuery("");
|
|
176
|
-
setSearchMode("title");
|
|
177
|
-
index = chronicle.rows.findIndex((row) => row.name === name);
|
|
178
|
-
}
|
|
179
|
-
setListSel(Math.max(0, index));
|
|
180
|
-
setFileSel(0);
|
|
181
|
-
setOpen(null);
|
|
182
|
-
setView("files");
|
|
183
|
-
},
|
|
184
|
-
api,
|
|
185
|
-
reader,
|
|
186
|
-
}
|
|
187
|
-
: null;
|
|
188
|
-
const bodyHeight = Math.max(5, rows - 4);
|
|
189
334
|
const move = (delta, key) => {
|
|
190
|
-
const
|
|
335
|
+
const d = derive();
|
|
336
|
+
const s = d.s;
|
|
337
|
+
const step = key.pageDown || key.pageUp ? Math.max(1, bodyHeight - 1) : 1;
|
|
191
338
|
const by = delta * step;
|
|
192
|
-
if (view === "list")
|
|
193
|
-
|
|
194
|
-
else if (view === "files")
|
|
195
|
-
|
|
196
|
-
else if (view === "hits")
|
|
197
|
-
|
|
198
|
-
else if (view === "
|
|
199
|
-
|
|
339
|
+
if (s.view === "list")
|
|
340
|
+
set({ listSel: clamp(s.listSel + by, d.filtered.length) });
|
|
341
|
+
else if (s.view === "files")
|
|
342
|
+
set({ fileSel: clamp(s.fileSel + by, d.files.length) });
|
|
343
|
+
else if (s.view === "hits")
|
|
344
|
+
set({ hitSel: clamp(s.hitSel + by, s.hits?.hits.length ?? 0) });
|
|
345
|
+
else if (s.view === "attention")
|
|
346
|
+
set({ boardSel: clamp(s.boardSel + by, d.board.length) });
|
|
347
|
+
else if (s.view === "file" && s.open)
|
|
348
|
+
set({ open: { ...s.open, scroll: clamp(s.open.scroll + by, Math.max(1, s.open.view.lines.length - bodyHeight + 1)) } });
|
|
200
349
|
};
|
|
201
350
|
const back = () => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
351
|
+
const s = ui.current;
|
|
352
|
+
const last = s.history[s.history.length - 1];
|
|
353
|
+
if (last && s.view === "files") {
|
|
354
|
+
set({ ...last, history: s.history.slice(0, -1) });
|
|
355
|
+
return;
|
|
205
356
|
}
|
|
206
|
-
|
|
207
|
-
|
|
357
|
+
if (s.view === "file" && s.open)
|
|
358
|
+
set({ view: s.open.from, open: null });
|
|
359
|
+
else if (s.view === "files" || s.view === "hits" || s.view === "attention")
|
|
360
|
+
set({ view: "list" });
|
|
361
|
+
};
|
|
362
|
+
const showItem = (item) => {
|
|
363
|
+
// The board lists open items without context; the item shows what it cites.
|
|
364
|
+
const chronicle = ui.current.chronicle;
|
|
365
|
+
const id = openOverlay({ type: "item", item });
|
|
366
|
+
loadEpisodePanel(chronicle.root, item.episode).then((panel) => {
|
|
367
|
+
const full = panel.items.find((candidate) => candidate.id === item.id);
|
|
368
|
+
if (full)
|
|
369
|
+
replaceOverlay(id, { type: "item", item: full });
|
|
370
|
+
}, () => { });
|
|
208
371
|
};
|
|
209
372
|
const forward = () => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
373
|
+
const d = derive();
|
|
374
|
+
const s = d.s;
|
|
375
|
+
if (s.view === "list" && d.currentRow) {
|
|
376
|
+
details.current.delete(`${s.access.mayWrite}:${d.currentRow.name}`);
|
|
377
|
+
set({ fileSel: 0, view: "files" });
|
|
213
378
|
}
|
|
214
|
-
else if (view === "files" && currentRow
|
|
215
|
-
const file = files[fileSel];
|
|
379
|
+
else if (s.view === "files" && d.currentRow) {
|
|
380
|
+
const file = d.files[clamp(s.fileSel, d.files.length)];
|
|
381
|
+
if (!file)
|
|
382
|
+
return;
|
|
216
383
|
if (file.kind !== "text")
|
|
217
|
-
return
|
|
218
|
-
openFile(currentRow.name, file.relativePath, "files");
|
|
384
|
+
return set({ status: `${cleanLine(file.relativePath)} is ${file.kind}: press o for its doors.` });
|
|
385
|
+
openFile(d.currentRow.name, file.relativePath, "files");
|
|
219
386
|
}
|
|
220
|
-
else if (view === "hits" && hit)
|
|
221
|
-
openFile(hit.episodePath, hit.file, "hits", hit.line);
|
|
387
|
+
else if (s.view === "hits" && d.hit)
|
|
388
|
+
openFile(d.hit.episodePath, d.hit.file, "hits", d.hit.line);
|
|
389
|
+
else if (s.view === "attention" && d.boardItem)
|
|
390
|
+
showItem(d.boardItem);
|
|
222
391
|
};
|
|
392
|
+
const answerInput = (item) => ({
|
|
393
|
+
type: "input",
|
|
394
|
+
title: `Answer · ${cleanLine(item.id)}`,
|
|
395
|
+
hint: cleanLine(item.question),
|
|
396
|
+
value: "",
|
|
397
|
+
submit: async (value) => {
|
|
398
|
+
const checked = checkAnswer(value);
|
|
399
|
+
if (!checked.ok)
|
|
400
|
+
throw new Error(checked.message);
|
|
401
|
+
const result = await answerAttention(ui.current.api, item.episode, item.id, checked.answer);
|
|
402
|
+
return answeredMessage(result.result).message;
|
|
403
|
+
},
|
|
404
|
+
after: async () => {
|
|
405
|
+
await refreshBoard();
|
|
406
|
+
// The item and the panel under it show the recorded answer.
|
|
407
|
+
const chronicle = ui.current.chronicle;
|
|
408
|
+
const panel = await loadEpisodePanel(chronicle.root, item.episode);
|
|
409
|
+
const stack = ui.current.overlays.map((entry) => {
|
|
410
|
+
const o = entry.overlay;
|
|
411
|
+
if (o.type === "item" && o.item.episode === item.episode && o.item.id === item.id) {
|
|
412
|
+
const fresh = panel.items.find((candidate) => candidate.id === item.id);
|
|
413
|
+
return fresh ? { id: entry.id, overlay: { type: "item", item: fresh } } : entry;
|
|
414
|
+
}
|
|
415
|
+
if (o.type === "panel" && o.episode === item.episode)
|
|
416
|
+
return { id: entry.id, overlay: { ...o, items: panel.items } };
|
|
417
|
+
return entry;
|
|
418
|
+
});
|
|
419
|
+
set({ overlays: stack });
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
// ── keys ─────────────────────────────────────────────────────────────────
|
|
223
423
|
useInput((input, key) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
424
|
+
if (key.ctrl || key.meta)
|
|
425
|
+
return;
|
|
426
|
+
// Several keys in one read: each character is its own press (UI review, F13).
|
|
427
|
+
const special = key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.pageUp || key.pageDown || key.return || key.escape || key.tab || key.backspace || key.delete || key.home || key.end;
|
|
428
|
+
if (!special && input.length > 1) {
|
|
429
|
+
for (const char of input) {
|
|
430
|
+
if (char === "\t")
|
|
431
|
+
handleKey("", { ...PLAIN_KEY, tab: true });
|
|
432
|
+
else if (char === "\r" || char === "\n")
|
|
433
|
+
handleKey("", { ...PLAIN_KEY, return: true });
|
|
434
|
+
else if (char === "\u007f" || char === "\b")
|
|
435
|
+
handleKey("", { ...PLAIN_KEY, backspace: true });
|
|
436
|
+
else
|
|
437
|
+
handleKey(char, PLAIN_KEY);
|
|
438
|
+
}
|
|
229
439
|
return;
|
|
230
440
|
}
|
|
231
441
|
handleKey(input, key);
|
|
232
442
|
});
|
|
233
443
|
function handleKey(input, key) {
|
|
234
|
-
|
|
444
|
+
const s = ui.current;
|
|
445
|
+
if (!s.chronicle) {
|
|
235
446
|
if (input === "q" || key.escape)
|
|
236
447
|
exit();
|
|
237
448
|
return;
|
|
238
449
|
}
|
|
239
|
-
|
|
240
|
-
|
|
450
|
+
const top = s.overlays[s.overlays.length - 1]?.overlay;
|
|
451
|
+
if (top?.type === "input") {
|
|
452
|
+
if (top.busy)
|
|
241
453
|
return;
|
|
242
454
|
if (key.escape)
|
|
243
|
-
return
|
|
455
|
+
return closeTop();
|
|
244
456
|
if (key.return) {
|
|
245
|
-
const value =
|
|
457
|
+
const value = top.value;
|
|
246
458
|
if (!value.trim())
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
459
|
+
return updateTop({ ...top, error: "Nothing to send yet." });
|
|
460
|
+
updateTop({ ...top, busy: true, error: undefined });
|
|
461
|
+
top.submit(value).then(async (message) => {
|
|
462
|
+
set({ status: cleanLine(message), overlays: ui.current.overlays.slice(0, -1) });
|
|
463
|
+
try {
|
|
464
|
+
await top.after?.();
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
set({ status: cleanLine(`Sent, but the refresh failed: ${error.message}`) });
|
|
468
|
+
}
|
|
469
|
+
}, (error) => updateTop({ ...top, busy: false, error: cleanLine(error.message) }));
|
|
254
470
|
return;
|
|
255
471
|
}
|
|
256
472
|
if (key.backspace || key.delete)
|
|
257
|
-
return
|
|
258
|
-
const text = input.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "")
|
|
259
|
-
if (text
|
|
260
|
-
|
|
473
|
+
return updateTop({ ...top, value: top.value.slice(0, -1), error: undefined });
|
|
474
|
+
const text = input.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
|
475
|
+
if (text)
|
|
476
|
+
updateTop({ ...top, value: top.value + text, error: undefined });
|
|
261
477
|
return;
|
|
262
478
|
}
|
|
263
|
-
if (
|
|
264
|
-
if (
|
|
265
|
-
return
|
|
266
|
-
if (
|
|
267
|
-
|
|
479
|
+
if (top) {
|
|
480
|
+
if (top.type === "help")
|
|
481
|
+
return closeTop();
|
|
482
|
+
if (key.escape || input === "q" || input === "h" || key.backspace || key.leftArrow)
|
|
483
|
+
return closeTop();
|
|
484
|
+
if (top.type === "doors") {
|
|
485
|
+
const count = top.doors.doors.length;
|
|
268
486
|
if (key.downArrow || input === "j")
|
|
269
|
-
return
|
|
487
|
+
return updateTop({ ...top, selected: clamp(top.selected + 1, count) });
|
|
270
488
|
if (key.upArrow || input === "k")
|
|
271
|
-
return
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (!overlay.canAnswer || !api)
|
|
277
|
-
return setStatus(`Answering needs the Miadi app and ${TOKEN_ENV} (or --env <file>).`);
|
|
278
|
-
const back = overlay;
|
|
279
|
-
return setOverlay({
|
|
280
|
-
type: "input",
|
|
281
|
-
title: `Answer · ${item.id}`,
|
|
282
|
-
back,
|
|
283
|
-
hint: item.question,
|
|
284
|
-
value: "",
|
|
285
|
-
submit: async (value) => {
|
|
286
|
-
await answerAttention(api, back.episode, item.id, value.trim());
|
|
287
|
-
return `Answered ${item.id} in ${rowByName(back.episode)?.number ?? back.episode}.`;
|
|
288
|
-
},
|
|
289
|
-
after: async () => {
|
|
290
|
-
const fresh = await attentionOverlay(context, back.episode);
|
|
291
|
-
setOverlay(fresh.type === "attention" ? { ...fresh, selected: back.selected } : fresh);
|
|
292
|
-
},
|
|
293
|
-
});
|
|
489
|
+
return updateTop({ ...top, selected: clamp(top.selected - 1, count) });
|
|
490
|
+
const door = top.doors.doors[top.selected];
|
|
491
|
+
if (door && key.return) {
|
|
492
|
+
const result = openUrl(door.url, s.chronicle.root, withheld);
|
|
493
|
+
return set({ status: "opened" in result ? `Opened ${cleanLine(door.label)} with ${result.opened}.` : `Not opened: ${result.refused}. The address: ${cleanLine(door.url)}` });
|
|
294
494
|
}
|
|
495
|
+
if (door && input === "r")
|
|
496
|
+
return void openOverlay({ type: "qr", title: top.doors.title, door, invert: false });
|
|
295
497
|
return;
|
|
296
498
|
}
|
|
297
|
-
if (
|
|
298
|
-
return
|
|
299
|
-
if (
|
|
300
|
-
|
|
499
|
+
if (top.type === "qr" && input === "i")
|
|
500
|
+
return updateTop({ ...top, invert: !top.invert });
|
|
501
|
+
if (top.type === "card" && input === "o")
|
|
502
|
+
return void openOverlay({ type: "doors", doors: top.doors, selected: 0 });
|
|
503
|
+
if (top.type === "panel") {
|
|
504
|
+
const { shown, view } = panelView(top.items, top.view, ROOM_LABELS, top.parseError);
|
|
505
|
+
if (key.tab)
|
|
506
|
+
return updateTop({ ...top, view: view === "history" ? "open" : "history", selected: 0 });
|
|
301
507
|
if (key.downArrow || input === "j")
|
|
302
|
-
return
|
|
508
|
+
return updateTop({ ...top, selected: clamp(top.selected + 1, shown.length) });
|
|
303
509
|
if (key.upArrow || input === "k")
|
|
304
|
-
return
|
|
305
|
-
const
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
510
|
+
return updateTop({ ...top, selected: clamp(top.selected - 1, shown.length) });
|
|
511
|
+
const item = shown[clamp(top.selected, shown.length)];
|
|
512
|
+
if (item && (key.return || input === "l"))
|
|
513
|
+
return void openOverlay({ type: "item", item });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (top.type === "item") {
|
|
517
|
+
if (key.return && top.item.state === "open") {
|
|
518
|
+
if (!s.mayAnswer || !s.api)
|
|
519
|
+
return set({ status: readOnlyLabel(TOKEN_ENV) });
|
|
520
|
+
return void openOverlay(answerInput(top.item));
|
|
521
|
+
}
|
|
522
|
+
if (input === "e") {
|
|
523
|
+
set({ overlays: [] });
|
|
524
|
+
return jumpTo(top.item.episode);
|
|
309
525
|
}
|
|
310
|
-
if (door && input === "r")
|
|
311
|
-
return setOverlay({ type: "qr", title: overlay.doors.title, door, invert: false });
|
|
312
526
|
}
|
|
313
|
-
if (overlay.type === "qr" && input === "i")
|
|
314
|
-
return setOverlay({ ...overlay, invert: !overlay.invert });
|
|
315
|
-
if (overlay.type === "help")
|
|
316
|
-
return setOverlay(null);
|
|
317
527
|
return;
|
|
318
528
|
}
|
|
319
|
-
if (inputMode) {
|
|
529
|
+
if (s.inputMode) {
|
|
320
530
|
if (key.escape || key.return)
|
|
321
|
-
return
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return;
|
|
531
|
+
return set({ inputMode: false });
|
|
532
|
+
if (key.tab) {
|
|
533
|
+
// The view follows the mode (UI review, F7).
|
|
534
|
+
const searchMode = s.searchMode === "title" ? "full" : "title";
|
|
535
|
+
const view = s.view === "attention" ? "attention" : searchMode === "title" ? "list" : s.hits ? "hits" : "list";
|
|
536
|
+
set({ searchMode, view });
|
|
537
|
+
return scheduleSearch();
|
|
538
|
+
}
|
|
328
539
|
if (key.upArrow || key.downArrow)
|
|
329
540
|
return move(key.downArrow ? 1 : -1, key);
|
|
330
541
|
if (key.backspace || key.delete) {
|
|
331
|
-
|
|
332
|
-
return
|
|
542
|
+
set({ query: s.query.slice(0, -1), listSel: 0, boardSel: 0 });
|
|
543
|
+
return scheduleSearch();
|
|
333
544
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
545
|
+
const text = input.replace(/[\u0000-\u001f\u007f]/g, "");
|
|
546
|
+
if (text) {
|
|
547
|
+
set({ query: s.query + text, listSel: 0, boardSel: 0 });
|
|
548
|
+
scheduleSearch();
|
|
337
549
|
}
|
|
338
550
|
return;
|
|
339
551
|
}
|
|
340
|
-
if (input === "/" && view !== "file") {
|
|
341
|
-
|
|
342
|
-
setView("list");
|
|
343
|
-
return setInputMode(true);
|
|
552
|
+
if (input === "/" && s.view !== "file") {
|
|
553
|
+
return set({ inputMode: true, view: s.view === "files" ? "list" : s.view });
|
|
344
554
|
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
555
|
+
if (input === "!")
|
|
556
|
+
return set({ view: s.view === "attention" ? "list" : "attention", boardSel: 0 });
|
|
557
|
+
if (key.tab && s.view === "file" && s.open) {
|
|
558
|
+
const count = s.open.view.refs.length;
|
|
347
559
|
if (count === 0)
|
|
348
560
|
return;
|
|
349
|
-
const refSel = key.shift ? (open.refSel <= 0 ? count - 1 : open.refSel - 1) : (open.refSel + 1) % count;
|
|
350
|
-
const ref = open.view.refs[refSel];
|
|
351
|
-
const line = open.view.lines.findIndex((screen) => screen.segments.some((
|
|
352
|
-
const scroll = line < open.scroll || line >= open.scroll + bodyHeight ? Math.max(0, line - 2) : open.scroll;
|
|
353
|
-
|
|
354
|
-
return setOpen({ ...open, refSel, scroll });
|
|
561
|
+
const refSel = key.shift ? (s.open.refSel <= 0 ? count - 1 : s.open.refSel - 1) : (s.open.refSel + 1) % count;
|
|
562
|
+
const ref = s.open.view.refs[refSel];
|
|
563
|
+
const line = s.open.view.lines.findIndex((screen) => screen.segments.some((segment) => segment.ref === refSel));
|
|
564
|
+
const scroll = line < s.open.scroll || line >= s.open.scroll + bodyHeight ? Math.max(0, line - 2) : s.open.scroll;
|
|
565
|
+
return set({ open: { ...s.open, refSel, scroll }, status: cleanLine(`${ref.kind} · ${ref.uri}`) });
|
|
355
566
|
}
|
|
356
|
-
if (key.tab && (view === "list" || view === "hits")) {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
setView("list");
|
|
361
|
-
else if (hits && query.trim().length >= 2)
|
|
362
|
-
setView("hits");
|
|
363
|
-
return;
|
|
567
|
+
if (key.tab && (s.view === "list" || s.view === "hits")) {
|
|
568
|
+
const searchMode = s.searchMode === "title" ? "full" : "title";
|
|
569
|
+
set({ searchMode, view: searchMode === "title" ? "list" : s.hits ? "hits" : "list" });
|
|
570
|
+
return scheduleSearch();
|
|
364
571
|
}
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
572
|
+
const context = contextFor();
|
|
573
|
+
const action = context ? actionFor(actions, input, key, context) : undefined;
|
|
574
|
+
if (action && context) {
|
|
575
|
+
Promise.resolve(action.run(context)).catch((error) => set({ status: cleanLine(error.message) }));
|
|
368
576
|
return;
|
|
369
577
|
}
|
|
370
578
|
if (key.downArrow || input === "j" || key.pageDown)
|
|
@@ -372,103 +580,169 @@ export function App({ root, query: initialQuery = "", search: initialSearch, epi
|
|
|
372
580
|
if (key.upArrow || input === "k" || key.pageUp)
|
|
373
581
|
return move(-1, key);
|
|
374
582
|
if (key.home || input === "g")
|
|
375
|
-
return move(-
|
|
583
|
+
return move(-1_000_000, PLAIN_KEY);
|
|
376
584
|
if (key.end || input === "G")
|
|
377
|
-
return move(
|
|
585
|
+
return move(1_000_000, PLAIN_KEY);
|
|
378
586
|
if (key.return || key.rightArrow || input === "l")
|
|
379
587
|
return forward();
|
|
380
588
|
if (key.escape || key.leftArrow || input === "h" || key.backspace)
|
|
381
589
|
return back();
|
|
382
590
|
if (input === "?")
|
|
383
|
-
return
|
|
591
|
+
return void openOverlay({ type: "help" });
|
|
384
592
|
if (input === "q")
|
|
385
593
|
exit();
|
|
386
594
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const compact = columns < 90;
|
|
595
|
+
// ── render ───────────────────────────────────────────────────────────────
|
|
596
|
+
const d = derive();
|
|
597
|
+
const s = d.s;
|
|
598
|
+
if (s.loadError)
|
|
599
|
+
return _jsxs(Text, { color: "red", children: ["miadi-chronicle: ", cleanLine(s.loadError)] });
|
|
600
|
+
if (!s.chronicle)
|
|
601
|
+
return _jsxs(Text, { children: ["Reading the chronicle at ", cleanLine(root), "\u2026"] });
|
|
602
|
+
const context = contextFor();
|
|
396
603
|
const leftWidth = compact ? columns : Math.min(64, Math.max(36, Math.floor(columns * 0.4)));
|
|
397
604
|
const rightWidth = compact ? columns : Math.max(20, columns - leftWidth - 1);
|
|
398
605
|
const available = actions.filter((action) => action.available(context));
|
|
399
|
-
const numberWidth = String(Math.max(0, ...chronicle.rows.map((row) => row.number ?? 0))).length;
|
|
606
|
+
const numberWidth = String(Math.max(0, ...s.chronicle.rows.map((row) => row.number ?? 0))).length;
|
|
400
607
|
const showAge = columns >= 44;
|
|
401
|
-
const ageWidth = Math.max(2, ...chronicle.rows.map((row) => ageOf(row.date).length));
|
|
402
|
-
const rowLine = (row, selected, marked) => (_jsxs(Text, { wrap: "truncate-end", inverse: selected, bold: marked, children: [_jsx(Text, { color: "yellow", children: String(row.number ?? "·").padStart(numberWidth) }), " ", _jsx(Text, { color: DIRECTION_COLOR[row.readings.direction], children: DIRECTION_MARK[row.readings.direction] }), " ", showAge ? _jsxs(Text, { color: "gray", children: [ageOf(row.date).padStart(ageWidth), " "] }) : null, row.structured ? row.title : _jsx(Text, { italic: true, color: "gray", children: row.slug ?? row.name })] }, row.name));
|
|
403
|
-
const selectedRow = clamp(listSel, filtered.length);
|
|
404
|
-
const selected = filtered[selectedRow];
|
|
405
|
-
// A compact list ends with the selected episode's whole title
|
|
406
|
-
// rows above can only show cut.
|
|
608
|
+
const ageWidth = Math.max(2, ...s.chronicle.rows.map((row) => ageOf(row.date).length));
|
|
609
|
+
const rowLine = (row, selected, marked) => (_jsxs(Text, { wrap: "truncate-end", inverse: selected, bold: marked, children: [_jsx(Text, { color: "yellow", children: String(row.number ?? "·").padStart(numberWidth) }), " ", _jsx(Text, { color: DIRECTION_COLOR[row.readings.direction], children: DIRECTION_MARK[row.readings.direction] }), " ", showAge ? _jsxs(Text, { color: "gray", children: [ageOf(row.date).padStart(ageWidth), " "] }) : null, row.structured ? row.title : _jsx(Text, { italic: true, color: "gray", children: row.slug ?? row.name }), row.audience === "community" ? _jsx(Text, { color: "magenta", children: " \u25C7" }) : null] }, row.name));
|
|
610
|
+
const selectedRow = clamp(s.listSel, d.filtered.length);
|
|
611
|
+
const selected = d.filtered[selectedRow];
|
|
612
|
+
// A compact list ends with the selected episode's whole title.
|
|
407
613
|
const peekLines = compact && selected ? 3 : 0;
|
|
408
|
-
const listWindow = windowOf(filtered, selectedRow, bodyHeight - peekLines);
|
|
409
|
-
const listPane = (_jsxs(Box, { flexDirection: "column", width: leftWidth, children: [listWindow.items.map((row, index) => {
|
|
614
|
+
const listWindow = windowOf(d.filtered, selectedRow, Math.max(1, bodyHeight - peekLines));
|
|
615
|
+
const listPane = (_jsxs(Box, { flexDirection: "column", width: leftWidth, height: bodyHeight, overflow: "hidden", flexShrink: 0, children: [listWindow.items.map((row, index) => {
|
|
410
616
|
const isSelected = listWindow.start + index === selectedRow;
|
|
411
|
-
return rowLine(row, isSelected && view === "list", isSelected && view === "files");
|
|
412
|
-
}), filtered.length === 0 ? _jsxs(Text, { color: "gray", children: ["No episode matches \u201C", query, "\u201D."] }) : null, peekLines && selected ? (_jsxs(Box, { flexDirection: "column", marginTop: Math.max(0, bodyHeight - peekLines - listWindow.items.length), children: [_jsx(Box, { height: 2, overflow: "hidden", children: _jsx(Text, { bold: true, wrap: "wrap", children: selected.title }) }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [_jsxs(Text, { color: "green", children: [selected.readings.direction, " \u00B7 ", selected.readings.theme] }), " \u00B7 ", formatDate(selected.date), selected.status ? ` · ${selected.status}` : ""] })] })) : null] }));
|
|
617
|
+
return rowLine(row, isSelected && s.view === "list", isSelected && s.view === "files");
|
|
618
|
+
}), d.filtered.length === 0 ? _jsxs(Text, { color: "gray", children: ["No episode matches \u201C", cleanLine(s.query), "\u201D."] }) : null, peekLines && selected ? (_jsxs(Box, { flexDirection: "column", marginTop: Math.max(0, bodyHeight - peekLines - listWindow.items.length), children: [_jsx(Box, { height: 2, overflow: "hidden", children: _jsx(Text, { bold: true, wrap: "wrap", children: selected.title }) }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [_jsxs(Text, { color: "green", children: [selected.readings.direction, " \u00B7 ", selected.readings.theme] }), " \u00B7 ", formatDate(selected.date), selected.status ? ` · ${selected.status}` : ""] })] })) : null] }));
|
|
413
619
|
const hitRows = compact ? 2 : 1;
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
const
|
|
419
|
-
|
|
620
|
+
const hitsTop = (s.searching ? 1 : 0) + (s.hits ? 1 : 0);
|
|
621
|
+
const hitWindow = s.hits ? windowOf(s.hits.hits, s.hitSel, Math.max(1, Math.floor((bodyHeight - hitsTop) / hitRows))) : null;
|
|
622
|
+
const hitsPane = (_jsxs(Box, { flexDirection: "column", width: leftWidth, height: bodyHeight, overflow: "hidden", flexShrink: 0, children: [s.searching ? _jsx(Text, { color: "gray", children: "Searching\u2026" }) : null, s.hits ? (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\u201C", cleanLine(s.hits.query), "\u201D ", s.hits.hits.length, s.hits.truncated ? "+" : "", " hits \u00B7 ", new Set(s.hits.hits.map((h) => h.episodePath)).size, " episodes \u00B7 newest first \u00B7 ", s.hits.engine, s.hits.partial ? " · some files unreadable" : ""] })) : null, hitWindow?.items.map((item, index) => {
|
|
623
|
+
const number = d.rowByName(item.episodePath)?.number;
|
|
624
|
+
const isSelected = hitWindow.start + index === s.hitSel;
|
|
625
|
+
const where = (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: String(number ?? "·").padStart(numberWidth) }), " ", _jsxs(Text, { color: "blue", children: [cleanLine(item.file), ":", item.line] })] }));
|
|
626
|
+
return compact ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "truncate-middle", inverse: isSelected, children: where }), _jsxs(Text, { wrap: "truncate-end", color: isSelected ? undefined : "gray", children: [" ", cleanLine(item.text).trim()] })] }, `${item.episodePath}/${item.file}:${item.line}`)) : (_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, children: [where, " ", cleanLine(item.text).trim()] }, `${item.episodePath}/${item.file}:${item.line}`));
|
|
420
627
|
})] }));
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
})] })) : (
|
|
429
|
-
|
|
628
|
+
const boardRows = compact ? 2 : 1;
|
|
629
|
+
const boardWindow = windowOf(d.board, clamp(s.boardSel, d.board.length), Math.max(1, Math.floor((bodyHeight - 1) / boardRows)));
|
|
630
|
+
const boardHeading = panelView(d.board, "open", BOARD_LABELS);
|
|
631
|
+
const boardPane = (_jsxs(Box, { flexDirection: "column", width: leftWidth, height: bodyHeight, overflow: "hidden", flexShrink: 0, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "yellow", children: boardHeading.heading }), " ", _jsx(Text, { color: "gray", children: boardHeading.meta })] }), s.board === null ? _jsx(Text, { color: "gray", children: "Reading attention\u2026" }) : null, s.board !== null && d.board.length === 0 && d.openCount > 0 ? (_jsxs(Text, { color: "gray", children: ["No open item matches this search \u2014 ", d.openCount, " still wait across the chronicle."] })) : null, boardWindow.items.map((item, index) => {
|
|
632
|
+
const isSelected = boardWindow.start + index === clamp(s.boardSel, d.board.length);
|
|
633
|
+
const number = d.rowByName(item.episode)?.number;
|
|
634
|
+
const head = (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "?" }), " ", _jsx(Text, { color: "yellow", children: String(number ?? "·").padStart(numberWidth) }), " ", cleanLine(item.id), _jsxs(Text, { color: "gray", children: [" \u00B7 ", item.asked ?? ""] })] }));
|
|
635
|
+
return compact ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "truncate-end", inverse: isSelected, children: head }), _jsxs(Text, { wrap: "truncate-end", color: isSelected ? undefined : "gray", children: [" ", cleanLine(item.question)] })] }, `${item.episode}:${item.id}`)) : (_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, children: [head, " ", _jsx(Text, { children: cleanLine(item.question) })] }, `${item.episode}:${item.id}`));
|
|
636
|
+
})] }));
|
|
637
|
+
const itemBody = (item, width) => (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, wrap: "wrap", children: clean(item.question) }), item.unlocks ? _jsxs(Text, { wrap: "wrap", color: "green", children: ["unlocks: ", clean(item.unlocks)] }) : null, _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [cleanLine(item.id), " \u00B7 ", d.rowByName(item.episode)?.number ?? "", " ", cleanLine(d.rowByName(item.episode)?.title ?? item.episode), item.asked ? ` · asked ${item.asked}` : ""] }), item.state === "answered" ? (_jsxs(Text, { wrap: "wrap", color: "cyan", children: ["answered", item.answered_at ? ` ${item.answered_at.slice(0, 10)}` : "", ": ", clean(item.answer ?? "")] })) : null, (item.context ?? []).length ? _jsx(Text, { color: "gray", children: ROOM_LABELS.context }) : null, (item.context ?? []).map((cited) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "blue", wrap: "truncate-middle", children: ["\u203A ", cleanLine(cited.ref), cited.exists ? "" : " (missing)"] }), cited.excerpt ? (_jsx(Box, { height: Math.min(6, clean(cited.excerpt).split("\n").length), overflow: "hidden", width: width, children: _jsx(Text, { wrap: "wrap", color: "gray", children: clean(cited.excerpt) }) })) : null] }, cited.ref)))] }));
|
|
638
|
+
const goal = d.detail?.summary?.goal ? cleanLine(d.detail.summary.goal.replace(/^\s*(["'])([\s\S]*)\1\s*$/, "$2")).replace(/\s+/g, " ").trim() : "";
|
|
639
|
+
const openHere = d.currentRow ? (s.board ?? []).filter((item) => item.episode === d.currentRow.name).length : 0;
|
|
640
|
+
const textWidth = Math.max(10, rightWidth - (compact ? 0 : 1));
|
|
641
|
+
const linesOf = (text, cap) => Math.min(cap, Math.max(1, wrapSegments([{ text, style: "text" }], textWidth).length));
|
|
642
|
+
const titleLines = d.currentRow ? linesOf(`${d.currentRow.number ?? "·"} ${d.currentRow.title}`, 3) : 0;
|
|
643
|
+
const goalLines = goal ? linesOf(goal, compact ? 6 : 8) : 0;
|
|
644
|
+
const fixedLines = titleLines + 2 + (openHere ? 1 : 0) + goalLines + (d.detail?.error ? 1 : 0) + 1;
|
|
645
|
+
const fileWindow = windowOf(d.files, clamp(s.fileSel, d.files.length), Math.max(1, bodyHeight - fixedLines));
|
|
646
|
+
const detailPane = (_jsx(Box, { flexDirection: "column", width: rightWidth, height: bodyHeight, overflow: "hidden", paddingLeft: compact ? 0 : 1, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: s.view === "attention" ? (d.boardItem ? itemBody(d.boardItem, rightWidth - 1) : _jsx(Text, { color: "gray", children: "Nothing selected." })) : d.currentRow ? (_jsxs(_Fragment, { children: [_jsx(Box, { height: titleLines, overflow: "hidden", flexShrink: 0, children: _jsxs(Text, { bold: true, wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: d.currentRow.number ?? "·" }), " ", d.currentRow.title] }) }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [[formatDate(d.currentRow.date), d.currentRow.status, d.currentRow.type].filter(Boolean).join(" · "), d.currentRow.structured ? "" : " · no episode.yaml", d.currentRow.audience === "community" ? " · community" : ""] }), _jsxs(Text, { color: "green", wrap: "truncate-end", children: ["reading: ", d.currentRow.readings.direction, " \u00B7 ", d.currentRow.readings.theme, " \u00B7 ", d.currentRow.readings.arc] }), openHere ? _jsxs(Text, { color: "yellow", children: ["? ", openHere, " open question", openHere === 1 ? "" : "s", " \u00B7 a"] }) : null, goal ? (_jsx(Box, { height: goalLines, overflow: "hidden", flexShrink: 0, children: _jsx(Text, { wrap: "wrap", children: goal }) })) : null, d.detail?.error ? _jsx(Text, { color: "red", children: cleanLine(d.detail.error) }) : null, _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["── ", d.files.length, d.files.length >= MAX_FILES ? "+" : "", " file", d.files.length === 1 ? "" : "s", s.view === "files" ? "" : " · Enter", " ──"] }), fileWindow.items.map((file, index) => {
|
|
647
|
+
const isSelected = s.view === "files" && fileWindow.start + index === clamp(s.fileSel, d.files.length);
|
|
648
|
+
return (_jsxs(Text, { wrap: "truncate-start", inverse: isSelected, color: file.kind === "text" ? "blue" : "gray", children: [cleanLine(file.relativePath), file.kind === "text" ? "" : ` · ${file.kind}`] }, file.relativePath));
|
|
649
|
+
})] })) : (_jsx(Text, { color: "gray", children: "Nothing selected." })) }) }));
|
|
650
|
+
const fileBody = s.open ? (_jsxs(Box, { flexDirection: "column", height: bodyHeight, overflow: "hidden", children: [s.open.empty ? _jsx(Text, { color: "gray", children: "(empty file)" }) : null, s.open.view.lines.slice(s.open.scroll, s.open.scroll + bodyHeight).map((line, index) => (_jsx(Segments, { segments: line.segments, selectedRef: s.open.refSel }, s.open.scroll + index)))] })) : null;
|
|
651
|
+
// Children never shrink: a fixed-height box clips what does not fit instead
|
|
652
|
+
// of squeezing rows to zero height, where Ink draws them over each other.
|
|
653
|
+
const box = (color, children) => (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingLeft: 1, paddingRight: 1, flexGrow: 1, height: bodyHeight, overflow: "hidden", children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: children }) }));
|
|
430
654
|
const overlayBody = (() => {
|
|
431
|
-
|
|
655
|
+
const top = s.overlays[s.overlays.length - 1]?.overlay;
|
|
656
|
+
if (!top)
|
|
432
657
|
return null;
|
|
433
|
-
if (
|
|
434
|
-
return (
|
|
658
|
+
if (top.type === "doors") {
|
|
659
|
+
return box("yellow", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Doors \u00B7 ", cleanLine(top.doors.title)] }), top.doors.note ? _jsx(Text, { color: "gray", wrap: "wrap", children: cleanLine(top.doors.note) }) : null, top.doors.doors.map((door, index) => compact ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { inverse: index === top.selected, color: "yellow", children: cleanLine(door.label) }), _jsxs(Text, { wrap: "truncate-end", children: [" ", link(door.url)] })] }, `${door.label}-${index}`)) : (_jsxs(Text, { wrap: "truncate-end", inverse: index === top.selected, children: [_jsx(Text, { color: "yellow", children: cleanLine(door.label).padEnd(16) }), " ", link(door.url)] }, `${door.label}-${index}`))), _jsx(Text, { color: "gray", children: "\u2191\u2193 \u00B7 Enter opens \u00B7 r QR \u00B7 Esc" })] }));
|
|
435
660
|
}
|
|
436
|
-
if (
|
|
437
|
-
|
|
661
|
+
if (top.type === "qr") {
|
|
662
|
+
const lines = qrLines(top.door.url, top.invert);
|
|
663
|
+
const local = /^https?:\/\/(127\.0\.0\.1|localhost)[:/]/.test(top.door.url) || top.door.url.startsWith("file:");
|
|
664
|
+
// A QR cut by the screen does not scan: say so instead (UI review, F9).
|
|
665
|
+
const room = bodyHeight - 2;
|
|
666
|
+
if (lines.length > room) {
|
|
667
|
+
return box("yellow", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, children: ["QR \u00B7 ", cleanLine(top.door.label)] }), _jsxs(Text, { wrap: "wrap", color: "yellow", children: ["This code needs ", lines.length + 2, " rows and the screen has ", bodyHeight, ". Enlarge the terminal, or open the address:"] }), _jsx(Text, { wrap: "wrap", children: link(top.door.url) }), _jsx(Text, { color: "gray", children: "Esc" })] }));
|
|
668
|
+
}
|
|
669
|
+
const spare = room - lines.length;
|
|
670
|
+
return box("yellow", _jsxs(_Fragment, { children: [spare >= 1 ? _jsxs(Text, { bold: true, wrap: "truncate-end", children: ["QR \u00B7 ", cleanLine(top.door.label)] }) : null, lines.map((line, index) => (_jsx(Text, { children: line }, index))), spare >= 2 ? _jsx(Text, { wrap: "truncate-end", children: link(top.door.url) }) : null, local && spare >= 4 ? (_jsx(Text, { color: "yellow", wrap: "wrap", children: "This address opens only on this machine. Set MIADI_URL_BASE_INTERNAL for a room a phone can open." })) : null, spare >= 3 ? _jsx(Text, { color: "gray", children: "i inverts \u00B7 Esc" }) : null] }));
|
|
438
671
|
}
|
|
439
|
-
if (
|
|
440
|
-
return (_jsxs(
|
|
672
|
+
if (top.type === "proof") {
|
|
673
|
+
return box("yellow", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Proof \u00B7 ", d.rowByName(top.episode)?.number ?? cleanLine(top.episode)] }), !top.status && !top.error ? _jsx(Text, { color: "gray", children: "Asking git and the wheel\u2026" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, top.status?.closing.map((stage) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: stage.state === "proven" ? "green" : stage.state === "owed" ? "yellow" : "red", children: [stage.state === "proven" ? "✓" : stage.state === "owed" ? "…" : "✗", " ", stage.stage] }), compact ? null : _jsxs(Text, { color: "gray", children: [" ", cleanLine(stage.observed)] })] }), compact ? _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [" ", cleanLine(stage.observed)] }) : null] }, stage.stage))), top.status?.drift.map((line) => (_jsxs(Text, { color: "yellow", wrap: "wrap", children: ["drift: ", cleanLine(line)] }, line))), _jsx(Text, { color: "gray", children: "Esc" })] }));
|
|
441
674
|
}
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingLeft: 1, paddingRight: 1, flexGrow: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Attention \u00B7 ", rowByName(overlay.episode)?.number ?? overlay.episode, " ", _jsxs(Text, { color: "gray", children: [overlay.items.filter((item) => item.state === "open").length, " open \u00B7 ", overlay.source === "app" ? "from the app" : "from disk"] })] }), itemWindow.items.map((item, index) => {
|
|
445
|
-
const selected = itemWindow.start + index === overlay.selected;
|
|
446
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", inverse: selected, children: [_jsx(Text, { color: item.state === "open" ? "yellow" : "green", children: item.state === "open" ? "?" : "✓" }), " ", item.id] }), _jsx(Box, { height: compact ? 2 : 1, overflow: "hidden", children: _jsxs(Text, { wrap: "wrap", color: selected ? undefined : "gray", children: [" ", item.state === "open" ? item.question : item.answer ?? item.question] }) })] }, item.id));
|
|
447
|
-
}), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\u2191\u2193 \u00B7 ", overlay.canAnswer ? "Enter answers an open item" : `answering needs ${TOKEN_ENV}`, " \u00B7 Esc"] })] }));
|
|
675
|
+
if (top.type === "input") {
|
|
676
|
+
return box("cyan", _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: cleanLine(top.title) }), top.hint ? _jsx(Text, { color: "gray", wrap: "wrap", children: cleanLine(top.hint) }) : null, _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "cyan", children: "\u203A " }), cleanLine(top.value), top.busy ? null : _jsx(Text, { inverse: true, children: " " })] }), top.busy ? _jsx(Text, { color: "gray", children: "Sending\u2026" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, _jsx(Text, { color: "gray", children: "Enter sends \u00B7 Esc cancels" })] }));
|
|
448
677
|
}
|
|
449
|
-
if (
|
|
450
|
-
const
|
|
451
|
-
|
|
678
|
+
if (top.type === "panel") {
|
|
679
|
+
const view = panelView(top.items, top.view, ROOM_LABELS, top.parseError);
|
|
680
|
+
const panelWindow = windowOf(view.shown, clamp(top.selected, view.shown.length), Math.max(1, Math.floor((bodyHeight - 5) / 2)));
|
|
681
|
+
return box("yellow", _jsxs(_Fragment, { children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: view.heading }), " ", _jsxs(Text, { color: "gray", children: ["\u00B7 ", d.rowByName(top.episode)?.number ?? cleanLine(top.episode), " \u00B7 ", view.meta] })] }), _jsxs(Text, { children: [_jsx(Text, { color: view.view === "open" ? "yellow" : "gray", children: ROOM_LABELS.openTab }), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: view.view === "history" ? "yellow" : "gray", children: ROOM_LABELS.historyTab })] }), panelWindow.items.map((item, index) => {
|
|
682
|
+
const isSelected = panelWindow.start + index === clamp(top.selected, view.shown.length);
|
|
683
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, children: [_jsx(Text, { color: item.state === "open" ? "yellow" : "green", children: item.state === "open" ? "?" : "✓" }), " ", cleanLine(item.id)] }), _jsxs(Text, { wrap: "truncate-end", color: isSelected ? undefined : "gray", children: [" ", cleanLine(item.state === "open" ? item.question : item.answer ?? item.question)] })] }, item.id));
|
|
684
|
+
}), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "Tab open | history \u00B7 Enter reads \u00B7 Esc" })] }));
|
|
452
685
|
}
|
|
453
|
-
if (
|
|
454
|
-
return (
|
|
686
|
+
if (top.type === "item") {
|
|
687
|
+
return box("yellow", _jsxs(_Fragment, { children: [itemBody(top.item, columns - 4), _jsxs(Text, { color: "gray", wrap: "wrap", children: [top.item.state === "open" ? (s.mayAnswer ? "Enter answers · " : `${readOnlyLabel(TOKEN_ENV)} · `) : "", "e opens its episode \u00B7 Esc"] })] }));
|
|
455
688
|
}
|
|
456
|
-
|
|
689
|
+
if (top.type === "card") {
|
|
690
|
+
const card = top.card;
|
|
691
|
+
return box("magenta", _jsxs(_Fragment, { children: [card.kind === "ceremony" ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["\u25C6 ", cleanLine(card.type ?? "ceremony"), " \u00B7 ", cleanLine(card.direction ?? "no direction"), " \u00B7 ", card.closed ? "closed" : "open"] }), card.intentions.map((line, index) => (_jsx(Text, { wrap: "wrap", children: cleanLine(line) }, index))), card.episode ? _jsxs(Text, { color: "cyan", wrap: "truncate-end", children: ["\u2197 ", d.rowByName(card.episode)?.number ?? "", " ", cleanLine(d.rowByName(card.episode)?.title ?? card.episode)] }) : null, card.circle ? _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\u25CB held in ", cleanLine(card.circle)] }) : _jsx(Text, { color: "gray", children: "held in no circle" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["\u25CB ", cleanLine(card.name), " \u00B7 ", cleanLine(card.direction ?? "no direction"), " \u00B7 ", card.active ? "active" : "inactive"] }), card.intention ? _jsx(Text, { wrap: "wrap", children: cleanLine(card.intention) }) : null, _jsx(Text, { color: "gray", wrap: "truncate-end", children: [card.type, card.facilitator ? `facilitated by ${card.facilitator}` : "", `${card.ceremonies} ceremon${card.ceremonies === 1 ? "y" : "ies"}`]
|
|
692
|
+
.filter(Boolean)
|
|
693
|
+
.map((part) => cleanLine(String(part)))
|
|
694
|
+
.join(" · ") })] })), _jsxs(Text, { color: "green", wrap: "truncate-end", children: ["your seat: ", card.can.length ? card.can.map(cleanLine).join(", ") : "read"] }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "o doors \u00B7 Esc" })] }));
|
|
695
|
+
}
|
|
696
|
+
return box("yellow", _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Keys" }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "/" }), " filter \u00B7 ", _jsx(Text, { color: "yellow", children: "Tab" }), " title or full text (in a file: next reference)"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "!" }), " the chronicle's attention board"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193 j k" }), " move \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp PgDn g G" }), " jump"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "Enter l \u2192" }), " open \u00B7 ", _jsx(Text, { color: "yellow", children: "Esc h \u2190" }), " back \u00B7 ", _jsx(Text, { color: "yellow", children: "q" }), " quit"] }), actions.map((action) => (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: action.key }), " ", action.description ?? action.label, action.effect === "write" ? _jsx(Text, { color: "red", children: " (writes)" }) : null] }, action.id))), _jsx(Text, { color: "gray", children: "Any key closes." })] }));
|
|
697
|
+
})();
|
|
698
|
+
const where = (() => {
|
|
699
|
+
if (s.view === "file" && s.open) {
|
|
700
|
+
const number = d.rowByName(s.open.name)?.number ?? "";
|
|
701
|
+
const line = `l.${s.open.view.lines[s.open.scroll]?.source ?? 1}`;
|
|
702
|
+
// The line number before the path, so a narrow screen keeps it (UI review, F24).
|
|
703
|
+
return `${number} · ${line} · ${middle(cleanLine(s.open.path), Math.max(12, columns - 30))}`;
|
|
704
|
+
}
|
|
705
|
+
const count = `${d.visible.length} episodes`;
|
|
706
|
+
const waiting = d.openCount ? ` · ! ${d.openCount}` : "";
|
|
707
|
+
return compact ? `${count}${waiting}` : `${cleanLine(s.chronicle.root)} · ${count}${waiting}`;
|
|
708
|
+
})();
|
|
709
|
+
const hints = (() => {
|
|
710
|
+
const view = s.view;
|
|
711
|
+
if (s.overlays.length)
|
|
712
|
+
return "Esc back";
|
|
713
|
+
if (view === "file")
|
|
714
|
+
return compact ? "Tab ref · Esc · q" : "Tab next reference ⇧Tab previous Esc back ? keys q quit";
|
|
715
|
+
if (view === "attention")
|
|
716
|
+
return compact ? "/ ⏎ ! Esc q" : "/ filter ⏎ read ! episodes Esc back ? keys q quit";
|
|
717
|
+
if (view === "files")
|
|
718
|
+
return compact ? "⏎ ! ? q" : "⏎ open ! attention Esc back ? keys q quit";
|
|
719
|
+
return compact ? "/ Tab ! ? q" : "/ filter Tab mode ⏎ open ! attention ? keys q quit";
|
|
457
720
|
})();
|
|
458
|
-
const where = view === "file" && open
|
|
459
|
-
? `${rowByName(open.name)?.number ?? ""} · ${open.path} · l.${open.view.lines[open.scroll]?.source ?? 1}`
|
|
460
|
-
: compact
|
|
461
|
-
? `${chronicle.rows.length} episodes`
|
|
462
|
-
: `${chronicle.root} · ${chronicle.rows.length} episodes · ${withManifest} with episode.yaml`;
|
|
463
721
|
const body = (() => {
|
|
464
722
|
if (overlayBody)
|
|
465
723
|
return overlayBody;
|
|
466
|
-
if (view === "file")
|
|
724
|
+
if (s.view === "file")
|
|
467
725
|
return fileBody;
|
|
468
|
-
if (compact)
|
|
469
|
-
|
|
470
|
-
|
|
726
|
+
if (compact) {
|
|
727
|
+
if (s.view === "hits")
|
|
728
|
+
return hitsPane;
|
|
729
|
+
if (s.view === "attention")
|
|
730
|
+
return boardPane;
|
|
731
|
+
if (s.view === "files")
|
|
732
|
+
return detailPane;
|
|
733
|
+
return listPane;
|
|
734
|
+
}
|
|
735
|
+
return (_jsxs(_Fragment, { children: [s.view === "hits" ? hitsPane : s.view === "attention" ? boardPane : listPane, detailPane] }));
|
|
471
736
|
})();
|
|
472
|
-
|
|
737
|
+
const readerLabel = s.api
|
|
738
|
+
? s.reader
|
|
739
|
+
? `as ${cleanLine(s.reader.name)}`
|
|
740
|
+
: s.api.accepted === false
|
|
741
|
+
? "token refused"
|
|
742
|
+
: s.api.signed
|
|
743
|
+
? "…"
|
|
744
|
+
: "read-only"
|
|
745
|
+
: "";
|
|
746
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, width: columns, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "yellow", children: "miadi-chronicle" }), " ", _jsx(Text, { color: "gray", children: where }), readerLabel ? _jsx(Text, { color: s.reader ? "green" : "gray", children: ` · ${readerLabel}` }) : null] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: s.inputMode ? "yellow" : "gray", children: "/ " }), cleanLine(s.query), s.inputMode ? _jsx(Text, { inverse: true, children: " " }) : null, " ", s.view === "attention" ? (_jsx(Text, { color: "gray", children: "attention" })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: s.searchMode === "title" ? "yellow" : "gray", children: "title" }), _jsx(Text, { color: "gray", children: compact ? "|" : " | " }), _jsx(Text, { color: s.searchMode === "full" ? "yellow" : "gray", children: compact ? "full" : "full text" })] })), s.view === "list" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.filtered.length] }) : null, s.view === "attention" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.board.length] }) : null] }), _jsx(Box, { flexDirection: "row", height: bodyHeight, overflow: "hidden", children: body }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: s.status }), _jsxs(Text, { wrap: "truncate-end", children: [available.map((action) => (_jsxs(Text, { children: [_jsx(Text, { color: action.effect === "write" ? "red" : "yellow", children: action.key === "Enter" ? "⏎" : action.key }), " ", action.label, " "] }, action.id))), _jsx(Text, { color: "gray", children: hints })] })] }));
|
|
473
747
|
}
|
|
474
748
|
//# sourceMappingURL=App.js.map
|