@miadi/chronicle-tui 0.2.0 → 0.4.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.
- package/README.md +53 -0
- package/dist/App.d.ts +16 -5
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +626 -251
- package/dist/App.js.map +1 -1
- package/dist/actions.d.ts +52 -9
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +97 -20
- package/dist/actions.js.map +1 -1
- package/dist/api.d.ts +132 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +272 -0
- package/dist/api.js.map +1 -0
- 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 +3 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +42 -4
- 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 +117 -23
- package/dist/data.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- 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 +46 -36
- 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 +4 -3
package/dist/App.js
CHANGED
|
@@ -1,17 +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 } from "./actions.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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";
|
|
18
|
+
import { TOKEN_ENV, answerAttention, connectApi } from "./api.js";
|
|
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";
|
|
15
23
|
const DIRECTION_MARK = { east: "E", south: "S", west: "W", north: "N" };
|
|
16
24
|
const DIRECTION_COLOR = { east: "yellow", south: "red", west: "blue", north: "white" };
|
|
17
25
|
function Segments({ segments, selectedRef }) {
|
|
@@ -29,255 +37,542 @@ function Segments({ segments, selectedRef }) {
|
|
|
29
37
|
return _jsx(Text, { children: segment.text }, index);
|
|
30
38
|
}) }));
|
|
31
39
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
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);
|
|
35
44
|
if (exact !== -1)
|
|
36
|
-
return exact;
|
|
37
|
-
const number = Number(
|
|
45
|
+
return [exact];
|
|
46
|
+
const number = Number(wanted.replace(/^ep/, ""));
|
|
38
47
|
if (Number.isInteger(number)) {
|
|
39
|
-
const byNumber = rows.
|
|
40
|
-
if (byNumber
|
|
48
|
+
const byNumber = rows.flatMap((row, index) => (row.number === number ? [index] : []));
|
|
49
|
+
if (byNumber.length)
|
|
41
50
|
return byNumber;
|
|
42
51
|
}
|
|
43
|
-
|
|
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)}`;
|
|
44
62
|
}
|
|
45
|
-
|
|
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;
|
|
46
87
|
const { exit } = useApp();
|
|
47
|
-
const {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
});
|
|
64
114
|
const details = useRef(new Map());
|
|
115
|
+
const overlaySeq = useRef(0);
|
|
65
116
|
const searchTurn = useRef(0);
|
|
117
|
+
const searchTimer = useRef(null);
|
|
118
|
+
const searchAbort = useRef(null);
|
|
66
119
|
const actions = useMemo(() => [...DEFAULT_ACTIONS, ...extra], [extra]);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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);
|
|
78
153
|
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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)
|
|
88
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;
|
|
175
|
+
}
|
|
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
|
+
};
|
|
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 }));
|
|
194
|
+
let live = true;
|
|
195
|
+
connectApi(url)
|
|
196
|
+
.then(async ({ api, warning }) => {
|
|
197
|
+
if (!live)
|
|
198
|
+
return;
|
|
199
|
+
set({ api, status: warning ? cleanLine(warning) : ui.current.status });
|
|
200
|
+
if (!api?.signed)
|
|
201
|
+
return;
|
|
202
|
+
try {
|
|
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();
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (live)
|
|
213
|
+
set({ status: cleanLine(`The token was refused by ${api.base}: ${error.message}`) });
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
.catch(() => { });
|
|
217
|
+
return () => {
|
|
218
|
+
live = false;
|
|
219
|
+
};
|
|
220
|
+
}, [root, url]);
|
|
221
|
+
// ── full-text search: debounced, cancelled when superseded ───────────────
|
|
222
|
+
const scheduleSearch = () => {
|
|
89
223
|
const turn = ++searchTurn.current;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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")
|
|
94
240
|
return;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
setSearching(false);
|
|
98
|
-
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 });
|
|
99
243
|
}, (error) => {
|
|
100
|
-
if (turn !== searchTurn.current)
|
|
244
|
+
if (turn !== searchTurn.current || error.name === "AbortError")
|
|
101
245
|
return;
|
|
102
|
-
|
|
103
|
-
setStatus(error.message);
|
|
246
|
+
set({ searching: false, status: cleanLine(error.message) });
|
|
104
247
|
});
|
|
105
248
|
}, 250);
|
|
106
|
-
return () => clearTimeout(timer);
|
|
107
|
-
}, [chronicle, query, searchMode]);
|
|
108
|
-
const rowByName = (name) => chronicle?.rows.find((row) => row.name === name);
|
|
109
|
-
const hit = view === "hits" || open?.from === "hits" ? hits?.hits[hitSel] : undefined;
|
|
110
|
-
const currentRow = view === "file" && open
|
|
111
|
-
? rowByName(open.name)
|
|
112
|
-
: view === "hits"
|
|
113
|
-
? hit && rowByName(hit.episodePath)
|
|
114
|
-
: filtered[clamp(listSel, filtered.length)];
|
|
115
|
-
const detailOf = (name) => {
|
|
116
|
-
const cached = details.current.get(name);
|
|
117
|
-
if (cached)
|
|
118
|
-
return cached;
|
|
119
|
-
const loaded = loadDetail(chronicle.root, name);
|
|
120
|
-
details.current.set(name, loaded);
|
|
121
|
-
return loaded;
|
|
122
249
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
127
325
|
if ("error" in text)
|
|
128
|
-
return
|
|
326
|
+
return set({ status: cleanLine(text.error) });
|
|
129
327
|
const shaped = fileView(text.content, columns - 2);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
});
|
|
133
333
|
};
|
|
134
|
-
const context = chronicle
|
|
135
|
-
? {
|
|
136
|
-
view,
|
|
137
|
-
chronicle,
|
|
138
|
-
episode: currentRow,
|
|
139
|
-
file: view === "file" ? open?.path : view === "files" ? files[clamp(fileSel, files.length)]?.relativePath : hit?.file,
|
|
140
|
-
reference: view === "file" && open && open.refSel >= 0 ? open.view.refs[open.refSel] : undefined,
|
|
141
|
-
hit,
|
|
142
|
-
show: setOverlay,
|
|
143
|
-
status: setStatus,
|
|
144
|
-
jumpTo: (name) => {
|
|
145
|
-
let index = filtered.findIndex((row) => row.name === name);
|
|
146
|
-
if (index === -1) {
|
|
147
|
-
setQuery("");
|
|
148
|
-
setSearchMode("title");
|
|
149
|
-
index = chronicle.rows.findIndex((row) => row.name === name);
|
|
150
|
-
}
|
|
151
|
-
setListSel(Math.max(0, index));
|
|
152
|
-
setFileSel(0);
|
|
153
|
-
setOpen(null);
|
|
154
|
-
setView("files");
|
|
155
|
-
},
|
|
156
|
-
}
|
|
157
|
-
: null;
|
|
158
|
-
const bodyHeight = Math.max(5, rows - 4);
|
|
159
334
|
const move = (delta, key) => {
|
|
160
|
-
const
|
|
335
|
+
const d = derive();
|
|
336
|
+
const s = d.s;
|
|
337
|
+
const step = key.pageDown || key.pageUp ? Math.max(1, bodyHeight - 1) : 1;
|
|
161
338
|
const by = delta * step;
|
|
162
|
-
if (view === "list")
|
|
163
|
-
|
|
164
|
-
else if (view === "files")
|
|
165
|
-
|
|
166
|
-
else if (view === "hits")
|
|
167
|
-
|
|
168
|
-
else if (view === "
|
|
169
|
-
|
|
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)) } });
|
|
170
349
|
};
|
|
171
350
|
const back = () => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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;
|
|
175
356
|
}
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}, () => { });
|
|
178
371
|
};
|
|
179
372
|
const forward = () => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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" });
|
|
183
378
|
}
|
|
184
|
-
else if (view === "files" && currentRow
|
|
185
|
-
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;
|
|
186
383
|
if (file.kind !== "text")
|
|
187
|
-
return
|
|
188
|
-
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");
|
|
189
386
|
}
|
|
190
|
-
else if (view === "hits" && hit)
|
|
191
|
-
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);
|
|
192
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 ─────────────────────────────────────────────────────────────────
|
|
193
423
|
useInput((input, key) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
199
439
|
return;
|
|
200
440
|
}
|
|
201
441
|
handleKey(input, key);
|
|
202
442
|
});
|
|
203
443
|
function handleKey(input, key) {
|
|
204
|
-
|
|
444
|
+
const s = ui.current;
|
|
445
|
+
if (!s.chronicle) {
|
|
205
446
|
if (input === "q" || key.escape)
|
|
206
447
|
exit();
|
|
207
448
|
return;
|
|
208
449
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
450
|
+
const top = s.overlays[s.overlays.length - 1]?.overlay;
|
|
451
|
+
if (top?.type === "input") {
|
|
452
|
+
if (top.busy)
|
|
453
|
+
return;
|
|
454
|
+
if (key.escape)
|
|
455
|
+
return closeTop();
|
|
456
|
+
if (key.return) {
|
|
457
|
+
const value = top.value;
|
|
458
|
+
if (!value.trim())
|
|
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) }));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (key.backspace || key.delete)
|
|
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 });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
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;
|
|
214
486
|
if (key.downArrow || input === "j")
|
|
215
|
-
return
|
|
487
|
+
return updateTop({ ...top, selected: clamp(top.selected + 1, count) });
|
|
216
488
|
if (key.upArrow || input === "k")
|
|
217
|
-
return
|
|
218
|
-
const door =
|
|
489
|
+
return updateTop({ ...top, selected: clamp(top.selected - 1, count) });
|
|
490
|
+
const door = top.doors.doors[top.selected];
|
|
219
491
|
if (door && key.return) {
|
|
220
|
-
const
|
|
221
|
-
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)}` });
|
|
222
494
|
}
|
|
223
495
|
if (door && input === "r")
|
|
224
|
-
return
|
|
496
|
+
return void openOverlay({ type: "qr", title: top.doors.title, door, invert: false });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
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 });
|
|
507
|
+
if (key.downArrow || input === "j")
|
|
508
|
+
return updateTop({ ...top, selected: clamp(top.selected + 1, shown.length) });
|
|
509
|
+
if (key.upArrow || input === "k")
|
|
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);
|
|
525
|
+
}
|
|
225
526
|
}
|
|
226
|
-
if (overlay.type === "qr" && input === "i")
|
|
227
|
-
return setOverlay({ ...overlay, invert: !overlay.invert });
|
|
228
|
-
if (overlay.type === "help")
|
|
229
|
-
return setOverlay(null);
|
|
230
527
|
return;
|
|
231
528
|
}
|
|
232
|
-
if (inputMode) {
|
|
529
|
+
if (s.inputMode) {
|
|
233
530
|
if (key.escape || key.return)
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
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
|
+
}
|
|
241
539
|
if (key.upArrow || key.downArrow)
|
|
242
540
|
return move(key.downArrow ? 1 : -1, key);
|
|
243
541
|
if (key.backspace || key.delete) {
|
|
244
|
-
|
|
245
|
-
return
|
|
542
|
+
set({ query: s.query.slice(0, -1), listSel: 0, boardSel: 0 });
|
|
543
|
+
return scheduleSearch();
|
|
246
544
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
545
|
+
const text = input.replace(/[\u0000-\u001f\u007f]/g, "");
|
|
546
|
+
if (text) {
|
|
547
|
+
set({ query: s.query + text, listSel: 0, boardSel: 0 });
|
|
548
|
+
scheduleSearch();
|
|
250
549
|
}
|
|
251
550
|
return;
|
|
252
551
|
}
|
|
253
|
-
if (input === "/" && view !== "file") {
|
|
254
|
-
|
|
255
|
-
setView("list");
|
|
256
|
-
return setInputMode(true);
|
|
552
|
+
if (input === "/" && s.view !== "file") {
|
|
553
|
+
return set({ inputMode: true, view: s.view === "files" ? "list" : s.view });
|
|
257
554
|
}
|
|
258
|
-
if (
|
|
259
|
-
|
|
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;
|
|
260
559
|
if (count === 0)
|
|
261
560
|
return;
|
|
262
|
-
const refSel = key.shift ? (open.refSel <= 0 ? count - 1 : open.refSel - 1) : (open.refSel + 1) % count;
|
|
263
|
-
const ref = open.view.refs[refSel];
|
|
264
|
-
const line = open.view.lines.findIndex((screen) => screen.segments.some((
|
|
265
|
-
const scroll = line < open.scroll || line >= open.scroll + bodyHeight ? Math.max(0, line - 2) : open.scroll;
|
|
266
|
-
|
|
267
|
-
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}`) });
|
|
268
566
|
}
|
|
269
|
-
if (key.tab && (view === "list" || view === "hits")) {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
setView("list");
|
|
274
|
-
else if (hits && query.trim().length >= 2)
|
|
275
|
-
setView("hits");
|
|
276
|
-
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();
|
|
277
571
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
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) }));
|
|
281
576
|
return;
|
|
282
577
|
}
|
|
283
578
|
if (key.downArrow || input === "j" || key.pageDown)
|
|
@@ -285,89 +580,169 @@ export function App({ root, query: initialQuery = "", search: initialSearch, epi
|
|
|
285
580
|
if (key.upArrow || input === "k" || key.pageUp)
|
|
286
581
|
return move(-1, key);
|
|
287
582
|
if (key.home || input === "g")
|
|
288
|
-
return move(-
|
|
583
|
+
return move(-1_000_000, PLAIN_KEY);
|
|
289
584
|
if (key.end || input === "G")
|
|
290
|
-
return move(
|
|
585
|
+
return move(1_000_000, PLAIN_KEY);
|
|
291
586
|
if (key.return || key.rightArrow || input === "l")
|
|
292
587
|
return forward();
|
|
293
588
|
if (key.escape || key.leftArrow || input === "h" || key.backspace)
|
|
294
589
|
return back();
|
|
295
590
|
if (input === "?")
|
|
296
|
-
return
|
|
591
|
+
return void openOverlay({ type: "help" });
|
|
297
592
|
if (input === "q")
|
|
298
593
|
exit();
|
|
299
594
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
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();
|
|
309
603
|
const leftWidth = compact ? columns : Math.min(64, Math.max(36, Math.floor(columns * 0.4)));
|
|
310
604
|
const rightWidth = compact ? columns : Math.max(20, columns - leftWidth - 1);
|
|
311
605
|
const available = actions.filter((action) => action.available(context));
|
|
312
|
-
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;
|
|
313
607
|
const showAge = columns >= 44;
|
|
314
|
-
const ageWidth = Math.max(2, ...chronicle.rows.map((row) => ageOf(row.date).length));
|
|
315
|
-
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));
|
|
316
|
-
const selectedRow = clamp(listSel, filtered.length);
|
|
317
|
-
const selected = filtered[selectedRow];
|
|
318
|
-
// A compact list ends with the selected episode's whole title
|
|
319
|
-
// 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.
|
|
320
613
|
const peekLines = compact && selected ? 3 : 0;
|
|
321
|
-
const listWindow = windowOf(filtered, selectedRow, bodyHeight - peekLines);
|
|
322
|
-
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) => {
|
|
323
616
|
const isSelected = listWindow.start + index === selectedRow;
|
|
324
|
-
return rowLine(row, isSelected && view === "list", isSelected && view === "files");
|
|
325
|
-
}), 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] }));
|
|
326
619
|
const hitRows = compact ? 2 : 1;
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
|
|
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}`));
|
|
627
|
+
})] }));
|
|
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}`));
|
|
333
636
|
})] }));
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
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 }) }));
|
|
343
654
|
const overlayBody = (() => {
|
|
344
|
-
|
|
655
|
+
const top = s.overlays[s.overlays.length - 1]?.overlay;
|
|
656
|
+
if (!top)
|
|
345
657
|
return null;
|
|
346
|
-
if (
|
|
347
|
-
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" })] }));
|
|
660
|
+
}
|
|
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] }));
|
|
348
671
|
}
|
|
349
|
-
if (
|
|
350
|
-
return (
|
|
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" })] }));
|
|
351
674
|
}
|
|
352
|
-
if (
|
|
353
|
-
return (
|
|
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" })] }));
|
|
354
677
|
}
|
|
355
|
-
|
|
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" })] }));
|
|
685
|
+
}
|
|
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"] })] }));
|
|
688
|
+
}
|
|
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";
|
|
356
720
|
})();
|
|
357
|
-
const where = view === "file" && open
|
|
358
|
-
? `${rowByName(open.name)?.number ?? ""} · ${open.path} · l.${open.view.lines[open.scroll]?.source ?? 1}`
|
|
359
|
-
: compact
|
|
360
|
-
? `${chronicle.rows.length} episodes`
|
|
361
|
-
: `${chronicle.root} · ${chronicle.rows.length} episodes · ${withManifest} with episode.yaml`;
|
|
362
721
|
const body = (() => {
|
|
363
722
|
if (overlayBody)
|
|
364
723
|
return overlayBody;
|
|
365
|
-
if (view === "file")
|
|
724
|
+
if (s.view === "file")
|
|
366
725
|
return fileBody;
|
|
367
|
-
if (compact)
|
|
368
|
-
|
|
369
|
-
|
|
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] }));
|
|
370
736
|
})();
|
|
371
|
-
|
|
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 })] })] }));
|
|
372
747
|
}
|
|
373
748
|
//# sourceMappingURL=App.js.map
|