@nmakarov/cli-toolkit 0.1.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 +490 -0
- package/dist/args.cjs +444 -0
- package/dist/args.cjs.map +1 -0
- package/dist/args.js +422 -0
- package/dist/args.js.map +1 -0
- package/dist/errors.cjs +68 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +1811 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +1751 -0
- package/dist/index.js.map +1 -0
- package/dist/params.cjs +377 -0
- package/dist/params.cjs.map +1 -0
- package/dist/params.js +336 -0
- package/dist/params.js.map +1 -0
- package/dist/screen.cjs +1068 -0
- package/dist/screen.cjs.map +1 -0
- package/dist/screen.js +1007 -0
- package/dist/screen.js.map +1 -0
- package/package.json +122 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1811 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Args: () => Args,
|
|
34
|
+
Divider: () => Divider,
|
|
35
|
+
FooterPresets: () => FooterPresets,
|
|
36
|
+
GridCell: () => GridCell,
|
|
37
|
+
InputField: () => InputField,
|
|
38
|
+
ListComponent: () => ListComponent,
|
|
39
|
+
ListItem: () => ListItem,
|
|
40
|
+
MultiColumnListComponent: () => MultiColumnListComponent,
|
|
41
|
+
MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
|
|
42
|
+
Params: () => Params,
|
|
43
|
+
ScreenBody: () => ScreenBody,
|
|
44
|
+
ScreenContainer: () => ScreenContainer,
|
|
45
|
+
ScreenDivider: () => ScreenDivider,
|
|
46
|
+
ScreenFooter: () => ScreenFooter,
|
|
47
|
+
ScreenRow: () => ScreenRow,
|
|
48
|
+
ScreenTitle: () => ScreenTitle,
|
|
49
|
+
TextBlock: () => TextBlock,
|
|
50
|
+
buildBreadcrumb: () => buildBreadcrumb,
|
|
51
|
+
buildDetailBreadcrumb: () => buildDetailBreadcrumb,
|
|
52
|
+
buildFooter: () => buildFooter,
|
|
53
|
+
getArgsInstance: () => getArgsInstance,
|
|
54
|
+
getParamsInstance: () => getParamsInstance,
|
|
55
|
+
joiEdateType: () => joiEdateType,
|
|
56
|
+
joiStringArrayType: () => joiStringArrayType,
|
|
57
|
+
organizeFooterMessages: () => organizeFooterMessages,
|
|
58
|
+
showListScreen: () => showListScreen,
|
|
59
|
+
showMenuScreen: () => showMenuScreen,
|
|
60
|
+
showMultiColumnListScreen: () => showMultiColumnListScreen,
|
|
61
|
+
showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
|
|
62
|
+
showScreen: () => showScreen,
|
|
63
|
+
showWordGridScreen: () => showWordGridScreen
|
|
64
|
+
});
|
|
65
|
+
module.exports = __toCommonJS(src_exports);
|
|
66
|
+
|
|
67
|
+
// src/args/index.ts
|
|
68
|
+
var import_fs = require("fs");
|
|
69
|
+
var import_path = require("path");
|
|
70
|
+
var import_dotenv = require("dotenv");
|
|
71
|
+
var Args = class {
|
|
72
|
+
args = {};
|
|
73
|
+
flags = {};
|
|
74
|
+
options = {};
|
|
75
|
+
commands = [];
|
|
76
|
+
usedKeys = /* @__PURE__ */ new Set();
|
|
77
|
+
aliases = {};
|
|
78
|
+
overrides = {};
|
|
79
|
+
defaults = {};
|
|
80
|
+
prefixes = [];
|
|
81
|
+
nots = [];
|
|
82
|
+
configValues = {};
|
|
83
|
+
configsLoaded = [];
|
|
84
|
+
env = "local";
|
|
85
|
+
constructor(config2 = {}) {
|
|
86
|
+
this.aliases = config2.aliases || {};
|
|
87
|
+
this.overrides = config2.overrides || {};
|
|
88
|
+
this.defaults = config2.defaults || {};
|
|
89
|
+
this.prefixes = config2.prefixes || ["not", "no"];
|
|
90
|
+
const args = config2.args || process.argv.slice(2);
|
|
91
|
+
this.parseArgs(args);
|
|
92
|
+
this.env = this.get("env")?.toLowerCase() || "local";
|
|
93
|
+
this.loadDotEnv();
|
|
94
|
+
this.loadConfigFiles();
|
|
95
|
+
this.checkConflicts();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse command line arguments
|
|
99
|
+
*/
|
|
100
|
+
parseArgs(args) {
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < args.length) {
|
|
103
|
+
const arg = args[i];
|
|
104
|
+
if (arg.startsWith("--")) {
|
|
105
|
+
const [key, value] = this.parseLongOption(arg);
|
|
106
|
+
this.setValue(key, value);
|
|
107
|
+
i++;
|
|
108
|
+
} else if (arg.startsWith("-")) {
|
|
109
|
+
const result = this.parseShortOption(arg, args, i);
|
|
110
|
+
if (result.consumed > 0) {
|
|
111
|
+
i += result.consumed;
|
|
112
|
+
} else {
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this.commands.push(arg);
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parse long option (--key=value or --key)
|
|
123
|
+
*/
|
|
124
|
+
parseLongOption(arg) {
|
|
125
|
+
const key = arg.slice(2);
|
|
126
|
+
const prefix = this.prefixes.find((p) => key.startsWith(p));
|
|
127
|
+
if (prefix) {
|
|
128
|
+
let strippedKey = key.slice(prefix.length);
|
|
129
|
+
if (strippedKey.startsWith("-")) {
|
|
130
|
+
strippedKey = strippedKey.slice(1);
|
|
131
|
+
}
|
|
132
|
+
this.nots.push(key);
|
|
133
|
+
return [strippedKey, false];
|
|
134
|
+
}
|
|
135
|
+
if (key.includes("=")) {
|
|
136
|
+
const eqIndex = key.indexOf("=");
|
|
137
|
+
const optionKey = key.slice(0, eqIndex);
|
|
138
|
+
const value = key.slice(eqIndex + 1);
|
|
139
|
+
return [optionKey, this.parseValue(value)];
|
|
140
|
+
} else {
|
|
141
|
+
return [key, true];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Parse short option (-k=value, -k, or bundled -vsd)
|
|
146
|
+
*/
|
|
147
|
+
parseShortOption(arg, args, index) {
|
|
148
|
+
const key = arg.slice(1);
|
|
149
|
+
if (key.length === 1 && index + 1 < args.length && !args[index + 1].startsWith("-")) {
|
|
150
|
+
const value = args[index + 1];
|
|
151
|
+
this.setValue(key, this.parseValue(value));
|
|
152
|
+
return { consumed: 2 };
|
|
153
|
+
}
|
|
154
|
+
if (key.length > 1 && !key.includes("=")) {
|
|
155
|
+
for (let i = 0; i < key.length; i++) {
|
|
156
|
+
const shortKey = key[i];
|
|
157
|
+
if (shortKey in this.aliases) {
|
|
158
|
+
this.setValue(shortKey, true);
|
|
159
|
+
} else {
|
|
160
|
+
this.args[shortKey] = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { consumed: 1 };
|
|
164
|
+
}
|
|
165
|
+
if (key.includes("=")) {
|
|
166
|
+
const eqIndex = key.indexOf("=");
|
|
167
|
+
const optionKey = key.slice(0, eqIndex);
|
|
168
|
+
const value = key.slice(eqIndex + 1);
|
|
169
|
+
if (optionKey.length > 1) {
|
|
170
|
+
for (let i = 0; i < optionKey.length - 1; i++) {
|
|
171
|
+
const shortKey = optionKey[i];
|
|
172
|
+
if (shortKey in this.aliases) {
|
|
173
|
+
this.setValue(shortKey, true);
|
|
174
|
+
} else {
|
|
175
|
+
this.args[shortKey] = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const lastKey = optionKey[optionKey.length - 1];
|
|
179
|
+
if (lastKey in this.aliases) {
|
|
180
|
+
this.setValue(lastKey, this.parseValue(value));
|
|
181
|
+
} else {
|
|
182
|
+
this.args[lastKey] = this.parseValue(value);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
this.setValue(optionKey, this.parseValue(value));
|
|
186
|
+
}
|
|
187
|
+
return { consumed: 1 };
|
|
188
|
+
} else {
|
|
189
|
+
this.setValue(key, true);
|
|
190
|
+
return { consumed: 1 };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parse value (handle quotes)
|
|
195
|
+
*/
|
|
196
|
+
parseValue(value) {
|
|
197
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
198
|
+
return value.slice(1, -1);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Set a value with proper categorization
|
|
204
|
+
*/
|
|
205
|
+
setValue(key, value) {
|
|
206
|
+
const resolvedKey = this.aliases[key] || key;
|
|
207
|
+
if (typeof value === "boolean") {
|
|
208
|
+
this.flags[resolvedKey] = value;
|
|
209
|
+
} else {
|
|
210
|
+
this.options[resolvedKey] = value;
|
|
211
|
+
}
|
|
212
|
+
this.args[resolvedKey.toLowerCase()] = value;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check for conflicts (short + long form of same option)
|
|
216
|
+
*/
|
|
217
|
+
checkConflicts() {
|
|
218
|
+
const conflicts = [];
|
|
219
|
+
for (const [shortKey, longKey] of Object.entries(this.aliases)) {
|
|
220
|
+
const hasShort = this.args[shortKey] !== void 0;
|
|
221
|
+
const hasLong = this.args[longKey] !== void 0;
|
|
222
|
+
if (hasShort && hasLong) {
|
|
223
|
+
conflicts.push(`Both -${shortKey} and --${longKey} specified`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (conflicts.length > 0) {
|
|
227
|
+
throw new Error(`Argument conflicts: ${conflicts.join(", ")}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get a value with precedence order
|
|
232
|
+
*/
|
|
233
|
+
get(key) {
|
|
234
|
+
const resolvedKey = this.aliases[key] || key;
|
|
235
|
+
this.usedKeys.add(resolvedKey);
|
|
236
|
+
if (this.overrides[resolvedKey] !== void 0) {
|
|
237
|
+
return this.overrides[resolvedKey];
|
|
238
|
+
}
|
|
239
|
+
const lcKey = resolvedKey.toLowerCase();
|
|
240
|
+
const lcKeyWithEnv = `${lcKey}${this.env ? `_${this.env.toLowerCase()}` : ""}`;
|
|
241
|
+
if (this.env && this.args[lcKeyWithEnv] !== void 0) {
|
|
242
|
+
return this.args[lcKeyWithEnv];
|
|
243
|
+
} else if (this.args[lcKey] !== void 0) {
|
|
244
|
+
return this.args[lcKey];
|
|
245
|
+
}
|
|
246
|
+
if (this.configValues[resolvedKey] !== void 0) {
|
|
247
|
+
return this.configValues[resolvedKey];
|
|
248
|
+
}
|
|
249
|
+
const envKey = this.toEnvKey(resolvedKey);
|
|
250
|
+
const envKeyWithEnv = `${envKey}${this.env ? `_${this.env.toUpperCase()}` : ""}`;
|
|
251
|
+
const envSpecificKey = Object.keys(process.env).find(
|
|
252
|
+
(k) => this.env && k.toUpperCase() === envKeyWithEnv
|
|
253
|
+
);
|
|
254
|
+
const envKeyFound = Object.keys(process.env).find((k) => k.toUpperCase() === envKey);
|
|
255
|
+
if (envSpecificKey) {
|
|
256
|
+
return process.env[envSpecificKey];
|
|
257
|
+
} else if (envKeyFound) {
|
|
258
|
+
return process.env[envKeyFound];
|
|
259
|
+
}
|
|
260
|
+
if (this.defaults[resolvedKey] !== void 0) {
|
|
261
|
+
return this.defaults[resolvedKey];
|
|
262
|
+
}
|
|
263
|
+
if (resolvedKey === "env" && process.env.NODE_ENV !== void 0) {
|
|
264
|
+
return process.env.NODE_ENV;
|
|
265
|
+
}
|
|
266
|
+
return void 0;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Set a value (for testing/internal use)
|
|
270
|
+
*/
|
|
271
|
+
set(key, value) {
|
|
272
|
+
this.args[key] = value;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Check if a command exists (case-insensitive)
|
|
276
|
+
*/
|
|
277
|
+
hasCommand(cmd) {
|
|
278
|
+
return this.commands.some((command) => command.toLowerCase() === cmd.toLowerCase());
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get all commands
|
|
282
|
+
*/
|
|
283
|
+
getCommands() {
|
|
284
|
+
return [...this.commands];
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get used keys (as array)
|
|
288
|
+
*/
|
|
289
|
+
getUsed() {
|
|
290
|
+
return Array.from(this.usedKeys);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get unused keys (as array)
|
|
294
|
+
*/
|
|
295
|
+
getUnused() {
|
|
296
|
+
const unused = [];
|
|
297
|
+
for (const key of Object.keys(this.args)) {
|
|
298
|
+
if (!this.usedKeys.has(key) && !this.nots.includes(key)) {
|
|
299
|
+
unused.push(key);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return unused;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Convert key to environment variable format
|
|
306
|
+
*/
|
|
307
|
+
toEnvKey(key) {
|
|
308
|
+
return key.replace(
|
|
309
|
+
/[A-Z0-9]/g,
|
|
310
|
+
(match, offset) => offset === 0 ? match : "_" + match.toLowerCase()
|
|
311
|
+
).toUpperCase();
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Load .env file
|
|
315
|
+
*/
|
|
316
|
+
loadDotEnv() {
|
|
317
|
+
const dotEnvPath = this.get("dotEnvPath") || process.cwd();
|
|
318
|
+
const dotEnvFile = this.get("dotEnvFile") || ".env";
|
|
319
|
+
if (this.get("dotEnvFile")) {
|
|
320
|
+
const customPath = (0, import_path.resolve)(dotEnvPath, dotEnvFile);
|
|
321
|
+
if ((0, import_fs.existsSync)(customPath)) {
|
|
322
|
+
(0, import_dotenv.config)({ path: customPath, quiet: true });
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
let dotEnvPathFile = null;
|
|
327
|
+
const envSpecificFile = `.env.${this.env}`;
|
|
328
|
+
const envSpecificPath = (0, import_path.resolve)(dotEnvPath, envSpecificFile);
|
|
329
|
+
if ((0, import_fs.existsSync)(envSpecificPath)) {
|
|
330
|
+
dotEnvPathFile = envSpecificPath;
|
|
331
|
+
}
|
|
332
|
+
if (!dotEnvPathFile && !this.get("dotEnvPath")) {
|
|
333
|
+
const examplesPath = (0, import_path.resolve)(dotEnvPath, "examples");
|
|
334
|
+
const examplesEnvSpecificPath = (0, import_path.resolve)(examplesPath, envSpecificFile);
|
|
335
|
+
if ((0, import_fs.existsSync)(examplesEnvSpecificPath)) {
|
|
336
|
+
dotEnvPathFile = examplesEnvSpecificPath;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!dotEnvPathFile) {
|
|
340
|
+
dotEnvPathFile = (0, import_path.resolve)(dotEnvPath, dotEnvFile);
|
|
341
|
+
if (!(0, import_fs.existsSync)(dotEnvPathFile)) {
|
|
342
|
+
if (!this.get("dotEnvPath")) {
|
|
343
|
+
const examplesPath = (0, import_path.resolve)(dotEnvPath, "examples");
|
|
344
|
+
const examplesEnvFile = (0, import_path.resolve)(examplesPath, dotEnvFile);
|
|
345
|
+
if ((0, import_fs.existsSync)(examplesEnvFile)) {
|
|
346
|
+
dotEnvPathFile = examplesEnvFile;
|
|
347
|
+
} else {
|
|
348
|
+
dotEnvPathFile = (0, import_path.resolve)(dotEnvPath, "..", dotEnvFile);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (dotEnvPathFile && (0, import_fs.existsSync)(dotEnvPathFile)) {
|
|
354
|
+
(0, import_dotenv.config)({ path: dotEnvPathFile, quiet: true });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Load configuration files
|
|
359
|
+
*/
|
|
360
|
+
loadConfigFiles() {
|
|
361
|
+
this.configsLoaded = [];
|
|
362
|
+
this.configValues = {};
|
|
363
|
+
const _defaultConfigExtension = this.get("defaultConfigExtension") || "js";
|
|
364
|
+
const optConfigFiles = this.get("config") || this.get("configs") || "";
|
|
365
|
+
const configFiles = optConfigFiles ? optConfigFiles.split(/,\s*/) : [];
|
|
366
|
+
const optConfigFilePath = this.get("configPath");
|
|
367
|
+
if (configFiles.length > 0) {
|
|
368
|
+
for (const cfgFile of configFiles) {
|
|
369
|
+
let notLoaded = false;
|
|
370
|
+
let notLoadedEnvSpecific = false;
|
|
371
|
+
const cfgFileWithPath = this.resolveFileWithPath(optConfigFilePath, cfgFile);
|
|
372
|
+
try {
|
|
373
|
+
const cfgContents = this.requireConfigFile(cfgFileWithPath);
|
|
374
|
+
this.configValues = { ...this.configValues, ...cfgContents };
|
|
375
|
+
this.configsLoaded.push(cfgFileWithPath);
|
|
376
|
+
} catch {
|
|
377
|
+
notLoaded = true;
|
|
378
|
+
}
|
|
379
|
+
const cfgEnvFileWithPath = this.resolveFileWithPath(
|
|
380
|
+
optConfigFilePath,
|
|
381
|
+
cfgFile,
|
|
382
|
+
this.env
|
|
383
|
+
);
|
|
384
|
+
if (cfgEnvFileWithPath !== cfgFileWithPath) {
|
|
385
|
+
try {
|
|
386
|
+
const cfgContents = this.requireConfigFile(cfgEnvFileWithPath);
|
|
387
|
+
this.configValues = { ...this.configValues, ...cfgContents };
|
|
388
|
+
this.configsLoaded.push(cfgEnvFileWithPath);
|
|
389
|
+
} catch {
|
|
390
|
+
notLoadedEnvSpecific = true;
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
notLoadedEnvSpecific = true;
|
|
394
|
+
}
|
|
395
|
+
if (notLoaded && notLoadedEnvSpecific) {
|
|
396
|
+
throw new Error(`can't load config file "${cfgFileWithPath}"`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Resolve file path with environment-specific naming
|
|
403
|
+
*/
|
|
404
|
+
resolveFileWithPath(optConfigFilePath, cfgFile, env) {
|
|
405
|
+
let cfgFileWithPath = optConfigFilePath ? (0, import_path.isAbsolute)(optConfigFilePath) ? (0, import_path.resolve)(optConfigFilePath, cfgFile) : (0, import_path.resolve)(process.cwd(), optConfigFilePath, cfgFile) : (0, import_path.isAbsolute)(cfgFile) ? cfgFile : (0, import_path.resolve)(process.cwd(), cfgFile);
|
|
406
|
+
const { basePathWithName, extension } = this.splitPath(cfgFileWithPath);
|
|
407
|
+
if (env) {
|
|
408
|
+
cfgFileWithPath = `${basePathWithName}.${env}.${extension || "js"}`;
|
|
409
|
+
} else {
|
|
410
|
+
cfgFileWithPath = `${basePathWithName}.${extension || "js"}`;
|
|
411
|
+
}
|
|
412
|
+
return cfgFileWithPath;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Split file path into base path and extension
|
|
416
|
+
*/
|
|
417
|
+
splitPath(filePath) {
|
|
418
|
+
const basePathWithName = (0, import_path.join)((0, import_path.dirname)(filePath), (0, import_path.basename)(filePath, (0, import_path.extname)(filePath)));
|
|
419
|
+
const extension = (0, import_path.extname)(filePath).slice(1);
|
|
420
|
+
return { basePathWithName, extension };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Require a configuration file (supports .js and .json)
|
|
424
|
+
*/
|
|
425
|
+
requireConfigFile(filePath) {
|
|
426
|
+
if (!(0, import_fs.existsSync)(filePath)) {
|
|
427
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
428
|
+
}
|
|
429
|
+
const ext = (0, import_path.extname)(filePath).toLowerCase();
|
|
430
|
+
if (ext === ".json") {
|
|
431
|
+
const content = (0, import_fs.readFileSync)(filePath, "utf8");
|
|
432
|
+
return JSON.parse(content);
|
|
433
|
+
} else if (ext === ".js") {
|
|
434
|
+
try {
|
|
435
|
+
delete require.cache[require.resolve(filePath)];
|
|
436
|
+
return require(filePath);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
throw new Error(`Failed to load JS config file: ${error instanceof Error ? error.message : String(error)}`);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
throw new Error(`Unsupported file extension: ${ext}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Get all parsed data
|
|
446
|
+
*/
|
|
447
|
+
getParsed() {
|
|
448
|
+
return {
|
|
449
|
+
command: this.commands[0] || "",
|
|
450
|
+
flags: { ...this.flags },
|
|
451
|
+
options: { ...this.options },
|
|
452
|
+
usedKeys: new Set(this.usedKeys)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Set prefixes dynamically and re-parse arguments (like legacy)
|
|
457
|
+
*/
|
|
458
|
+
setPrefixes(prefixes) {
|
|
459
|
+
const arr = Array.isArray(prefixes) ? prefixes : prefixes.split(/,\s*/);
|
|
460
|
+
const sortedArr = arr.sort(
|
|
461
|
+
(a, b) => a.length < b.length ? 1 : a.length > b.length ? -1 : 0
|
|
462
|
+
);
|
|
463
|
+
this.prefixes = sortedArr.map((el) => el.toLowerCase());
|
|
464
|
+
const args = process.argv.slice(2);
|
|
465
|
+
this.parseArgs(args);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
var instance = null;
|
|
469
|
+
function getArgsInstance() {
|
|
470
|
+
return instance;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/params/index.ts
|
|
474
|
+
var import_joi = __toESM(require("joi"), 1);
|
|
475
|
+
|
|
476
|
+
// src/errors.ts
|
|
477
|
+
var FrameworkError = class extends Error {
|
|
478
|
+
constructor(message) {
|
|
479
|
+
super(message);
|
|
480
|
+
this.name = "FrameworkError";
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
var ParamError = class extends FrameworkError {
|
|
484
|
+
constructor(message) {
|
|
485
|
+
super(message);
|
|
486
|
+
this.name = "ParamError";
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// src/params/custom-types.ts
|
|
491
|
+
var joiEdateType = (value, helpers) => {
|
|
492
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
|
|
493
|
+
const testDate = new Date(value);
|
|
494
|
+
if (!isNaN(testDate.getTime())) {
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (value instanceof Date) {
|
|
499
|
+
return value.toISOString();
|
|
500
|
+
}
|
|
501
|
+
if (typeof value !== "string") {
|
|
502
|
+
value = String(value);
|
|
503
|
+
}
|
|
504
|
+
if (value.toLowerCase() === "now") {
|
|
505
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
506
|
+
}
|
|
507
|
+
const referenceRegex = /^@(\w+)([+-]\d+[smhdwy])$/i;
|
|
508
|
+
const referenceMatch = value.match(referenceRegex);
|
|
509
|
+
if (referenceMatch) {
|
|
510
|
+
const [, paramName, relativeExpr] = referenceMatch;
|
|
511
|
+
const context = helpers.prefs?.context;
|
|
512
|
+
if (!context || !context.params) {
|
|
513
|
+
throw new ParamError(`Cannot resolve cross-parameter reference @${paramName}: context not available. Ensure parameters are processed with proper context.`);
|
|
514
|
+
}
|
|
515
|
+
const referencedValue = context.params[paramName];
|
|
516
|
+
if (referencedValue === void 0 || referencedValue === null) {
|
|
517
|
+
throw new ParamError(`Cannot resolve @${paramName}: parameter "${paramName}" is not defined or has no value. Parameters are evaluated left-to-right.`);
|
|
518
|
+
}
|
|
519
|
+
let referenceDate;
|
|
520
|
+
if (referencedValue instanceof Date) {
|
|
521
|
+
referenceDate = referencedValue;
|
|
522
|
+
} else if (typeof referencedValue === "string") {
|
|
523
|
+
referenceDate = new Date(referencedValue);
|
|
524
|
+
if (isNaN(referenceDate.getTime())) {
|
|
525
|
+
throw new ParamError(`Referenced parameter @${paramName} has invalid date value: ${referencedValue}`);
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
throw new ParamError(`Referenced parameter @${paramName} is not a valid date type (found: ${typeof referencedValue})`);
|
|
529
|
+
}
|
|
530
|
+
const relativeMatch2 = relativeExpr.match(/^([+-])(\d+)([smhdwy])$/i);
|
|
531
|
+
if (!relativeMatch2) {
|
|
532
|
+
throw new ParamError(`Invalid relative time expression in @${paramName}${relativeExpr}`);
|
|
533
|
+
}
|
|
534
|
+
const [, sign, amount, unit] = relativeMatch2;
|
|
535
|
+
const offset = calculateTimeOffset(parseInt(amount, 10), unit, sign);
|
|
536
|
+
const resultDate = new Date(referenceDate.getTime() + offset);
|
|
537
|
+
return resultDate.toISOString();
|
|
538
|
+
}
|
|
539
|
+
const relativeTimeRegex = /^([+-])(\d+)([smhdwy])$/i;
|
|
540
|
+
const relativeMatch = value.match(relativeTimeRegex);
|
|
541
|
+
if (relativeMatch) {
|
|
542
|
+
const [, sign, amount, unit] = relativeMatch;
|
|
543
|
+
const numAmount = parseInt(amount, 10);
|
|
544
|
+
if (isNaN(numAmount)) {
|
|
545
|
+
throw new ParamError(`Invalid relative time amount: ${amount}`);
|
|
546
|
+
}
|
|
547
|
+
const offset = calculateTimeOffset(numAmount, unit, sign);
|
|
548
|
+
const resultDate = new Date(Date.now() + offset);
|
|
549
|
+
return resultDate.toISOString();
|
|
550
|
+
}
|
|
551
|
+
const parsedDate = new Date(value);
|
|
552
|
+
if (isNaN(parsedDate.getTime())) {
|
|
553
|
+
throw new ParamError(`Invalid date format: ${value}. Expected a valid date string, "now", relative time expression (e.g., "-2h", "+1d"), or cross-parameter reference (e.g., "@startTime+2h")`);
|
|
554
|
+
}
|
|
555
|
+
return parsedDate.toISOString();
|
|
556
|
+
};
|
|
557
|
+
function calculateTimeOffset(amount, unit, sign) {
|
|
558
|
+
let multiplier = 1;
|
|
559
|
+
switch (unit.toLowerCase()) {
|
|
560
|
+
case "s":
|
|
561
|
+
multiplier = 1e3;
|
|
562
|
+
break;
|
|
563
|
+
case "m":
|
|
564
|
+
multiplier = 60 * 1e3;
|
|
565
|
+
break;
|
|
566
|
+
case "h":
|
|
567
|
+
multiplier = 60 * 60 * 1e3;
|
|
568
|
+
break;
|
|
569
|
+
case "d":
|
|
570
|
+
multiplier = 24 * 60 * 60 * 1e3;
|
|
571
|
+
break;
|
|
572
|
+
case "w":
|
|
573
|
+
multiplier = 7 * 24 * 60 * 60 * 1e3;
|
|
574
|
+
break;
|
|
575
|
+
case "y":
|
|
576
|
+
multiplier = 365 * 24 * 60 * 60 * 1e3;
|
|
577
|
+
break;
|
|
578
|
+
default:
|
|
579
|
+
throw new ParamError(`Invalid time unit: ${unit}. Supported units: s, m, h, d, w, y`);
|
|
580
|
+
}
|
|
581
|
+
return sign === "+" ? amount * multiplier : -amount * multiplier;
|
|
582
|
+
}
|
|
583
|
+
var joiStringArrayType = (type) => (value, helpers) => {
|
|
584
|
+
if (value === void 0 || typeof value === "function") {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
const arr = value.split(/,\s*/).map((el) => {
|
|
588
|
+
if (type === "number") {
|
|
589
|
+
const v = parseInt(el, 10);
|
|
590
|
+
if (isNaN(v)) {
|
|
591
|
+
throw new ParamError(`array element "${el}" should be numeric`);
|
|
592
|
+
}
|
|
593
|
+
return v;
|
|
594
|
+
} else if (type === "boolean") {
|
|
595
|
+
const v = el.match(/true|t|yes|1/i) ? true : el.match(/false|f|no|0/i) ? false : null;
|
|
596
|
+
if (v === null) {
|
|
597
|
+
throw new ParamError(`array element "${el}" should be boolean`);
|
|
598
|
+
}
|
|
599
|
+
return v;
|
|
600
|
+
} else if (type === "string") {
|
|
601
|
+
return el;
|
|
602
|
+
} else {
|
|
603
|
+
throw new ParamError(`unknown type "${type}" for array elements`);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
return arr;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/params/index.ts
|
|
610
|
+
var Params = class {
|
|
611
|
+
params = {};
|
|
612
|
+
definitions = {};
|
|
613
|
+
args;
|
|
614
|
+
paramSetters = [];
|
|
615
|
+
paramGetters = [];
|
|
616
|
+
constructor({ args }, opts = {}) {
|
|
617
|
+
this.args = args;
|
|
618
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
619
|
+
this.params[k] = v;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Assign a parameter definition
|
|
624
|
+
*/
|
|
625
|
+
assignDefinition(key, definition) {
|
|
626
|
+
if (this.definitions[key] && !definition) {
|
|
627
|
+
return this.definitions[key];
|
|
628
|
+
}
|
|
629
|
+
let type;
|
|
630
|
+
if (!definition) {
|
|
631
|
+
type = import_joi.default.string();
|
|
632
|
+
} else if (import_joi.default.isSchema(definition)) {
|
|
633
|
+
type = definition;
|
|
634
|
+
} else if (import_joi.default.isSchema(definition.type)) {
|
|
635
|
+
type = definition.type;
|
|
636
|
+
} else if (typeof definition === "string") {
|
|
637
|
+
type = this.toJoi(definition);
|
|
638
|
+
} else if (typeof definition.type === "string") {
|
|
639
|
+
type = this.toJoi(definition.type);
|
|
640
|
+
} else if (!definition.type) {
|
|
641
|
+
type = import_joi.default.string();
|
|
642
|
+
} else {
|
|
643
|
+
type = import_joi.default.string();
|
|
644
|
+
}
|
|
645
|
+
if (!this.definitions[key]) {
|
|
646
|
+
this.definitions[key] = {};
|
|
647
|
+
}
|
|
648
|
+
this.definitions[key].type = type;
|
|
649
|
+
if (definition && definition.values) {
|
|
650
|
+
if (Array.isArray(definition.values)) {
|
|
651
|
+
this.definitions[key].values = definition.values;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return this.definitions[key];
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Convert string definition to Joi schema
|
|
658
|
+
*/
|
|
659
|
+
toJoi(str) {
|
|
660
|
+
let type;
|
|
661
|
+
if (str.match(/^string|^text/i)) {
|
|
662
|
+
type = import_joi.default.string();
|
|
663
|
+
} else if (str.match(/^number|^integer|^int/i)) {
|
|
664
|
+
type = import_joi.default.number();
|
|
665
|
+
} else if (str.match(/^boolean|^bool/i)) {
|
|
666
|
+
type = import_joi.default.boolean();
|
|
667
|
+
} else if (str.match(/^date/i)) {
|
|
668
|
+
type = import_joi.default.date();
|
|
669
|
+
} else if (str.match(/^edate/i)) {
|
|
670
|
+
type = import_joi.default.custom(joiEdateType);
|
|
671
|
+
} else if (str.match(/^duration/i)) {
|
|
672
|
+
type = import_joi.default.string().isoDuration();
|
|
673
|
+
} else if (str.match(/^array/i)) {
|
|
674
|
+
let elementTypes = "string";
|
|
675
|
+
const tmp = str.match(/\((.*)\)/);
|
|
676
|
+
if (tmp && tmp[1].match(/string/i)) {
|
|
677
|
+
elementTypes = "string";
|
|
678
|
+
} else if (tmp && tmp[1].match(/number|integer|int/i)) {
|
|
679
|
+
elementTypes = "number";
|
|
680
|
+
} else if (tmp && tmp[1].match(/boolean|bool/i)) {
|
|
681
|
+
elementTypes = "boolean";
|
|
682
|
+
}
|
|
683
|
+
type = import_joi.default.custom(joiStringArrayType(elementTypes));
|
|
684
|
+
} else {
|
|
685
|
+
type = import_joi.default.string();
|
|
686
|
+
}
|
|
687
|
+
const regexForDefault = /\bdefault\s+([^\s]+)/;
|
|
688
|
+
const matchForDefault = str.match(regexForDefault);
|
|
689
|
+
if (matchForDefault) {
|
|
690
|
+
const defValObj = type.validate(matchForDefault[1]);
|
|
691
|
+
if (defValObj.error) {
|
|
692
|
+
throw new ParamError(`default value "${defValObj.value}" type mismatch`);
|
|
693
|
+
}
|
|
694
|
+
type = type.default(defValObj.value);
|
|
695
|
+
} else if (str.match(/required/)) {
|
|
696
|
+
type = type.required();
|
|
697
|
+
}
|
|
698
|
+
return type;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Validate a value against a definition
|
|
702
|
+
*/
|
|
703
|
+
validate(key, val, def) {
|
|
704
|
+
const { value, error } = def.type.validate(val, { context: { params: this.params } });
|
|
705
|
+
if (error) {
|
|
706
|
+
const errs = error.details.map((el) => el.message).join(", ");
|
|
707
|
+
throw new ParamError(`"${key}" validation error: ${errs}`);
|
|
708
|
+
}
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Get a parameter value with validation
|
|
713
|
+
*/
|
|
714
|
+
get(key, definition) {
|
|
715
|
+
const def = this.assignDefinition(key, definition);
|
|
716
|
+
let valFromGetters = void 0;
|
|
717
|
+
if (def.volatile || true) {
|
|
718
|
+
valFromGetters = this.runAllRegisteredGetters(key);
|
|
719
|
+
}
|
|
720
|
+
const valFromArgs = this.args.get(key);
|
|
721
|
+
const valFromParams = this.params[key];
|
|
722
|
+
const res = valFromGetters ? this.validate(key, valFromGetters, def) : valFromArgs ? this.validate(key, valFromArgs, def) : this.validate(key, valFromParams, def);
|
|
723
|
+
if (res !== void 0 && def.values && !def.values.includes(res)) {
|
|
724
|
+
throw new ParamError(`key ${key} should be one of ${def.values}`);
|
|
725
|
+
}
|
|
726
|
+
return res;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Set a parameter value with validation
|
|
730
|
+
*/
|
|
731
|
+
set(key, val, definition) {
|
|
732
|
+
if (val && val.type && val.value) {
|
|
733
|
+
definition = val;
|
|
734
|
+
val = val.value;
|
|
735
|
+
}
|
|
736
|
+
const def = this.assignDefinition(key, definition);
|
|
737
|
+
if (!this.runAllRegisteredSetters(key, val)) {
|
|
738
|
+
this.params[key] = val;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Get all parameters from definitions
|
|
743
|
+
* Processes parameters left-to-right to support cross-parameter references
|
|
744
|
+
*/
|
|
745
|
+
getAll(defs) {
|
|
746
|
+
const res = {};
|
|
747
|
+
for (const [k, def] of Object.entries(defs)) {
|
|
748
|
+
const value = this.get(k, def);
|
|
749
|
+
res[k] = value;
|
|
750
|
+
if (value !== void 0) {
|
|
751
|
+
this.params[k] = value;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return res;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Run all registered getters for a key
|
|
758
|
+
*/
|
|
759
|
+
runAllRegisteredGetters(key) {
|
|
760
|
+
let val = null;
|
|
761
|
+
for (const getter of this.paramGetters) {
|
|
762
|
+
val = getter(key, this.definitions[key]);
|
|
763
|
+
if (val !== void 0) {
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return val;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Run all registered setters for a key
|
|
771
|
+
*/
|
|
772
|
+
runAllRegisteredSetters(key, value) {
|
|
773
|
+
let setterUsed = false;
|
|
774
|
+
for (const setter of this.paramSetters) {
|
|
775
|
+
setterUsed = setter(key, value);
|
|
776
|
+
if (setterUsed) {
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return setterUsed;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Register a parameter getter
|
|
784
|
+
*/
|
|
785
|
+
registerParamGetter(fn) {
|
|
786
|
+
this.paramGetters.push(fn);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Register a parameter setter
|
|
790
|
+
*/
|
|
791
|
+
registerParamSetter(fn) {
|
|
792
|
+
this.paramSetters.push(fn);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
var paramsInstance = null;
|
|
796
|
+
var getParamsInstance = () => paramsInstance;
|
|
797
|
+
|
|
798
|
+
// src/screen/screens.ts
|
|
799
|
+
var import_react3 = require("react");
|
|
800
|
+
var import_ink3 = require("ink");
|
|
801
|
+
|
|
802
|
+
// src/screen/components.ts
|
|
803
|
+
var import_react = require("react");
|
|
804
|
+
var import_ink = require("ink");
|
|
805
|
+
function getScreenWidth(maxWidth = null) {
|
|
806
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
807
|
+
const availableWidth = Math.max(20, terminalWidth - 4);
|
|
808
|
+
return maxWidth ? Math.min(availableWidth, maxWidth) : availableWidth;
|
|
809
|
+
}
|
|
810
|
+
function ScreenContainer({ children }) {
|
|
811
|
+
const width = getScreenWidth();
|
|
812
|
+
return (0, import_react.createElement)(import_ink.Box, {
|
|
813
|
+
flexDirection: "column",
|
|
814
|
+
marginTop: 1,
|
|
815
|
+
borderStyle: "single",
|
|
816
|
+
borderColor: "cyan",
|
|
817
|
+
paddingX: 1,
|
|
818
|
+
width
|
|
819
|
+
// Use the calculated width directly
|
|
820
|
+
}, children);
|
|
821
|
+
}
|
|
822
|
+
function ScreenRow({ children }) {
|
|
823
|
+
return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column" }, children);
|
|
824
|
+
}
|
|
825
|
+
function ScreenTitle({ text }) {
|
|
826
|
+
return (0, import_react.createElement)(
|
|
827
|
+
ScreenRow,
|
|
828
|
+
{},
|
|
829
|
+
(0, import_react.createElement)(import_ink.Text, { bold: true, color: "cyan" }, text)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
function ScreenDivider({ width }) {
|
|
833
|
+
const dividerWidth = width || getScreenWidth() - 4;
|
|
834
|
+
return (0, import_react.createElement)(import_ink.Text, { color: "cyan", dimColor: true }, "\u2500".repeat(dividerWidth));
|
|
835
|
+
}
|
|
836
|
+
function ScreenBody({ children, alignItems = "flex-start" }) {
|
|
837
|
+
return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column", alignItems }, children);
|
|
838
|
+
}
|
|
839
|
+
function ScreenFooter({ lines, textStyle }) {
|
|
840
|
+
const defaultTextStyle = {
|
|
841
|
+
dimColor: true,
|
|
842
|
+
color: "white"
|
|
843
|
+
};
|
|
844
|
+
const finalTextStyle = { ...defaultTextStyle, ...textStyle };
|
|
845
|
+
const flattenAndWrap = (items, keyPrefix = "") => {
|
|
846
|
+
const result = [];
|
|
847
|
+
let keyIndex = 0;
|
|
848
|
+
items.forEach((item, index) => {
|
|
849
|
+
if (Array.isArray(item)) {
|
|
850
|
+
const nested = flattenAndWrap(item, `${keyPrefix}-${index}`);
|
|
851
|
+
result.push(...nested);
|
|
852
|
+
} else if (typeof item === "string") {
|
|
853
|
+
result.push(
|
|
854
|
+
(0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, item)
|
|
855
|
+
);
|
|
856
|
+
} else {
|
|
857
|
+
const element = item;
|
|
858
|
+
if (element.key === null || element.key === void 0) {
|
|
859
|
+
result.push(
|
|
860
|
+
(0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, element)
|
|
861
|
+
);
|
|
862
|
+
} else {
|
|
863
|
+
result.push(element);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
return result;
|
|
868
|
+
};
|
|
869
|
+
const wrappedItems = flattenAndWrap(lines);
|
|
870
|
+
return (0, import_react.createElement)(
|
|
871
|
+
import_ink.Box,
|
|
872
|
+
{ flexDirection: "column" },
|
|
873
|
+
(0, import_react.createElement)(import_ink.Box, { flexDirection: "row" }, ...wrappedItems)
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/screen/list-components.ts
|
|
878
|
+
var import_react2 = __toESM(require("react"), 1);
|
|
879
|
+
var import_ink2 = require("ink");
|
|
880
|
+
var h2 = import_react2.createElement;
|
|
881
|
+
function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
|
|
882
|
+
const [, forceUpdate] = (0, import_react2.useState)({});
|
|
883
|
+
const termWidth = (process.stdout.columns || 80) - 8;
|
|
884
|
+
const maxItemLength = Math.max(...items.map((w) => w.length));
|
|
885
|
+
const columnWidth = maxItemLength + 3;
|
|
886
|
+
const columns = Math.max(1, Math.floor(termWidth / columnWidth));
|
|
887
|
+
const itemsPerColumn = Math.ceil(items.length / columns);
|
|
888
|
+
(0, import_react2.useEffect)(() => {
|
|
889
|
+
ctx.setAction("moveUp", () => {
|
|
890
|
+
selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
|
|
891
|
+
forceUpdate({});
|
|
892
|
+
});
|
|
893
|
+
ctx.setAction("moveDown", () => {
|
|
894
|
+
selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
|
|
895
|
+
forceUpdate({});
|
|
896
|
+
});
|
|
897
|
+
ctx.setAction("moveLeft", () => {
|
|
898
|
+
if (selectedIndexRef.current === 0) {
|
|
899
|
+
ctx.goBack();
|
|
900
|
+
} else {
|
|
901
|
+
selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
|
|
902
|
+
forceUpdate({});
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
ctx.setAction("moveRight", () => {
|
|
906
|
+
selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
|
|
907
|
+
forceUpdate({});
|
|
908
|
+
});
|
|
909
|
+
ctx.setKeyBinding([
|
|
910
|
+
{ key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
|
|
911
|
+
{ key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
|
|
912
|
+
{ key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
|
|
913
|
+
{ key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
|
|
914
|
+
]);
|
|
915
|
+
ctx.addFooter(`Total: ${items.length} items`);
|
|
916
|
+
}, []);
|
|
917
|
+
const selectedIndex = selectedIndexRef.current;
|
|
918
|
+
const rows = [];
|
|
919
|
+
for (let row = 0; row < itemsPerColumn; row++) {
|
|
920
|
+
const cols = [];
|
|
921
|
+
for (let col = 0; col < columns; col++) {
|
|
922
|
+
const index = col * itemsPerColumn + row;
|
|
923
|
+
if (index < items.length) {
|
|
924
|
+
const isSelected = index === selectedIndex;
|
|
925
|
+
cols.push(
|
|
926
|
+
h2(
|
|
927
|
+
import_ink2.Box,
|
|
928
|
+
{ key: index, width: columnWidth },
|
|
929
|
+
h2(import_ink2.Text, {
|
|
930
|
+
color: isSelected ? "black" : "white",
|
|
931
|
+
backgroundColor: isSelected ? "cyan" : void 0,
|
|
932
|
+
bold: isSelected
|
|
933
|
+
}, items[index].padEnd(maxItemLength))
|
|
934
|
+
)
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
rows.push(
|
|
939
|
+
h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return h2(import_ink2.Box, { flexDirection: "column" }, ...rows);
|
|
943
|
+
}
|
|
944
|
+
function MultiColumnListWithPreviewComponent({
|
|
945
|
+
items,
|
|
946
|
+
getPreviewContent,
|
|
947
|
+
ctx,
|
|
948
|
+
selectedIndexRef
|
|
949
|
+
}) {
|
|
950
|
+
const [, forceUpdate] = (0, import_react2.useState)({});
|
|
951
|
+
const termWidth = (process.stdout.columns || 80) - 8;
|
|
952
|
+
const maxItemLength = Math.max(...items.map((w) => w.length));
|
|
953
|
+
const columnWidth = maxItemLength + 3;
|
|
954
|
+
const columns = Math.max(1, Math.floor(termWidth / columnWidth));
|
|
955
|
+
const itemsPerColumn = Math.ceil(items.length / columns);
|
|
956
|
+
(0, import_react2.useEffect)(() => {
|
|
957
|
+
ctx.setAction("moveUp", () => {
|
|
958
|
+
selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
|
|
959
|
+
forceUpdate({});
|
|
960
|
+
});
|
|
961
|
+
ctx.setAction("moveDown", () => {
|
|
962
|
+
selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
|
|
963
|
+
forceUpdate({});
|
|
964
|
+
});
|
|
965
|
+
ctx.setAction("moveLeft", () => {
|
|
966
|
+
if (selectedIndexRef.current === 0) {
|
|
967
|
+
ctx.goBack();
|
|
968
|
+
} else {
|
|
969
|
+
selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
|
|
970
|
+
forceUpdate({});
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
ctx.setAction("moveRight", () => {
|
|
974
|
+
selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
|
|
975
|
+
forceUpdate({});
|
|
976
|
+
});
|
|
977
|
+
ctx.setKeyBinding([
|
|
978
|
+
{ key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
|
|
979
|
+
{ key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
|
|
980
|
+
{ key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
|
|
981
|
+
{ key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
|
|
982
|
+
]);
|
|
983
|
+
ctx.addFooter(`Total: ${items.length} items`);
|
|
984
|
+
}, []);
|
|
985
|
+
const selectedIndex = selectedIndexRef.current;
|
|
986
|
+
const selectedItem = items[selectedIndex];
|
|
987
|
+
const rows = [];
|
|
988
|
+
for (let row = 0; row < itemsPerColumn; row++) {
|
|
989
|
+
const cols = [];
|
|
990
|
+
for (let col = 0; col < columns; col++) {
|
|
991
|
+
const index = col * itemsPerColumn + row;
|
|
992
|
+
if (index < items.length) {
|
|
993
|
+
const isSelected = index === selectedIndex;
|
|
994
|
+
cols.push(
|
|
995
|
+
h2(
|
|
996
|
+
import_ink2.Box,
|
|
997
|
+
{ key: index, width: columnWidth },
|
|
998
|
+
h2(import_ink2.Text, {
|
|
999
|
+
color: isSelected ? "black" : "white",
|
|
1000
|
+
backgroundColor: isSelected ? "cyan" : void 0,
|
|
1001
|
+
bold: isSelected
|
|
1002
|
+
}, items[index].padEnd(maxItemLength))
|
|
1003
|
+
)
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
rows.push(
|
|
1008
|
+
h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const previewContent = getPreviewContent ? getPreviewContent(selectedItem) : selectedItem;
|
|
1012
|
+
const previewRows = [];
|
|
1013
|
+
if (typeof previewContent === "string") {
|
|
1014
|
+
previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(import_ink2.Text, { bold: true }, previewContent) }));
|
|
1015
|
+
} else if (typeof previewContent === "object" && !import_react2.default.isValidElement(previewContent) && previewContent !== null) {
|
|
1016
|
+
Object.entries(previewContent).forEach(([key, value], idx) => {
|
|
1017
|
+
previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(import_ink2.Text, {}, `${key}: ${value}`) }));
|
|
1018
|
+
});
|
|
1019
|
+
} else if (import_react2.default.isValidElement(previewContent)) {
|
|
1020
|
+
previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
|
|
1021
|
+
}
|
|
1022
|
+
return h2(
|
|
1023
|
+
import_ink2.Box,
|
|
1024
|
+
{ flexDirection: "column" },
|
|
1025
|
+
...rows,
|
|
1026
|
+
h2(ScreenRow, { key: "spacer-1", children: h2(import_ink2.Text, {}, " ") }),
|
|
1027
|
+
h2(ScreenDivider, { key: "divider" }),
|
|
1028
|
+
h2(ScreenRow, { key: "spacer-2", children: h2(import_ink2.Text, {}, " ") }),
|
|
1029
|
+
...previewRows
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
|
|
1033
|
+
const [, forceUpdate] = (0, import_react2.useState)({});
|
|
1034
|
+
const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
|
|
1035
|
+
const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
|
|
1036
|
+
const scrollStateRef = (0, import_react2.useRef)({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
|
|
1037
|
+
const defaultGetTitle = (item) => {
|
|
1038
|
+
return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
|
|
1039
|
+
};
|
|
1040
|
+
const titleGetter = getTitle || defaultGetTitle;
|
|
1041
|
+
const displayItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
|
|
1042
|
+
const titleA = titleGetter(a).toLowerCase();
|
|
1043
|
+
const titleB = titleGetter(b).toLowerCase();
|
|
1044
|
+
if (sortOrder === "asc") {
|
|
1045
|
+
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
1046
|
+
} else {
|
|
1047
|
+
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
1048
|
+
}
|
|
1049
|
+
}) : items;
|
|
1050
|
+
const effectiveMaxHeight = maxHeight || displayItems.length;
|
|
1051
|
+
const canScroll = displayItems.length > effectiveMaxHeight;
|
|
1052
|
+
const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
|
|
1053
|
+
const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
|
|
1054
|
+
const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
|
|
1055
|
+
const canScrollUp = clampedScrollOffset > 0;
|
|
1056
|
+
const canScrollDown = clampedScrollOffset < maxScrollOffset;
|
|
1057
|
+
scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
|
|
1058
|
+
(0, import_react2.useEffect)(() => {
|
|
1059
|
+
ctx.setAction("moveUp", () => {
|
|
1060
|
+
const newIndex = Math.max(0, selectedIndexRef.current - 1);
|
|
1061
|
+
selectedIndexRef.current = newIndex;
|
|
1062
|
+
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
1063
|
+
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
1064
|
+
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
1065
|
+
if (newIndex < currentClampedScrollOffset) {
|
|
1066
|
+
setScrollOffset(newIndex);
|
|
1067
|
+
}
|
|
1068
|
+
forceUpdate({});
|
|
1069
|
+
});
|
|
1070
|
+
ctx.setAction("moveDown", () => {
|
|
1071
|
+
const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
|
|
1072
|
+
const titleA = titleGetter(a).toLowerCase();
|
|
1073
|
+
const titleB = titleGetter(b).toLowerCase();
|
|
1074
|
+
if (sortOrder === "asc") {
|
|
1075
|
+
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
1076
|
+
} else {
|
|
1077
|
+
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
1078
|
+
}
|
|
1079
|
+
}) : items;
|
|
1080
|
+
const maxIndex = currentItems.length - 1;
|
|
1081
|
+
const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
|
|
1082
|
+
selectedIndexRef.current = newIndex;
|
|
1083
|
+
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
1084
|
+
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
1085
|
+
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
1086
|
+
if (newIndex >= currentClampedScrollOffset + currentMaxHeight) {
|
|
1087
|
+
setScrollOffset(newIndex - currentMaxHeight + 1);
|
|
1088
|
+
}
|
|
1089
|
+
forceUpdate({});
|
|
1090
|
+
});
|
|
1091
|
+
ctx.setAction("scrollUp", () => {
|
|
1092
|
+
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
1093
|
+
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
1094
|
+
const newScrollOffset = Math.max(0, currentScrollOffset - 1);
|
|
1095
|
+
setScrollOffset(newScrollOffset);
|
|
1096
|
+
forceUpdate({});
|
|
1097
|
+
});
|
|
1098
|
+
ctx.setAction("scrollDown", () => {
|
|
1099
|
+
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
1100
|
+
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
1101
|
+
const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
|
|
1102
|
+
setScrollOffset(newScrollOffset);
|
|
1103
|
+
forceUpdate({});
|
|
1104
|
+
});
|
|
1105
|
+
if (sortable) {
|
|
1106
|
+
ctx.setAction("toggleSort", () => {
|
|
1107
|
+
const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
|
|
1108
|
+
const currentSelectedItem = displayItems[selectedIndexRef.current];
|
|
1109
|
+
setSortOrder(nextSort);
|
|
1110
|
+
const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
|
|
1111
|
+
const titleA = titleGetter(a).toLowerCase();
|
|
1112
|
+
const titleB = titleGetter(b).toLowerCase();
|
|
1113
|
+
if (nextSort === "asc") {
|
|
1114
|
+
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
1115
|
+
} else {
|
|
1116
|
+
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
1117
|
+
}
|
|
1118
|
+
}) : items;
|
|
1119
|
+
const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
|
|
1120
|
+
if (newIndex !== -1) {
|
|
1121
|
+
selectedIndexRef.current = newIndex;
|
|
1122
|
+
setScrollOffset(newIndex);
|
|
1123
|
+
} else {
|
|
1124
|
+
selectedIndexRef.current = 0;
|
|
1125
|
+
setScrollOffset(0);
|
|
1126
|
+
}
|
|
1127
|
+
forceUpdate({});
|
|
1128
|
+
});
|
|
1129
|
+
const defaultHighlightStyle = {
|
|
1130
|
+
color: "black",
|
|
1131
|
+
backgroundColor: "green",
|
|
1132
|
+
bold: true
|
|
1133
|
+
};
|
|
1134
|
+
const highlightStyle = { ...defaultHighlightStyle, ...sortHighlightStyle };
|
|
1135
|
+
const sortCaption = () => {
|
|
1136
|
+
if (sortOrder === "none") {
|
|
1137
|
+
return h2(import_ink2.Text, {}, "s to toggle sort");
|
|
1138
|
+
} else {
|
|
1139
|
+
const sortLabel = sortOrder === "asc" ? "ASC" : "DESC";
|
|
1140
|
+
return h2(
|
|
1141
|
+
import_ink2.Text,
|
|
1142
|
+
{},
|
|
1143
|
+
"s to toggle ",
|
|
1144
|
+
h2(import_ink2.Text, { color: "white", bold: true }, "sort"),
|
|
1145
|
+
" ",
|
|
1146
|
+
h2(import_ink2.Text, highlightStyle, ` ${sortLabel} `)
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
ctx.setKeyBinding([
|
|
1151
|
+
{ key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
|
|
1152
|
+
{ key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
|
|
1153
|
+
{
|
|
1154
|
+
key: "s",
|
|
1155
|
+
caption: sortCaption,
|
|
1156
|
+
action: "toggleSort",
|
|
1157
|
+
order: 5
|
|
1158
|
+
}
|
|
1159
|
+
]);
|
|
1160
|
+
ctx.update();
|
|
1161
|
+
} else {
|
|
1162
|
+
ctx.setKeyBinding([
|
|
1163
|
+
{ key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
|
|
1164
|
+
{ key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
|
|
1165
|
+
]);
|
|
1166
|
+
}
|
|
1167
|
+
}, [sortOrder, sortable]);
|
|
1168
|
+
const selectedIndex = selectedIndexRef.current;
|
|
1169
|
+
const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
|
|
1170
|
+
const isFirstVisible = displayIndex === 0;
|
|
1171
|
+
const isLastVisible = displayIndex === visibleItems.length - 1;
|
|
1172
|
+
let arrowPrefix = "";
|
|
1173
|
+
let selectionPrefix = "";
|
|
1174
|
+
if (isFirstVisible && canScrollUp) {
|
|
1175
|
+
arrowPrefix = "\u2191 ";
|
|
1176
|
+
} else if (isLastVisible && canScrollDown) {
|
|
1177
|
+
arrowPrefix = "\u2193 ";
|
|
1178
|
+
} else {
|
|
1179
|
+
arrowPrefix = " ";
|
|
1180
|
+
}
|
|
1181
|
+
if (isSelected) {
|
|
1182
|
+
selectionPrefix = selectionMarker;
|
|
1183
|
+
} else {
|
|
1184
|
+
selectionPrefix = " ".repeat(selectionMarker.length);
|
|
1185
|
+
}
|
|
1186
|
+
return h2(
|
|
1187
|
+
import_ink2.Box,
|
|
1188
|
+
{ flexDirection: "row" },
|
|
1189
|
+
// Arrow (clickable if functional, not highlighted)
|
|
1190
|
+
h2(import_ink2.Text, {
|
|
1191
|
+
key: `arrow-${actualIndex}`,
|
|
1192
|
+
color: "white"
|
|
1193
|
+
}, arrowPrefix),
|
|
1194
|
+
// Selection marker space (always same width, not highlighted)
|
|
1195
|
+
h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
|
|
1196
|
+
// Item name (highlighted if selected)
|
|
1197
|
+
h2(import_ink2.Text, {
|
|
1198
|
+
key: `name-${actualIndex}`,
|
|
1199
|
+
color: isSelected ? "black" : "white",
|
|
1200
|
+
backgroundColor: isSelected ? "cyan" : void 0,
|
|
1201
|
+
bold: isSelected
|
|
1202
|
+
}, item.name)
|
|
1203
|
+
);
|
|
1204
|
+
};
|
|
1205
|
+
const itemRenderer = renderItem || defaultRenderItem;
|
|
1206
|
+
return h2(
|
|
1207
|
+
import_ink2.Box,
|
|
1208
|
+
{ flexDirection: "column" },
|
|
1209
|
+
...visibleItems.map((item, displayIndex) => {
|
|
1210
|
+
const actualIndex = clampedScrollOffset + displayIndex;
|
|
1211
|
+
const isSelected = actualIndex === selectedIndex;
|
|
1212
|
+
if (renderItem) {
|
|
1213
|
+
const isFirstVisible = displayIndex === 0;
|
|
1214
|
+
const isLastVisible = displayIndex === visibleItems.length - 1;
|
|
1215
|
+
let arrowPrefix = "";
|
|
1216
|
+
let selectionPrefix = "";
|
|
1217
|
+
if (isFirstVisible && canScrollUp) {
|
|
1218
|
+
arrowPrefix = "\u2191 ";
|
|
1219
|
+
} else if (isLastVisible && canScrollDown) {
|
|
1220
|
+
arrowPrefix = "\u2193 ";
|
|
1221
|
+
} else {
|
|
1222
|
+
arrowPrefix = " ";
|
|
1223
|
+
}
|
|
1224
|
+
if (isSelected) {
|
|
1225
|
+
selectionPrefix = selectionMarker;
|
|
1226
|
+
} else {
|
|
1227
|
+
selectionPrefix = " ".repeat(selectionMarker.length);
|
|
1228
|
+
}
|
|
1229
|
+
return h2(ScreenRow, {
|
|
1230
|
+
key: `item-${actualIndex}`,
|
|
1231
|
+
children: h2(
|
|
1232
|
+
import_ink2.Box,
|
|
1233
|
+
{ flexDirection: "row" },
|
|
1234
|
+
// Arrow (clickable if functional, not highlighted)
|
|
1235
|
+
h2(import_ink2.Text, {
|
|
1236
|
+
key: `arrow-${actualIndex}`,
|
|
1237
|
+
color: "white"
|
|
1238
|
+
}, arrowPrefix),
|
|
1239
|
+
// Selection marker space (always same width, not highlighted)
|
|
1240
|
+
h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
|
|
1241
|
+
// Custom rendered content
|
|
1242
|
+
renderItem(item, isSelected, displayIndex)
|
|
1243
|
+
)
|
|
1244
|
+
});
|
|
1245
|
+
} else {
|
|
1246
|
+
return h2(ScreenRow, {
|
|
1247
|
+
key: `item-${actualIndex}`,
|
|
1248
|
+
children: itemRenderer(item, isSelected, displayIndex, actualIndex)
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
})
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/screen/screens.ts
|
|
1256
|
+
function groupKeyBindings(bindings) {
|
|
1257
|
+
const groups = {};
|
|
1258
|
+
const enabledBindings = bindings.filter((b) => b.enabled !== false);
|
|
1259
|
+
enabledBindings.forEach((binding) => {
|
|
1260
|
+
const caption = typeof binding.caption === "string" ? binding.caption : "";
|
|
1261
|
+
if (!groups[caption]) {
|
|
1262
|
+
groups[caption] = {
|
|
1263
|
+
keys: [],
|
|
1264
|
+
caption,
|
|
1265
|
+
order: binding.order || 999
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
groups[caption].keys.push(binding.key);
|
|
1269
|
+
});
|
|
1270
|
+
return Object.values(groups);
|
|
1271
|
+
}
|
|
1272
|
+
function formatKeyBindings(bindings, mode = "long") {
|
|
1273
|
+
const resolvedBindings = bindings.map((binding) => {
|
|
1274
|
+
let resolvedCaption = binding.caption;
|
|
1275
|
+
if (typeof binding.caption === "function") {
|
|
1276
|
+
resolvedCaption = binding.caption();
|
|
1277
|
+
}
|
|
1278
|
+
return {
|
|
1279
|
+
...binding,
|
|
1280
|
+
resolvedCaption
|
|
1281
|
+
};
|
|
1282
|
+
});
|
|
1283
|
+
const groups = groupKeyBindings(resolvedBindings.map((b) => ({
|
|
1284
|
+
...b,
|
|
1285
|
+
caption: typeof b.resolvedCaption === "string" ? b.resolvedCaption : ""
|
|
1286
|
+
})));
|
|
1287
|
+
groups.sort((a, b) => a.order - b.order);
|
|
1288
|
+
const items = [];
|
|
1289
|
+
groups.forEach((group) => {
|
|
1290
|
+
const bindingWithCustom = resolvedBindings.find(
|
|
1291
|
+
(b) => group.keys.includes(b.key) && typeof b.resolvedCaption !== "string"
|
|
1292
|
+
);
|
|
1293
|
+
if (bindingWithCustom && bindingWithCustom.resolvedCaption) {
|
|
1294
|
+
items.push(bindingWithCustom.resolvedCaption);
|
|
1295
|
+
} else {
|
|
1296
|
+
const keyStr = formatKeys(group.keys);
|
|
1297
|
+
if (mode === "long") {
|
|
1298
|
+
items.push(`${keyStr} to ${group.caption}`);
|
|
1299
|
+
} else {
|
|
1300
|
+
items.push(keyStr);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
return items;
|
|
1305
|
+
}
|
|
1306
|
+
function formatKeys(keys) {
|
|
1307
|
+
const keyMap = {
|
|
1308
|
+
"escape": "esc",
|
|
1309
|
+
"leftArrow": "\u2190",
|
|
1310
|
+
"rightArrow": "\u2192",
|
|
1311
|
+
"upArrow": "\u2191",
|
|
1312
|
+
"downArrow": "\u2193",
|
|
1313
|
+
"return": "enter"
|
|
1314
|
+
};
|
|
1315
|
+
return keys.map((k) => keyMap[k] || k).join("/");
|
|
1316
|
+
}
|
|
1317
|
+
async function showScreen(config2) {
|
|
1318
|
+
const {
|
|
1319
|
+
title,
|
|
1320
|
+
onRender,
|
|
1321
|
+
parentData = {}
|
|
1322
|
+
} = config2;
|
|
1323
|
+
return new Promise((resolve2) => {
|
|
1324
|
+
let instance2;
|
|
1325
|
+
const keyBindings = [];
|
|
1326
|
+
const actions = {};
|
|
1327
|
+
const customFooterItems = [];
|
|
1328
|
+
let renderResult = null;
|
|
1329
|
+
let initialized = false;
|
|
1330
|
+
const Screen = () => {
|
|
1331
|
+
const [updateCounter, setUpdateCounter] = (0, import_react3.useState)(0);
|
|
1332
|
+
if (!initialized) {
|
|
1333
|
+
const defaultBindings = [
|
|
1334
|
+
{ key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
|
|
1335
|
+
{ key: "leftArrow", caption: "go back", action: "back", protected: false, order: 1 }
|
|
1336
|
+
// Note: 'select' is not a default - components add it if needed
|
|
1337
|
+
];
|
|
1338
|
+
defaultBindings.forEach((binding) => {
|
|
1339
|
+
keyBindings.push(binding);
|
|
1340
|
+
});
|
|
1341
|
+
actions.back = () => {
|
|
1342
|
+
cleanup(null);
|
|
1343
|
+
};
|
|
1344
|
+
initialized = true;
|
|
1345
|
+
}
|
|
1346
|
+
const context = {
|
|
1347
|
+
setAction: (actionName, handlerFn) => {
|
|
1348
|
+
actions[actionName] = handlerFn;
|
|
1349
|
+
},
|
|
1350
|
+
setKeyBinding: (bindingOrBindings) => {
|
|
1351
|
+
const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
|
|
1352
|
+
bindingsToSet.forEach((binding) => {
|
|
1353
|
+
const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
|
|
1354
|
+
if (existingIndex >= 0) {
|
|
1355
|
+
const existing = keyBindings[existingIndex];
|
|
1356
|
+
if (existing.protected) {
|
|
1357
|
+
console.warn(`Cannot override protected key: ${binding.key}`);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
keyBindings[existingIndex] = {
|
|
1361
|
+
...existing,
|
|
1362
|
+
...binding,
|
|
1363
|
+
order: binding.order !== void 0 ? binding.order : existing.order,
|
|
1364
|
+
enabled: binding.enabled !== void 0 ? binding.enabled : existing.enabled !== void 0 ? existing.enabled : true
|
|
1365
|
+
};
|
|
1366
|
+
} else {
|
|
1367
|
+
keyBindings.push({
|
|
1368
|
+
protected: false,
|
|
1369
|
+
order: 999,
|
|
1370
|
+
enabled: true,
|
|
1371
|
+
...binding
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
},
|
|
1376
|
+
updateKeyBinding: (keyName, updates) => {
|
|
1377
|
+
const index = keyBindings.findIndex((b) => b.key === keyName);
|
|
1378
|
+
if (index >= 0) {
|
|
1379
|
+
keyBindings[index] = {
|
|
1380
|
+
...keyBindings[index],
|
|
1381
|
+
...updates
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
removeKeyBinding: (keyName) => {
|
|
1386
|
+
const index = keyBindings.findIndex((b) => b.key === keyName);
|
|
1387
|
+
if (index >= 0) {
|
|
1388
|
+
if (keyBindings[index].protected) {
|
|
1389
|
+
console.warn(`Cannot remove protected key: ${keyName}`);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
keyBindings.splice(index, 1);
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
addFooter: (item) => {
|
|
1396
|
+
customFooterItems.push(item);
|
|
1397
|
+
},
|
|
1398
|
+
clearFooter: () => {
|
|
1399
|
+
customFooterItems.length = 0;
|
|
1400
|
+
},
|
|
1401
|
+
setFooter: (items) => {
|
|
1402
|
+
customFooterItems.length = 0;
|
|
1403
|
+
const itemsArray = Array.isArray(items) ? items : [items];
|
|
1404
|
+
customFooterItems.push(...itemsArray);
|
|
1405
|
+
},
|
|
1406
|
+
update: () => {
|
|
1407
|
+
setUpdateCounter((c) => c + 1);
|
|
1408
|
+
},
|
|
1409
|
+
goBack: () => {
|
|
1410
|
+
if (actions.back) {
|
|
1411
|
+
actions.back();
|
|
1412
|
+
}
|
|
1413
|
+
},
|
|
1414
|
+
close: (result) => {
|
|
1415
|
+
cleanup(result);
|
|
1416
|
+
},
|
|
1417
|
+
parentData
|
|
1418
|
+
};
|
|
1419
|
+
if (!renderResult) {
|
|
1420
|
+
renderResult = onRender(context);
|
|
1421
|
+
}
|
|
1422
|
+
(0, import_ink3.useInput)((input, key) => {
|
|
1423
|
+
if (key.ctrl && input === "c") {
|
|
1424
|
+
cleanup(null);
|
|
1425
|
+
process.exit(0);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
let matchedBinding = null;
|
|
1429
|
+
for (const binding of keyBindings) {
|
|
1430
|
+
let keyMatches = false;
|
|
1431
|
+
if (key[binding.key]) {
|
|
1432
|
+
keyMatches = true;
|
|
1433
|
+
} else if (input === binding.key) {
|
|
1434
|
+
keyMatches = true;
|
|
1435
|
+
}
|
|
1436
|
+
if (keyMatches) {
|
|
1437
|
+
if (binding.enabled === false) {
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (binding.condition && !binding.condition(context)) {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
matchedBinding = binding;
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (matchedBinding && actions[matchedBinding.action]) {
|
|
1448
|
+
const actionResult = actions[matchedBinding.action]({
|
|
1449
|
+
input,
|
|
1450
|
+
key,
|
|
1451
|
+
binding: matchedBinding
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
const footerLines = [];
|
|
1456
|
+
const bindingItems = formatKeyBindings(keyBindings, "long");
|
|
1457
|
+
if (bindingItems.length > 0) {
|
|
1458
|
+
const bindingsLine = [];
|
|
1459
|
+
bindingItems.forEach((item, idx) => {
|
|
1460
|
+
if (idx > 0) {
|
|
1461
|
+
bindingsLine.push(", ");
|
|
1462
|
+
}
|
|
1463
|
+
bindingsLine.push(item);
|
|
1464
|
+
});
|
|
1465
|
+
const allStrings = bindingItems.every((item) => typeof item === "string");
|
|
1466
|
+
if (allStrings) {
|
|
1467
|
+
footerLines.push(bindingsLine.join(""));
|
|
1468
|
+
} else {
|
|
1469
|
+
const wrappedBindingsLine = bindingsLine.map(
|
|
1470
|
+
(item) => typeof item === "string" ? (0, import_react3.createElement)(import_ink3.Text, {}, item) : item
|
|
1471
|
+
);
|
|
1472
|
+
footerLines.push(wrappedBindingsLine);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
customFooterItems.forEach((item) => {
|
|
1476
|
+
if (typeof item === "string") {
|
|
1477
|
+
footerLines.push(item);
|
|
1478
|
+
} else {
|
|
1479
|
+
footerLines.push(item);
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
return (0, import_react3.createElement)(
|
|
1483
|
+
ScreenContainer,
|
|
1484
|
+
{},
|
|
1485
|
+
(0, import_react3.createElement)(ScreenTitle, { text: title }),
|
|
1486
|
+
(0, import_react3.createElement)(ScreenDivider),
|
|
1487
|
+
(0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
|
|
1488
|
+
renderResult,
|
|
1489
|
+
(0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
|
|
1490
|
+
(0, import_react3.createElement)(ScreenDivider),
|
|
1491
|
+
(0, import_react3.createElement)(ScreenFooter, { lines: footerLines })
|
|
1492
|
+
);
|
|
1493
|
+
};
|
|
1494
|
+
const cleanup = (result) => {
|
|
1495
|
+
if (instance2) instance2.unmount();
|
|
1496
|
+
setTimeout(() => resolve2(result), 50);
|
|
1497
|
+
};
|
|
1498
|
+
instance2 = (0, import_ink3.render)((0, import_react3.createElement)(Screen));
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
async function showListScreen(config2) {
|
|
1502
|
+
const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker } = config2;
|
|
1503
|
+
return showScreen({
|
|
1504
|
+
title,
|
|
1505
|
+
parentData,
|
|
1506
|
+
onRender: (ctx) => {
|
|
1507
|
+
const selectedIndexRef = { current: initialSelectedIndex };
|
|
1508
|
+
ctx.setAction("select", () => {
|
|
1509
|
+
const selected = items[selectedIndexRef.current];
|
|
1510
|
+
if (onSelect) {
|
|
1511
|
+
const result = onSelect(selected.value, selectedIndexRef.current);
|
|
1512
|
+
ctx.close(result);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
if (onEscape) {
|
|
1516
|
+
ctx.setAction("back", () => {
|
|
1517
|
+
const result = onEscape(selectedIndexRef.current);
|
|
1518
|
+
ctx.close(result);
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
|
|
1522
|
+
return (0, import_react3.createElement)(ListComponent, { items, ctx, selectedIndexRef, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker });
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
async function showMultiColumnListScreen(config2) {
|
|
1527
|
+
const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
|
|
1528
|
+
return showScreen({
|
|
1529
|
+
title,
|
|
1530
|
+
parentData,
|
|
1531
|
+
onRender: (ctx) => {
|
|
1532
|
+
const selectedIndexRef = { current: initialSelectedIndex };
|
|
1533
|
+
ctx.setAction("select", () => {
|
|
1534
|
+
const selected = items[selectedIndexRef.current];
|
|
1535
|
+
if (onSelect) {
|
|
1536
|
+
const result = onSelect(selected, selectedIndexRef.current);
|
|
1537
|
+
ctx.close(result);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
if (onEscape) {
|
|
1541
|
+
ctx.setAction("back", () => {
|
|
1542
|
+
const result = onEscape(selectedIndexRef.current);
|
|
1543
|
+
ctx.close(result);
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
|
|
1547
|
+
return (0, import_react3.createElement)(MultiColumnListComponent, { items, ctx, selectedIndexRef });
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
async function showMultiColumnListWithPreviewScreen(config2) {
|
|
1552
|
+
const { title, items, getPreviewContent, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
|
|
1553
|
+
return showScreen({
|
|
1554
|
+
title,
|
|
1555
|
+
parentData,
|
|
1556
|
+
onRender: (ctx) => {
|
|
1557
|
+
const selectedIndexRef = { current: initialSelectedIndex };
|
|
1558
|
+
ctx.setAction("select", () => {
|
|
1559
|
+
const selected = items[selectedIndexRef.current];
|
|
1560
|
+
if (onSelect) {
|
|
1561
|
+
const result = onSelect(selected, selectedIndexRef.current);
|
|
1562
|
+
ctx.close(result);
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
if (onEscape) {
|
|
1566
|
+
ctx.setAction("back", () => {
|
|
1567
|
+
const result = onEscape(selectedIndexRef.current);
|
|
1568
|
+
ctx.close(result);
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
|
|
1572
|
+
return (0, import_react3.createElement)(MultiColumnListWithPreviewComponent, { items, getPreviewContent, ctx, selectedIndexRef });
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
var showMenuScreen = showListScreen;
|
|
1577
|
+
var showWordGridScreen = showMultiColumnListScreen;
|
|
1578
|
+
|
|
1579
|
+
// src/screen/ui-elements.ts
|
|
1580
|
+
var import_react4 = require("react");
|
|
1581
|
+
var import_ink4 = require("ink");
|
|
1582
|
+
function ListItem({
|
|
1583
|
+
children,
|
|
1584
|
+
isSelected = false,
|
|
1585
|
+
color = "white",
|
|
1586
|
+
backgroundColor,
|
|
1587
|
+
bold = false,
|
|
1588
|
+
dimColor = false
|
|
1589
|
+
}) {
|
|
1590
|
+
return (0, import_react4.createElement)(
|
|
1591
|
+
import_ink4.Box,
|
|
1592
|
+
{},
|
|
1593
|
+
(0, import_react4.createElement)(import_ink4.Text, {
|
|
1594
|
+
color: isSelected ? backgroundColor || "green" : color,
|
|
1595
|
+
backgroundColor: isSelected ? color : backgroundColor,
|
|
1596
|
+
bold: isSelected || bold,
|
|
1597
|
+
dimColor: !isSelected && dimColor
|
|
1598
|
+
}, children)
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
function TextBlock({
|
|
1602
|
+
text,
|
|
1603
|
+
color = "white",
|
|
1604
|
+
dimmed = false,
|
|
1605
|
+
bold = false,
|
|
1606
|
+
maxWidth
|
|
1607
|
+
}) {
|
|
1608
|
+
return (0, import_react4.createElement)(
|
|
1609
|
+
import_ink4.Box,
|
|
1610
|
+
{},
|
|
1611
|
+
(0, import_react4.createElement)(import_ink4.Text, {
|
|
1612
|
+
color,
|
|
1613
|
+
dimColor: dimmed,
|
|
1614
|
+
bold
|
|
1615
|
+
}, text)
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
function Divider({ character = "\u2500", width = 80 }) {
|
|
1619
|
+
return (0, import_react4.createElement)(
|
|
1620
|
+
import_ink4.Box,
|
|
1621
|
+
{ marginY: 1 },
|
|
1622
|
+
(0, import_react4.createElement)(import_ink4.Text, { dimColor: true }, character.repeat(width))
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
function GridCell({
|
|
1626
|
+
children,
|
|
1627
|
+
width,
|
|
1628
|
+
color = "white",
|
|
1629
|
+
backgroundColor,
|
|
1630
|
+
bold = false,
|
|
1631
|
+
dimColor = false,
|
|
1632
|
+
align = "left"
|
|
1633
|
+
}) {
|
|
1634
|
+
return (0, import_react4.createElement)(
|
|
1635
|
+
import_ink4.Box,
|
|
1636
|
+
{ width },
|
|
1637
|
+
(0, import_react4.createElement)(import_ink4.Text, {
|
|
1638
|
+
color,
|
|
1639
|
+
backgroundColor,
|
|
1640
|
+
bold,
|
|
1641
|
+
dimColor,
|
|
1642
|
+
textAlign: align
|
|
1643
|
+
}, children)
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
function InputField({ prompt, value, onChange, onSubmit }) {
|
|
1647
|
+
return (0, import_react4.createElement)(
|
|
1648
|
+
import_ink4.Box,
|
|
1649
|
+
{ flexDirection: "column" },
|
|
1650
|
+
(0, import_react4.createElement)(import_ink4.Text, {}, prompt),
|
|
1651
|
+
(0, import_react4.createElement)(
|
|
1652
|
+
import_ink4.Box,
|
|
1653
|
+
{ marginTop: 1 },
|
|
1654
|
+
(0, import_react4.createElement)(import_ink4.Text, { color: "cyan" }, " > ", value, "_")
|
|
1655
|
+
)
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/screen/utils.ts
|
|
1660
|
+
function buildBreadcrumb(parts) {
|
|
1661
|
+
if (parts.length === 0) return "";
|
|
1662
|
+
if (parts.length === 1) return parts[0];
|
|
1663
|
+
return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
|
|
1664
|
+
}
|
|
1665
|
+
function buildDetailBreadcrumb(path, suffix = "") {
|
|
1666
|
+
if (path.length <= 1) {
|
|
1667
|
+
return suffix ? `\u2190 ${suffix}` : path[0] || "";
|
|
1668
|
+
}
|
|
1669
|
+
const breadcrumb = buildBreadcrumb(path);
|
|
1670
|
+
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/screen/footer-builder.ts
|
|
1674
|
+
function buildFooter(config2 = {}) {
|
|
1675
|
+
const {
|
|
1676
|
+
navigation = null,
|
|
1677
|
+
actions = null,
|
|
1678
|
+
info = null,
|
|
1679
|
+
escape = "Esc to go back",
|
|
1680
|
+
custom = null
|
|
1681
|
+
} = config2;
|
|
1682
|
+
const lines = [];
|
|
1683
|
+
const mainParts = [];
|
|
1684
|
+
if (navigation) {
|
|
1685
|
+
mainParts.push(navigation);
|
|
1686
|
+
}
|
|
1687
|
+
if (actions) {
|
|
1688
|
+
mainParts.push(actions);
|
|
1689
|
+
}
|
|
1690
|
+
if (escape) {
|
|
1691
|
+
mainParts.push(escape);
|
|
1692
|
+
}
|
|
1693
|
+
if (mainParts.length > 0) {
|
|
1694
|
+
lines.push(mainParts.join(", "));
|
|
1695
|
+
}
|
|
1696
|
+
if (info) {
|
|
1697
|
+
const infoLines = Array.isArray(info) ? info : [info];
|
|
1698
|
+
lines.push(...infoLines);
|
|
1699
|
+
}
|
|
1700
|
+
if (custom) {
|
|
1701
|
+
const customLines = Array.isArray(custom) ? custom : [custom];
|
|
1702
|
+
lines.push(...customLines);
|
|
1703
|
+
}
|
|
1704
|
+
return lines;
|
|
1705
|
+
}
|
|
1706
|
+
var FooterPresets = {
|
|
1707
|
+
/**
|
|
1708
|
+
* Menu screen footer
|
|
1709
|
+
*/
|
|
1710
|
+
menu: (customInfo = null) => buildFooter({
|
|
1711
|
+
navigation: "\u2191/\u2193 to navigate",
|
|
1712
|
+
actions: "Enter to select",
|
|
1713
|
+
escape: "Esc to go back",
|
|
1714
|
+
info: customInfo
|
|
1715
|
+
}),
|
|
1716
|
+
/**
|
|
1717
|
+
* Word grid footer
|
|
1718
|
+
*/
|
|
1719
|
+
wordGrid: (totalWords) => buildFooter({
|
|
1720
|
+
navigation: "\u2191\u2193\u2190\u2192 to navigate",
|
|
1721
|
+
actions: "Enter to select",
|
|
1722
|
+
escape: "Esc to go back",
|
|
1723
|
+
info: `Total: ${totalWords} words`
|
|
1724
|
+
}),
|
|
1725
|
+
/**
|
|
1726
|
+
* Text input footer
|
|
1727
|
+
*/
|
|
1728
|
+
textInput: () => buildFooter({
|
|
1729
|
+
actions: "Type and press Enter to submit",
|
|
1730
|
+
escape: "Esc to cancel"
|
|
1731
|
+
}),
|
|
1732
|
+
/**
|
|
1733
|
+
* Info/static screen footer
|
|
1734
|
+
*/
|
|
1735
|
+
info: () => buildFooter({
|
|
1736
|
+
escape: "Esc to continue"
|
|
1737
|
+
}),
|
|
1738
|
+
/**
|
|
1739
|
+
* Main menu footer (escape exits)
|
|
1740
|
+
*/
|
|
1741
|
+
mainMenu: () => buildFooter({
|
|
1742
|
+
navigation: "\u2191/\u2193 to navigate",
|
|
1743
|
+
actions: "Enter to select",
|
|
1744
|
+
escape: "Esc to exit"
|
|
1745
|
+
}),
|
|
1746
|
+
/**
|
|
1747
|
+
* Action menu footer (for word cards, etc.)
|
|
1748
|
+
*/
|
|
1749
|
+
actionMenu: (hasAudio = false) => {
|
|
1750
|
+
const parts = buildFooter({
|
|
1751
|
+
navigation: "\u2191/\u2193 to navigate",
|
|
1752
|
+
actions: "Enter to select",
|
|
1753
|
+
escape: "Esc to go back"
|
|
1754
|
+
});
|
|
1755
|
+
if (hasAudio) {
|
|
1756
|
+
parts.push("Audio available");
|
|
1757
|
+
}
|
|
1758
|
+
return parts;
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
function organizeFooterMessages(messages) {
|
|
1762
|
+
if (!messages || messages.length === 0) {
|
|
1763
|
+
return ["Esc to go back"];
|
|
1764
|
+
}
|
|
1765
|
+
const navigation = messages.filter((m) => m.includes("\u2191") || m.includes("\u2193") || m.includes("\u2190") || m.includes("\u2192"));
|
|
1766
|
+
const actions = messages.filter((m) => m.includes("Enter") || m.includes("select") || m.includes("submit"));
|
|
1767
|
+
const escape = messages.filter((m) => m.includes("Esc"));
|
|
1768
|
+
const others = messages.filter(
|
|
1769
|
+
(m) => !navigation.includes(m) && !actions.includes(m) && !escape.includes(m)
|
|
1770
|
+
);
|
|
1771
|
+
const lines = [];
|
|
1772
|
+
const mainLine = [...navigation, ...actions, ...escape].join(", ");
|
|
1773
|
+
if (mainLine) lines.push(mainLine);
|
|
1774
|
+
lines.push(...others);
|
|
1775
|
+
return lines;
|
|
1776
|
+
}
|
|
1777
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1778
|
+
0 && (module.exports = {
|
|
1779
|
+
Args,
|
|
1780
|
+
Divider,
|
|
1781
|
+
FooterPresets,
|
|
1782
|
+
GridCell,
|
|
1783
|
+
InputField,
|
|
1784
|
+
ListComponent,
|
|
1785
|
+
ListItem,
|
|
1786
|
+
MultiColumnListComponent,
|
|
1787
|
+
MultiColumnListWithPreviewComponent,
|
|
1788
|
+
Params,
|
|
1789
|
+
ScreenBody,
|
|
1790
|
+
ScreenContainer,
|
|
1791
|
+
ScreenDivider,
|
|
1792
|
+
ScreenFooter,
|
|
1793
|
+
ScreenRow,
|
|
1794
|
+
ScreenTitle,
|
|
1795
|
+
TextBlock,
|
|
1796
|
+
buildBreadcrumb,
|
|
1797
|
+
buildDetailBreadcrumb,
|
|
1798
|
+
buildFooter,
|
|
1799
|
+
getArgsInstance,
|
|
1800
|
+
getParamsInstance,
|
|
1801
|
+
joiEdateType,
|
|
1802
|
+
joiStringArrayType,
|
|
1803
|
+
organizeFooterMessages,
|
|
1804
|
+
showListScreen,
|
|
1805
|
+
showMenuScreen,
|
|
1806
|
+
showMultiColumnListScreen,
|
|
1807
|
+
showMultiColumnListWithPreviewScreen,
|
|
1808
|
+
showScreen,
|
|
1809
|
+
showWordGridScreen
|
|
1810
|
+
});
|
|
1811
|
+
//# sourceMappingURL=index.cjs.map
|