ink-cartridge 3.6.1 → 3.7.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/dist/dev/dev-screen.js
CHANGED
|
@@ -5,6 +5,7 @@ import { useKeyboard, useModalMissListener } from "../keyboard/hook.js";
|
|
|
5
5
|
import { ModalContext } from "../screen/ModalContext.js";
|
|
6
6
|
import { registerComponent } from "../screen/registry.js";
|
|
7
7
|
import { closeDevTool } from "./entrance.js";
|
|
8
|
+
import GlobalKeyDisplayBox from "./globalKey-display.js";
|
|
8
9
|
const PANEL_HEIGHT = 30;
|
|
9
10
|
/**
|
|
10
11
|
* Developer debugging modal for the ink-cartridge screen system.
|
|
@@ -44,6 +45,7 @@ export function DevScreen({ top: initialTop, left }) {
|
|
|
44
45
|
const { boundKeyboard } = useKeyboard();
|
|
45
46
|
const modalId = useContext(ModalContext);
|
|
46
47
|
const { rows } = useWindowSize();
|
|
48
|
+
const { openModal } = useScreenSystem();
|
|
47
49
|
const [offsetTop, setOffsetTop] = useState(initialTop);
|
|
48
50
|
const [flashBorder, setFlashBorder] = useState(false);
|
|
49
51
|
const flashTimerRef = useRef(null);
|
|
@@ -76,10 +78,21 @@ export function DevScreen({ top: initialTop, left }) {
|
|
|
76
78
|
if (modalId)
|
|
77
79
|
closeDevTool();
|
|
78
80
|
});
|
|
81
|
+
const u4 = boundKeyboard(['ctrl+g'], () => {
|
|
82
|
+
openModal('__global-display__', GlobalKeyDisplayBox, {
|
|
83
|
+
top: initialTop + 3,
|
|
84
|
+
// Add three to prevent complete coverage of the DevBox from fragmenting the visual experience
|
|
85
|
+
left: left
|
|
86
|
+
});
|
|
87
|
+
// Actually, we don't need to pass in zindex here.
|
|
88
|
+
// Because the current mechanism is that when no zindex is passed, the following modal box overwrites the preceding modal box by default
|
|
89
|
+
// And our automatic activation mechanism ensures that it is also the one in the highest mode box.
|
|
90
|
+
});
|
|
79
91
|
return () => {
|
|
80
92
|
u1();
|
|
81
93
|
u2();
|
|
82
94
|
u3();
|
|
95
|
+
u4();
|
|
83
96
|
};
|
|
84
97
|
}, [modalId]);
|
|
85
98
|
// When the terminal is resized the current position may land outside
|
|
@@ -132,7 +145,7 @@ export function DevScreen({ top: initialTop, left }) {
|
|
|
132
145
|
React.createElement(Text, { dimColor: true },
|
|
133
146
|
"Screens: ",
|
|
134
147
|
currentPath.length,
|
|
135
|
-
" | \u2191\u2193 Move | Esc
|
|
148
|
+
" | \u2191\u2193 Move | Esc CloseI | Ctrl + G GlobalKeys Display"),
|
|
136
149
|
React.createElement(Text, { dimColor: true },
|
|
137
150
|
"Top: ",
|
|
138
151
|
offsetTop,
|
package/dist/dev/entrance.d.ts
CHANGED
|
@@ -32,9 +32,7 @@ import { DevProps } from "./types.js";
|
|
|
32
32
|
* });
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
-
export declare function openDevTool({ top, left, zindex }: DevProps
|
|
36
|
-
zindex?: number;
|
|
37
|
-
}): void;
|
|
35
|
+
export declare function openDevTool({ top, left, zindex }: DevProps): void;
|
|
38
36
|
/**
|
|
39
37
|
* Close the developer debugging modal.
|
|
40
38
|
*
|
|
@@ -1,8 +1,145 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
|
+
import { useKeyboard } from "../keyboard/hook.js";
|
|
4
|
+
import { ModalContext } from "../screen/ModalContext.js";
|
|
5
|
+
import { useScreenSystem } from "../screen/hook.js";
|
|
6
|
+
import { registerComponent } from "../screen/registry.js";
|
|
7
|
+
import { SelectInput } from "../components/select/SelectInput.js";
|
|
8
|
+
// This modal box displays information about all registered global keys.
|
|
9
|
+
// Because it is a modal, its keyboard is independent and will not be
|
|
10
|
+
// affected by DevTool. When the user presses Enter on a list item the
|
|
11
|
+
// detail panel expands; Enter again collapses back to the list.
|
|
12
|
+
// @2026-07-16 v3.6.2
|
|
13
|
+
const PANEL_HEIGHT = 30;
|
|
14
|
+
/**
|
|
15
|
+
* Build a compact label for a global key entry.
|
|
16
|
+
*
|
|
17
|
+
* Shows the key name(s) followed by short badges for non-default options:
|
|
18
|
+
* `[ao]` = affectOverlay, `[xno]` = executeWhenNoOverlay, `[×N]` = times.
|
|
19
|
+
*/
|
|
20
|
+
function buildLabel(entry) {
|
|
21
|
+
const keys = Array.isArray(entry.key) ? entry.key.join(', ') : entry.key;
|
|
22
|
+
const badges = [];
|
|
23
|
+
if (entry.affectOverlay)
|
|
24
|
+
badges.push('[ao]');
|
|
25
|
+
if (entry.executeWhenNoOverlay)
|
|
26
|
+
badges.push('[xno]');
|
|
27
|
+
if (entry.times !== undefined)
|
|
28
|
+
badges.push(`[×${entry.times}]`);
|
|
29
|
+
return badges.length > 0 ? `${keys} ${badges.join(' ')}` : keys;
|
|
8
30
|
}
|
|
31
|
+
function fmtCategory(category) {
|
|
32
|
+
if (category === undefined || category === '*')
|
|
33
|
+
return '*';
|
|
34
|
+
return category.map(c => c.displayName || c.name || '?').join(', ');
|
|
35
|
+
}
|
|
36
|
+
/** Renders a boolean value in green (true) or red (false). */
|
|
37
|
+
function BoolVal({ v }) {
|
|
38
|
+
const c = v === false ? 'red' : 'green';
|
|
39
|
+
return React.createElement(Text, { color: c }, String(v ?? true));
|
|
40
|
+
}
|
|
41
|
+
/** Two-column detail row: dim label on left, value slot on right. */
|
|
42
|
+
function Row({ label, children }) {
|
|
43
|
+
return (React.createElement(Box, { flexDirection: "row" },
|
|
44
|
+
React.createElement(Box, { width: 18 },
|
|
45
|
+
React.createElement(Text, { dimColor: true }, label)),
|
|
46
|
+
children));
|
|
47
|
+
}
|
|
48
|
+
/** Blue separator line for visual sectioning. */
|
|
49
|
+
const Sep = () => (React.createElement(Box, null,
|
|
50
|
+
React.createElement(Text, { color: "blue", dimColor: true }, "─".repeat(50))));
|
|
51
|
+
export default function GlobalKeyDisplayBox({ top: initialTop, left }) {
|
|
52
|
+
const { getGlobalKeys, boundKeyboard } = useKeyboard();
|
|
53
|
+
const { closeModal } = useScreenSystem();
|
|
54
|
+
const modalId = useContext(ModalContext);
|
|
55
|
+
const { rows } = useWindowSize();
|
|
56
|
+
const [offsetTop, setOffsetTop] = useState(initialTop);
|
|
57
|
+
const [expandedEntry, setExpandedEntry] = useState(null);
|
|
58
|
+
const clampTopRef = useRef((next) => next);
|
|
59
|
+
clampTopRef.current = (next) => Math.max(0, Math.min(next, rows - PANEL_HEIGHT));
|
|
60
|
+
const expandedEntryRef = useRef(expandedEntry);
|
|
61
|
+
expandedEntryRef.current = expandedEntry;
|
|
62
|
+
// Bind return at the screen level so Enter collapses the detail panel.
|
|
63
|
+
// When the SelectInput is rendered (not expanded), its focus-level
|
|
64
|
+
// return binding takes priority and this binding is never reached.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const unReturn = boundKeyboard(['return'], () => {
|
|
67
|
+
if (expandedEntryRef.current) {
|
|
68
|
+
setExpandedEntry(null);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return () => { unReturn(); };
|
|
72
|
+
}, []);
|
|
73
|
+
// Panel movement via up/down arrow keys
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const uUp = boundKeyboard(['up'], () => setOffsetTop(prev => clampTopRef.current(prev - 1)), {
|
|
76
|
+
focusId: 'globalKey-control'
|
|
77
|
+
});
|
|
78
|
+
const uDown = boundKeyboard(['down'], () => setOffsetTop(prev => clampTopRef.current(prev + 1)), {
|
|
79
|
+
focusId: 'globalKey-control'
|
|
80
|
+
});
|
|
81
|
+
return () => { uUp(); uDown(); };
|
|
82
|
+
}, []);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const unEscape = boundKeyboard(['escape'], () => {
|
|
85
|
+
if (modalId)
|
|
86
|
+
closeModal(modalId);
|
|
87
|
+
});
|
|
88
|
+
return () => { unEscape(); };
|
|
89
|
+
}, [modalId]);
|
|
90
|
+
// Re-clamp on terminal resize
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
setOffsetTop(prev => clampTopRef.current(prev));
|
|
93
|
+
}, [rows]);
|
|
94
|
+
const entries = getGlobalKeys();
|
|
95
|
+
const items = entries.map((entry, i) => ({
|
|
96
|
+
label: buildLabel(entry),
|
|
97
|
+
value: entry,
|
|
98
|
+
Key: `gk-${i}`,
|
|
99
|
+
}));
|
|
100
|
+
const handleSelect = (item) => {
|
|
101
|
+
if (expandedEntry === item.value) {
|
|
102
|
+
setExpandedEntry(null);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
setExpandedEntry(item.value);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return (React.createElement(Box, { position: "absolute", top: offsetTop, left: left, height: PANEL_HEIGHT, width: '100%', borderStyle: 'bold', borderColor: 'white', backgroundColor: 'black', flexDirection: "column", paddingX: 1, paddingY: 1 },
|
|
109
|
+
React.createElement(Box, null,
|
|
110
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "\u258C Global Keys "),
|
|
111
|
+
React.createElement(Text, { dimColor: true },
|
|
112
|
+
"(",
|
|
113
|
+
entries.length,
|
|
114
|
+
")")),
|
|
115
|
+
React.createElement(Sep, null),
|
|
116
|
+
expandedEntry ? (React.createElement(Box, { flexDirection: "column", paddingY: 1 },
|
|
117
|
+
React.createElement(Box, { paddingBottom: 1 },
|
|
118
|
+
React.createElement(Text, { color: "yellow", bold: true }, Array.isArray(expandedEntry.key) ? expandedEntry.key.join(', ') : expandedEntry.key)),
|
|
119
|
+
React.createElement(Sep, null),
|
|
120
|
+
React.createElement(Box, { paddingY: 1, flexDirection: "column" },
|
|
121
|
+
React.createElement(Row, { label: "Cover" },
|
|
122
|
+
React.createElement(BoolVal, { v: expandedEntry.cover })),
|
|
123
|
+
React.createElement(Row, { label: "AffectOverlay" },
|
|
124
|
+
React.createElement(BoolVal, { v: expandedEntry.affectOverlay })),
|
|
125
|
+
React.createElement(Row, { label: "ExecWhenNoOv" },
|
|
126
|
+
React.createElement(BoolVal, { v: expandedEntry.executeWhenNoOverlay })),
|
|
127
|
+
React.createElement(Row, { label: "Times" },
|
|
128
|
+
React.createElement(Text, { color: "white" }, expandedEntry.times ?? '—')),
|
|
129
|
+
React.createElement(Row, { label: "Category" },
|
|
130
|
+
React.createElement(Text, { color: "white" }, fmtCategory(expandedEntry.category))),
|
|
131
|
+
React.createElement(Row, { label: "When" },
|
|
132
|
+
React.createElement(BoolVal, { v: expandedEntry.when != null })),
|
|
133
|
+
React.createElement(Row, { label: "Observer" },
|
|
134
|
+
React.createElement(BoolVal, { v: expandedEntry.observer != null }))))) : items.length === 0 ? (React.createElement(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center" },
|
|
135
|
+
React.createElement(Text, { color: "gray" }, "No global keys registered."))) : (React.createElement(SelectInput, { items: items, onSelect: handleSelect, focusId: "global-key-list", limit: 9 })),
|
|
136
|
+
React.createElement(Sep, null),
|
|
137
|
+
React.createElement(Box, { paddingTop: 1 },
|
|
138
|
+
React.createElement(Text, { dimColor: true }, expandedEntry
|
|
139
|
+
? 'Enter: collapse | Esc: close'
|
|
140
|
+
: 'Tab: switch focus | Enter: details | Esc: close'))));
|
|
141
|
+
}
|
|
142
|
+
registerComponent(GlobalKeyDisplayBox, {
|
|
143
|
+
top: 0,
|
|
144
|
+
left: 0
|
|
145
|
+
});
|
package/dist/dev/types.d.ts
CHANGED
|
@@ -6,4 +6,18 @@ export interface DevProps {
|
|
|
6
6
|
top: number;
|
|
7
7
|
/** Horizontal position in columns — 0 is the left edge. */
|
|
8
8
|
left: number;
|
|
9
|
+
zindex?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Props accepted by the {@link GlobalKeyDisplayBox} global-keys inspector modal.
|
|
13
|
+
*
|
|
14
|
+
* Opened via `Ctrl+G` while the {@link DevScreen} modal is active. Renders as
|
|
15
|
+
* a white-bordered panel listing all registered global key bindings with
|
|
16
|
+
* expandable detail cards.
|
|
17
|
+
*/
|
|
18
|
+
export interface GlobalProps {
|
|
19
|
+
/** Vertical position in rows — 0 is the top of the terminal. */
|
|
20
|
+
top: number;
|
|
21
|
+
/** Horizontal position in columns — 0 is the left edge. */
|
|
22
|
+
left: number;
|
|
9
23
|
}
|
|
@@ -145,8 +145,32 @@ export function KeyboardProvider({ children }) {
|
|
|
145
145
|
const pushOwner = useCallback((owner) => {
|
|
146
146
|
ownerStackRef.current = [...ownerStackRef.current, owner];
|
|
147
147
|
}, []);
|
|
148
|
+
// fix: _popOwner used `filter(o => o !== owner)` which deletes EVERY
|
|
149
|
+
// occurrence of owner from the stack. When multiple components inside
|
|
150
|
+
// the same modal/overlay each call `useKeyboard()`, they all push the
|
|
151
|
+
// same owner ID — e.g. GlobalKeyDisplayBox, SelectInput, and
|
|
152
|
+
// useFocusState() each push `modalId`, creating [modalId, modalId, modalId].
|
|
153
|
+
//
|
|
154
|
+
// When a nested component unmounts, its _popOwner cleanup would wipe
|
|
155
|
+
// ALL three entries, emptying the stack while the outer component is
|
|
156
|
+
// still mounted. This makes `getCurrentOwner()` return null, causing:
|
|
157
|
+
// - focusUnregister() to silently no-op (stale focus state remains)
|
|
158
|
+
// - focusCurrent/focusSet/focusNext/focusPrev all blind to the layer
|
|
159
|
+
// - re-mounting components (e.g. SelectInput after collapsing a
|
|
160
|
+
// detail view) unable to restore keyboard focus
|
|
161
|
+
//
|
|
162
|
+
// Switch to `lastIndexOf` + slice so only the LAST occurrence (the
|
|
163
|
+
// caller's own push) is removed — proper LIFO stack behaviour.
|
|
164
|
+
// @2026-06-25 v3.7.0
|
|
148
165
|
const popOwner = useCallback((owner) => {
|
|
149
|
-
|
|
166
|
+
const stack = ownerStackRef.current;
|
|
167
|
+
const idx = stack.lastIndexOf(owner);
|
|
168
|
+
if (idx !== -1) {
|
|
169
|
+
ownerStackRef.current = [
|
|
170
|
+
...stack.slice(0, idx),
|
|
171
|
+
...stack.slice(idx + 1),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
150
174
|
}, []);
|
|
151
175
|
const enableWildcardPriority = useCallback(() => {
|
|
152
176
|
wildcardPriorityCountRef.current += 1;
|