ink-cartridge 3.6.1 → 3.7.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.
@@ -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 Close"),
148
+ " | \u2191\u2193 Move | Esc CloseI | Ctrl + G GlobalKeys Display"),
136
149
  React.createElement(Text, { dimColor: true },
137
150
  "Top: ",
138
151
  offsetTop,
@@ -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,2 +1,3 @@
1
1
  import React from "react";
2
- export default function GlobalKeyDisplayBox(): React.JSX.Element;
2
+ import { GlobalProps } from "./types.js";
3
+ export default function GlobalKeyDisplayBox({ top: initialTop, left }: GlobalProps): React.JSX.Element;
@@ -1,8 +1,147 @@
1
- import { Box } from "ink";
2
- import React from "react";
3
- // This modal box is used to display information about all registered global keys.
4
- // Because it is a modal box, its keyboard is independent and will not be affected by DevTool.
5
- // Also make sure that the zindex is greater than the zindex of the DevTool so that the visual effects and keyboard reception are applied correctly when the modal box is opened
6
- export default function GlobalKeyDisplayBox() {
7
- return (React.createElement(Box, { position: "absolute" }));
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
+ !expandedEntry && (React.createElement(Box, null,
142
+ React.createElement(Text, { dimColor: true }, "After collapsing from detail, press Tab to restore the selection highlight.")))));
143
+ }
144
+ registerComponent(GlobalKeyDisplayBox, {
145
+ top: 0,
146
+ left: 0
147
+ });
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ink-cartridge",
3
- "version": "3.6.1",
3
+ "version": "3.7.0",
4
4
  "description": "Ready-to-use Ink components and screen management system for building terminal UIs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",