@olenbetong/appframe-cli 4.3.1 → 4.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/cli/af-apply.js +1 -1
  3. package/cli/af-deploy.js +1 -1
  4. package/cli/editor/TransactionsEditor.js +134 -158
  5. package/cli/editor/TransactionsPreviewDialog.js +175 -104
  6. package/cli/editor/tableFormatting.js +9 -80
  7. package/cli/editor/useCommand.js +148 -0
  8. package/cli/editor/useScrollableText.js +28 -0
  9. package/cli/editor/useTransactionEdits.js +142 -0
  10. package/cli/editor/useTransactionText.js +113 -0
  11. package/cli/editor/useTransactions.js +53 -0
  12. package/cli/editor/useTransactionsSelection.js +41 -0
  13. package/cli/editor/useTransactionsTableLayout.js +54 -0
  14. package/cli/editor/{useTransactionsTableViewport.js → useVirtualScrolling.js} +62 -24
  15. package/cli/editor/useVirtualText.js +18 -0
  16. package/package.json +2 -2
  17. package/src/af-apply.ts +1 -1
  18. package/src/af-deploy.ts +1 -1
  19. package/src/editor/TransactionsEditor.tsx +155 -180
  20. package/src/editor/TransactionsPreviewDialog.tsx +196 -146
  21. package/src/editor/tableFormatting.ts +8 -94
  22. package/src/editor/useCommand.ts +191 -0
  23. package/src/editor/useScrollableText.ts +48 -0
  24. package/src/editor/useTransactionEdits.ts +183 -0
  25. package/src/editor/useTransactionText.ts +153 -0
  26. package/src/editor/useTransactions.ts +75 -0
  27. package/src/editor/useTransactionsSelection.ts +54 -0
  28. package/src/editor/useTransactionsTableLayout.ts +84 -0
  29. package/src/editor/{useTransactionsTableViewport.ts → useVirtualScrolling.ts} +87 -38
  30. package/src/editor/useVirtualText.ts +32 -0
  31. package/tsconfig.build.tsbuildinfo +1 -1
  32. package/cli/editor/useTransactionsEditorData.js +0 -206
  33. package/cli/editor/useTransactionsEditorInput.js +0 -109
  34. package/src/editor/useTransactionsEditorData.ts +0 -245
  35. package/src/editor/useTransactionsEditorInput.ts +0 -147
@@ -1,116 +1,178 @@
1
1
  "use strict";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { Box, Text, useInput } from "ink";
4
- import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { transactionStatusLabels } from "@olenbetong/appframe-updater";
4
+ import { Box, Text } from "ink";
5
+ import { useMemo } from "react";
5
6
  import { FullScreenBox } from "./FullScreenBox.js";
7
+ import { STATUS_DIRTY_INDICATOR } from "./tableFormatting.js";
8
+ import { useCommand } from "./useCommand.js";
6
9
  import { useScreenSize } from "./useScreenSize.js";
7
- function formatPreviewValue(value) {
8
- if (typeof value === "string") {
9
- let lines = value.split(/\r?\n/);
10
- return lines.length > 0 ? lines : [""];
11
- }
12
- if (value && typeof value === "object") {
13
- try {
14
- let serialized = JSON.stringify(value, null, 2);
15
- if (serialized) {
16
- return serialized.split("\n");
17
- }
18
- } catch {
19
- }
20
- }
21
- if (value === null) {
22
- return ["null"];
10
+ import { useScrollableText } from "./useScrollableText.js";
11
+ import { useTransactionEdits } from "./useTransactionEdits.js";
12
+ import { useTransactions } from "./useTransactions.js";
13
+ import { useTransactionText } from "./useTransactionText.js";
14
+ import { useVirtualText } from "./useVirtualText.js";
15
+ const PREVIEW_SAVE_SHORTCUTS = ["ctrl+s", "meta+s"];
16
+ const NO_TRANSACTION_FILTER = "1 = 0";
17
+ function escapePrimKey(primKey) {
18
+ return primKey.replace(/'/g, "''");
19
+ }
20
+ function createPrimKeyFilter(primKey) {
21
+ if (!primKey) {
22
+ return NO_TRANSACTION_FILTER;
23
23
  }
24
- return [String(value)];
24
+ return `PrimKey = '${escapePrimKey(primKey)}'`;
25
25
  }
26
- export function TransactionsPreviewDialog({ record, frameWidth }) {
27
- let previewEntries = useMemo(
28
- () => Object.entries(record).filter(([key]) => key !== "PrimKey").map(([key, value]) => ({ key, lines: formatPreviewValue(value) })),
29
- [record]
30
- );
31
- let maxKeyLength = useMemo(
32
- () => previewEntries.reduce((max, entry) => Math.max(max, entry.key.length), 0),
33
- [previewEntries]
26
+ function Shortcut({ keyboard, command }) {
27
+ return /* @__PURE__ */ jsxs(Text, { children: [
28
+ "[",
29
+ keyboard,
30
+ "] ",
31
+ command
32
+ ] });
33
+ }
34
+ export function TransactionsPreviewDialog({
35
+ server,
36
+ namespace,
37
+ frameWidth,
38
+ focusedPrimKey,
39
+ focusNext,
40
+ focusPrevious
41
+ }) {
42
+ let transactionFilter = useMemo(() => createPrimKeyFilter(focusedPrimKey), [focusedPrimKey]);
43
+ let { loading, error, refresh, normalizedTransactions, transactionMap } = useTransactions({
44
+ server,
45
+ namespace,
46
+ filter: transactionFilter
47
+ });
48
+ let selectedPrimKeys = useMemo(
49
+ () => focusedPrimKey ? /* @__PURE__ */ new Set([focusedPrimKey]) : /* @__PURE__ */ new Set(),
50
+ [focusedPrimKey]
34
51
  );
35
- let previewItems = useMemo(() => {
36
- if (previewEntries.length === 0) {
37
- return [];
52
+ let { pendingStatuses, cycleStatus, save } = useTransactionEdits({
53
+ normalizedTransactions,
54
+ transactionRecordMap: transactionMap,
55
+ selectedPrimKeys,
56
+ server,
57
+ refresh
58
+ });
59
+ let record = focusedPrimKey ? transactionMap.get(focusedPrimKey) : void 0;
60
+ let pendingStatus = focusedPrimKey ? pendingStatuses[focusedPrimKey] : void 0;
61
+ let statusPreviewValue = useMemo(() => {
62
+ if (!record) {
63
+ return "";
64
+ }
65
+ let originalStatus = record.Status;
66
+ let numericOriginal = typeof originalStatus === "number" ? originalStatus : typeof originalStatus === "string" ? Number(originalStatus) : null;
67
+ let baseText;
68
+ if (pendingStatus !== void 0) {
69
+ baseText = String(pendingStatus);
70
+ } else if (originalStatus === null) {
71
+ baseText = "null";
72
+ } else if (originalStatus === void 0) {
73
+ baseText = "";
74
+ } else {
75
+ baseText = String(originalStatus);
38
76
  }
39
- let items = [];
40
- previewEntries.forEach((entry, entryIndex) => {
41
- entry.lines.forEach((line, lineIndex) => {
42
- items.push({
43
- type: "line",
44
- key: `${entry.key}-${lineIndex}`,
45
- entryKey: entry.key,
46
- line,
47
- lineIndex
48
- });
49
- });
50
- if (entryIndex < previewEntries.length - 1) {
51
- items.push({ type: "spacer", key: `${entry.key}-spacer` });
77
+ let labelSource = null;
78
+ if (pendingStatus !== void 0) {
79
+ labelSource = pendingStatus;
80
+ } else if (numericOriginal !== null && Number.isFinite(numericOriginal)) {
81
+ labelSource = numericOriginal;
82
+ }
83
+ if (labelSource !== null) {
84
+ let label = transactionStatusLabels[labelSource];
85
+ if (label) {
86
+ baseText = baseText.length > 0 ? `${baseText} (${label})` : `(${label})`;
52
87
  }
53
- });
54
- return items;
55
- }, [previewEntries]);
56
- let rawKeyColumnWidth = Math.max(maxKeyLength, 1);
57
- let availableDialogWidth = Math.max(frameWidth - 4, 1);
58
- let dialogWidth = Math.min(Math.max(rawKeyColumnWidth + 80, 10), availableDialogWidth);
59
- let keyColumnWidth = Math.min(rawKeyColumnWidth, Math.max(dialogWidth - 4, 1));
88
+ }
89
+ let isDirty = false;
90
+ if (pendingStatus !== void 0) {
91
+ isDirty = numericOriginal === null || !Number.isFinite(numericOriginal) || pendingStatus !== numericOriginal;
92
+ }
93
+ if (isDirty) {
94
+ baseText = `${baseText}${STATUS_DIRTY_INDICATOR}`;
95
+ }
96
+ return baseText;
97
+ }, [pendingStatus, record]);
98
+ let { text: transactionText, dialogWidth } = useTransactionText({
99
+ transaction: record,
100
+ statusPreviewValue,
101
+ frameWidth
102
+ });
60
103
  let { height: screenHeight } = useScreenSize();
61
- let headerLineCount = 3 + (record.Name ? 1 : 0);
104
+ let headerLineCount = 3 + (record?.Name ? 1 : 0);
62
105
  let marginLines = 1;
63
- let nonContentLines = headerLineCount + marginLines;
64
- let contentLineCount = previewItems.length > 0 ? previewItems.length : 1;
65
- let naturalDialogHeight = nonContentLines + contentLineCount;
66
- let maxDialogHeight = Math.max(screenHeight - 4, nonContentLines + 1);
67
- maxDialogHeight = Math.min(maxDialogHeight, Math.max(screenHeight - 2, 1));
68
- let dialogHeight = Math.min(naturalDialogHeight, maxDialogHeight);
69
- let contentHeight = Math.max(dialogHeight - nonContentLines, 1);
70
- let [scrollOffset, setScrollOffset] = useState(0);
106
+ let baseNonContentLines = headerLineCount + marginLines;
107
+ let contentLineCount = transactionText.length > 0 ? transactionText.length : 1;
108
+ let computeLayout = (extraNonContentLines) => {
109
+ let nonContentLines = baseNonContentLines + extraNonContentLines;
110
+ let naturalDialogHeight = nonContentLines + contentLineCount;
111
+ let maxDialogHeight = Math.max(screenHeight - 4, nonContentLines + 1);
112
+ maxDialogHeight = Math.min(maxDialogHeight, Math.max(screenHeight - 2, 1));
113
+ let dialogHeightValue = Math.min(naturalDialogHeight, maxDialogHeight);
114
+ let contentHeightValue = Math.max(dialogHeightValue - nonContentLines, 1);
115
+ return { dialogHeight: dialogHeightValue, contentHeight: contentHeightValue, nonContentLines };
116
+ };
117
+ let { dialogHeight, contentHeight } = computeLayout(0);
118
+ let showScrollSummary = record && transactionText.length > contentHeight;
119
+ if (showScrollSummary) {
120
+ ({ dialogHeight, contentHeight } = computeLayout(2));
121
+ }
71
122
  let previewSignature = useMemo(
72
- () => previewItems.map((item) => item.type === "spacer" ? item.key : `${item.key}:${item.line}`).join("|"),
73
- [previewItems]
123
+ () => `${focusedPrimKey ?? ""}|${transactionText.join("|")}`,
124
+ [focusedPrimKey, transactionText]
74
125
  );
75
- let lastPreviewSignatureRef = useRef(previewSignature);
76
- useEffect(() => {
77
- if (lastPreviewSignatureRef.current !== previewSignature) {
78
- lastPreviewSignatureRef.current = previewSignature;
79
- setScrollOffset(0);
80
- }
81
- }, [previewSignature]);
82
- let totalItems = previewItems.length;
83
- let maxScrollOffset = Math.max((totalItems || 1) - contentHeight, 0);
84
- useEffect(() => {
85
- setScrollOffset((offset) => Math.min(offset, maxScrollOffset));
86
- }, [maxScrollOffset]);
87
- useInput(
88
- (_, key) => {
89
- if (key.upArrow) {
90
- setScrollOffset((offset) => Math.max(offset - 1, 0));
91
- return;
92
- }
93
- if (key.downArrow) {
94
- setScrollOffset((offset) => Math.min(offset + 1, maxScrollOffset));
95
- return;
126
+ let { scrollOffset } = useScrollableText({
127
+ textLength: transactionText.length,
128
+ viewportHeight: contentHeight,
129
+ signature: previewSignature
130
+ });
131
+ let { visibleText, totalLines, offset } = useVirtualText({
132
+ text: transactionText,
133
+ viewportHeight: contentHeight,
134
+ scrollOffset
135
+ });
136
+ useCommand("left", focusPrevious, true);
137
+ useCommand("right", focusNext, true);
138
+ useCommand(
139
+ PREVIEW_SAVE_SHORTCUTS,
140
+ () => {
141
+ if (focusedPrimKey) {
142
+ save({ primKey: focusedPrimKey });
96
143
  }
97
- if (key.pageUp) {
98
- setScrollOffset((offset) => Math.max(offset - Math.max(contentHeight - 1, 1), 0));
99
- return;
100
- }
101
- if (key.pageDown) {
102
- setScrollOffset((offset) => Math.min(offset + Math.max(contentHeight - 1, 1), maxScrollOffset));
103
- return;
144
+ },
145
+ focusedPrimKey !== null
146
+ );
147
+ useCommand(
148
+ "s",
149
+ () => {
150
+ if (focusedPrimKey) {
151
+ cycleStatus(focusedPrimKey, { exclusive: true });
104
152
  }
105
153
  },
106
- { isActive: previewItems.length > contentHeight }
154
+ focusedPrimKey !== null
107
155
  );
108
- let visibleItems = previewItems.slice(scrollOffset, scrollOffset + contentHeight);
109
156
  let summaryText = useMemo(() => {
110
- let start = scrollOffset + 1;
111
- let end = Math.min(scrollOffset + contentHeight, previewItems.length);
112
- return `Showing ${start}\u2013${end} of ${previewItems.length}`;
113
- }, [contentHeight, previewItems.length, scrollOffset]);
157
+ if (totalLines === 0) {
158
+ return "";
159
+ }
160
+ let start = offset + 1;
161
+ let end = Math.min(offset + contentHeight, totalLines);
162
+ return `Showing ${start}\u2013${end} of ${totalLines}`;
163
+ }, [contentHeight, offset, totalLines]);
164
+ let bodyContent;
165
+ if (!focusedPrimKey) {
166
+ bodyContent = /* @__PURE__ */ jsx(Text, { color: "gray", children: "No transaction selected." });
167
+ } else if (error) {
168
+ bodyContent = /* @__PURE__ */ jsx(Text, { color: "red", children: error.message });
169
+ } else if (!record) {
170
+ bodyContent = loading ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "Loading transaction\u2026" }) : /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Transaction not found." });
171
+ } else if (transactionText.length === 0) {
172
+ bodyContent = /* @__PURE__ */ jsx(Text, { color: "gray", children: "No data available." });
173
+ } else {
174
+ bodyContent = visibleText.map((line, index) => /* @__PURE__ */ jsx(Text, { children: line.length > 0 ? line : " " }, `${offset + index}`));
175
+ }
114
176
  return /* @__PURE__ */ jsx(FullScreenBox, { position: "absolute", justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs(
115
177
  Box,
116
178
  {
@@ -123,15 +185,24 @@ export function TransactionsPreviewDialog({ record, frameWidth }) {
123
185
  backgroundColor: "black",
124
186
  children: [
125
187
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Transaction preview" }),
126
- record.Name ? /* @__PURE__ */ jsx(Text, { children: record.Name }) : null,
127
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "Press Escape to close. Use \u2191/\u2193 or PgUp/PgDn to scroll." }),
128
- /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", height: contentHeight, children: previewEntries.length === 0 ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "No data available." }) : visibleItems.map(
129
- (item) => item.type === "spacer" ? /* @__PURE__ */ jsx(Text, { children: " " }, item.key) : /* @__PURE__ */ jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [
130
- /* @__PURE__ */ jsx(Box, { width: keyColumnWidth, marginRight: 1, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { bold: item.lineIndex === 0, wrap: "truncate-end", children: item.lineIndex === 0 ? item.entryKey.padEnd(keyColumnWidth) : "" }) }),
131
- /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: item.line }) })
132
- ] }, item.key)
133
- ) }),
134
- previewEntries.length > 0 && previewItems.length > contentHeight ? /* @__PURE__ */ jsx(Text, { color: "gray", children: summaryText }) : null
188
+ record?.Name ? /* @__PURE__ */ jsx(Text, { children: record.Name }) : null,
189
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
190
+ /* @__PURE__ */ jsx(Shortcut, { keyboard: "esc", command: "Close preview" }),
191
+ " \xB7 ",
192
+ /* @__PURE__ */ jsx(Shortcut, { keyboard: "\u2190/\u2192", command: "Change focus" }),
193
+ " \xB7",
194
+ " ",
195
+ /* @__PURE__ */ jsx(Shortcut, { keyboard: "\u2191/\u2193", command: "Scroll" }),
196
+ " \xB7 ",
197
+ /* @__PURE__ */ jsx(Shortcut, { keyboard: "PgUp/PgDn", command: "Scroll faster" }),
198
+ " \xB7",
199
+ " ",
200
+ /* @__PURE__ */ jsx(Shortcut, { keyboard: "s", command: "Cycle status" }),
201
+ " \xB7 ",
202
+ /* @__PURE__ */ jsx(Shortcut, { keyboard: "Ctrl+S", command: "Save" })
203
+ ] }),
204
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", height: contentHeight, children: bodyContent }),
205
+ showScrollSummary ? /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx(Text, { color: "gray", children: summaryText }) }) : null
135
206
  ]
136
207
  }
137
208
  ) });
@@ -9,7 +9,6 @@ const STATUS_COLUMN_MIN_WIDTH = (() => {
9
9
  let longest = labels.reduce((max, label) => Math.max(max, label.length), 0);
10
10
  return Math.max("Status".length, longest + STATUS_DIRTY_INDICATOR.length);
11
11
  })();
12
- export const ERROR_COLUMN_MAX_LINES = 5;
13
12
  export function sanitizeCellValue(value) {
14
13
  let s = String(value ?? "");
15
14
  s = s.replace(/[\r\n]+/g, " ").replace(/\t/g, " ");
@@ -28,55 +27,6 @@ export function truncateCellValue(value, width) {
28
27
  }
29
28
  return `${value.slice(0, width - 1)}\u2026`;
30
29
  }
31
- export function wrapCellValue(value, width, maxLines) {
32
- if (width <= 0 || maxLines <= 0) {
33
- return [];
34
- }
35
- let sanitized = sanitizeCellValue(value);
36
- if (sanitized.length === 0) {
37
- return ["".padEnd(width, " ")];
38
- }
39
- let remaining = sanitized;
40
- let lines = [];
41
- while (remaining.length > 0 && lines.length < maxLines) {
42
- if (remaining.length <= width) {
43
- lines.push(remaining.padEnd(width, " "));
44
- remaining = "";
45
- break;
46
- }
47
- let sliceEnd = Math.min(width, remaining.length);
48
- let breakIndex = remaining.lastIndexOf(" ", sliceEnd);
49
- if (breakIndex <= 0) {
50
- breakIndex = sliceEnd;
51
- }
52
- let chunk = remaining.slice(0, breakIndex).trimEnd();
53
- if (chunk.length === 0) {
54
- chunk = remaining.slice(0, width);
55
- breakIndex = chunk.length;
56
- }
57
- lines.push(chunk.padEnd(width, " "));
58
- remaining = remaining.slice(breakIndex).trimStart();
59
- }
60
- if (remaining.length > 0) {
61
- let ellipsisLine;
62
- if (lines.length === 0) {
63
- let truncated = remaining.slice(0, Math.max(width - 1, 0));
64
- ellipsisLine = `${truncated}${width > 0 ? "\u2026" : ""}`;
65
- } else {
66
- let last = lines.pop() ?? "";
67
- let trimmed = last.trimEnd();
68
- if (trimmed.length >= width) {
69
- trimmed = trimmed.slice(0, Math.max(width - 1, 0));
70
- }
71
- ellipsisLine = `${trimmed}${width > 0 ? "\u2026" : ""}`;
72
- }
73
- lines.push(ellipsisLine.padEnd(width, " "));
74
- }
75
- if (lines.length === 0) {
76
- lines.push("".padEnd(width, " "));
77
- }
78
- return lines;
79
- }
80
30
  export function calculateColumnWidths({
81
31
  columns,
82
32
  rows,
@@ -156,35 +106,16 @@ export function formatRowLines({
156
106
  widths,
157
107
  separator = DEFAULT_TABLE_SEPARATOR
158
108
  }) {
159
- let columnLines = columns.map((column, index) => {
109
+ let parts = columns.map((column, index) => {
160
110
  let width = widths[index];
161
- if (column.wrap) {
162
- let maxLines = column.maxWrapLines ?? 1;
163
- return wrapCellValue(String(row[column.key] ?? ""), width, maxLines);
164
- }
165
111
  let value = sanitizeCellValue(row[column.key]);
166
- return [truncateCellValue(value, width)];
112
+ return truncateCellValue(value, width);
167
113
  });
168
- let height = columnLines.reduce((max, lines2) => Math.max(max, lines2.length), 0);
169
- let lines = [];
170
- for (let lineIndex = 0; lineIndex < height; lineIndex++) {
171
- let parts = columnLines.map((column, index) => {
172
- let width = widths[index];
173
- let value = column[lineIndex] ?? "";
174
- if (value.length < width) {
175
- return value.padEnd(width, " ");
176
- }
177
- if (value.length > width) {
178
- return truncateCellValue(value, width);
179
- }
180
- return value;
181
- });
182
- lines.push(parts.join(separator));
183
- }
184
- if (lines.length === 0) {
185
- lines.push(columns.map((_, index) => "".padEnd(widths[index], " ")).join(separator));
114
+ let line = parts.join(separator);
115
+ if (line.length === 0) {
116
+ return [columns.map((_, index) => "".padEnd(widths[index], " ")).join(separator)];
186
117
  }
187
- return lines;
118
+ return [line];
188
119
  }
189
120
  export function addViewportEllipsis(line) {
190
121
  if (line.length === 0) {
@@ -233,8 +164,8 @@ export function createStatusLine(message, stats, lineWidth, prefix = "") {
233
164
  export function createTransactionColumns(showError) {
234
165
  let columns = [
235
166
  { key: "Namespace", header: "Namespace", min: 10, max: 24, weight: 0, shrinkPriority: 3 },
236
- { key: "Name", header: "Name", min: 4, max: 40, weight: 1, shrinkPriority: 1 },
237
- { key: "CreatedBy", header: "CreatedBy", min: 8, max: 20, weight: 0, shrinkPriority: 4 },
167
+ { key: "Name", header: "Name", min: 32, max: 40, weight: 1, shrinkPriority: 1 },
168
+ { key: "CreatedBy", header: "CreatedBy", min: 13, max: 20, weight: 0, shrinkPriority: 4 },
238
169
  {
239
170
  key: "Status",
240
171
  header: "Status",
@@ -251,9 +182,7 @@ export function createTransactionColumns(showError) {
251
182
  min: 20,
252
183
  max: 120,
253
184
  weight: 2,
254
- shrinkPriority: 0,
255
- wrap: true,
256
- maxWrapLines: ERROR_COLUMN_MAX_LINES
185
+ shrinkPriority: 0
257
186
  });
258
187
  }
259
188
  return columns;
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ import { useInput } from "ink";
3
+ import { useCallback, useMemo, useRef } from "react";
4
+ const HOME_SEQUENCES = /* @__PURE__ */ new Set(["\x1B[H", "\x1B[1~", "\x1BOH"]);
5
+ const END_SEQUENCES = /* @__PURE__ */ new Set(["\x1B[F", "\x1B[4~", "\x1BOF"]);
6
+ const PAGE_UP_SEQUENCES = /* @__PURE__ */ new Set(["\x1B[5~"]);
7
+ const PAGE_DOWN_SEQUENCES = /* @__PURE__ */ new Set(["\x1B[6~"]);
8
+ const KEY_ALIASES = {
9
+ leftarrow: "left",
10
+ rightarrow: "right",
11
+ uparrow: "up",
12
+ downarrow: "down",
13
+ return: "enter",
14
+ enter: "enter",
15
+ esc: "escape",
16
+ spacebar: "space",
17
+ pagedown: "pagedown",
18
+ pageup: "pageup"
19
+ };
20
+ const RELAXED_KEYS = /* @__PURE__ */ new Set([
21
+ "escape",
22
+ "left",
23
+ "right",
24
+ "up",
25
+ "down",
26
+ "home",
27
+ "end",
28
+ "pageup",
29
+ "pagedown",
30
+ "tab",
31
+ "enter",
32
+ "delete",
33
+ "backspace"
34
+ ]);
35
+ function parseShortcut(shortcut) {
36
+ let tokens = shortcut.trim().toLowerCase().split("+").map((token) => token.trim()).filter(Boolean);
37
+ let definition = {
38
+ key: null,
39
+ ctrl: false,
40
+ meta: false,
41
+ shift: false
42
+ };
43
+ for (let token of tokens) {
44
+ if (token === "ctrl" || token === "control") {
45
+ definition.ctrl = true;
46
+ continue;
47
+ }
48
+ if (token === "meta" || token === "cmd" || token === "command") {
49
+ definition.meta = true;
50
+ continue;
51
+ }
52
+ if (token === "shift") {
53
+ definition.shift = true;
54
+ continue;
55
+ }
56
+ let alias = KEY_ALIASES[token];
57
+ definition.key = alias ?? token;
58
+ }
59
+ return definition;
60
+ }
61
+ function getInputKey(input, key) {
62
+ if (key.escape) {
63
+ return "escape";
64
+ }
65
+ if (key.leftArrow) {
66
+ return "left";
67
+ }
68
+ if (key.rightArrow) {
69
+ return "right";
70
+ }
71
+ if (key.upArrow) {
72
+ return "up";
73
+ }
74
+ if (key.downArrow) {
75
+ return "down";
76
+ }
77
+ if (key.pageUp || PAGE_UP_SEQUENCES.has(input)) {
78
+ return "pageup";
79
+ }
80
+ if (key.pageDown || PAGE_DOWN_SEQUENCES.has(input)) {
81
+ return "pagedown";
82
+ }
83
+ if (key.return) {
84
+ return "enter";
85
+ }
86
+ if (key.delete) {
87
+ return "delete";
88
+ }
89
+ if (key.backspace) {
90
+ return "backspace";
91
+ }
92
+ if (key.tab) {
93
+ return "tab";
94
+ }
95
+ if (key.shift && !input) {
96
+ return null;
97
+ }
98
+ if (input === " ") {
99
+ return "space";
100
+ }
101
+ if (HOME_SEQUENCES.has(input) || (key.home ?? false)) {
102
+ return "home";
103
+ }
104
+ if (END_SEQUENCES.has(input) || (key.end ?? false)) {
105
+ return "end";
106
+ }
107
+ if (input) {
108
+ return input.toLowerCase();
109
+ }
110
+ return null;
111
+ }
112
+ function matchesShortcut(definition, input, key) {
113
+ let actualKey = getInputKey(input, key);
114
+ let relaxed = actualKey !== null && RELAXED_KEYS.has(actualKey);
115
+ if (definition.ctrl && !key.ctrl) return false;
116
+ if (definition.meta && !key.meta) return false;
117
+ if (definition.shift && !key.shift) return false;
118
+ if (!relaxed) {
119
+ if (!definition.ctrl && key.ctrl) return false;
120
+ if (!definition.meta && key.meta) return false;
121
+ if (!definition.shift && key.shift) return false;
122
+ }
123
+ if (definition.key === null) {
124
+ return actualKey !== null;
125
+ }
126
+ return actualKey === definition.key;
127
+ }
128
+ function normalizeShortcuts(shortcuts) {
129
+ let entries = Array.isArray(shortcuts) ? shortcuts : [shortcuts];
130
+ return entries.map(parseShortcut);
131
+ }
132
+ export function useCommand(shortcut, handler, enabled = true) {
133
+ let definitions = useMemo(() => normalizeShortcuts(shortcut), [shortcut]);
134
+ let callback = useRef(handler);
135
+ callback.current = handler;
136
+ let inputHandler = useCallback(
137
+ (input, key) => {
138
+ for (let definition of definitions) {
139
+ if (matchesShortcut(definition, input, key)) {
140
+ callback.current();
141
+ return;
142
+ }
143
+ }
144
+ },
145
+ [definitions]
146
+ );
147
+ useInput(inputHandler, { isActive: enabled });
148
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { useCommand } from "./useCommand.js";
4
+ export function useScrollableText({
5
+ textLength,
6
+ viewportHeight,
7
+ signature
8
+ }) {
9
+ let [scrollOffset, setScrollOffset] = useState(0);
10
+ let lastSignatureRef = useRef(signature);
11
+ useEffect(() => {
12
+ if (lastSignatureRef.current !== signature) {
13
+ lastSignatureRef.current = signature;
14
+ setScrollOffset(0);
15
+ }
16
+ }, [signature]);
17
+ let maxScrollOffset = useMemo(() => Math.max((textLength || 1) - viewportHeight, 0), [textLength, viewportHeight]);
18
+ useEffect(() => {
19
+ setScrollOffset((offset) => Math.min(offset, maxScrollOffset));
20
+ }, [maxScrollOffset]);
21
+ let canScroll = textLength > viewportHeight && viewportHeight > 0;
22
+ let pageStep = useMemo(() => Math.max(viewportHeight - 1, 1), [viewportHeight]);
23
+ useCommand("up", () => setScrollOffset((offset) => Math.max(offset - 1, 0)), canScroll);
24
+ useCommand("down", () => setScrollOffset((offset) => Math.min(offset + 1, maxScrollOffset)), canScroll);
25
+ useCommand("pageup", () => setScrollOffset((offset) => Math.max(offset - pageStep, 0)), canScroll);
26
+ useCommand("pagedown", () => setScrollOffset((offset) => Math.min(offset + pageStep, maxScrollOffset)), canScroll);
27
+ return { scrollOffset, canScroll, maxScrollOffset, pageStep };
28
+ }