@pablozaiden/terminatui 0.1.2 → 0.3.0-beta-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +43 -0
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +62 -0
- package/examples/tui-app/commands/config/app/index.ts +23 -0
- package/examples/tui-app/commands/config/app/set.ts +96 -0
- package/examples/tui-app/commands/config/index.ts +28 -0
- package/examples/tui-app/commands/config/user/get.ts +61 -0
- package/examples/tui-app/commands/config/user/index.ts +23 -0
- package/examples/tui-app/commands/config/user/set.ts +57 -0
- package/examples/tui-app/commands/greet.ts +14 -11
- package/examples/tui-app/commands/math.ts +6 -9
- package/examples/tui-app/commands/status.ts +24 -13
- package/examples/tui-app/index.ts +7 -3
- package/guides/01-hello-world.md +7 -2
- package/guides/02-adding-options.md +2 -2
- package/guides/03-multiple-commands.md +6 -8
- package/guides/04-subcommands.md +8 -8
- package/guides/05-interactive-tui.md +45 -30
- package/guides/06-config-validation.md +4 -12
- package/guides/07-async-cancellation.md +15 -69
- package/guides/08-complete-application.md +13 -179
- package/guides/README.md +7 -3
- package/package.json +4 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +19 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +4 -4
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +70 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +52 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +160 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +60 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/examples/tui-app/commands/index.ts +0 -3
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/index.ts +0 -137
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -582
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Command, type CommandExecutionContext, type CommandResult } from "../../../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../../../src/types/command";
|
|
4
|
+
|
|
5
|
+
const options = {
|
|
6
|
+
key: {
|
|
7
|
+
type: "string",
|
|
8
|
+
description: "Application configuration key to set",
|
|
9
|
+
required: true,
|
|
10
|
+
label: "Key",
|
|
11
|
+
order: 1,
|
|
12
|
+
group: "Required",
|
|
13
|
+
placeholder: "e.g., port, debug, logLevel",
|
|
14
|
+
},
|
|
15
|
+
value: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Value to set",
|
|
18
|
+
required: true,
|
|
19
|
+
label: "Value",
|
|
20
|
+
order: 2,
|
|
21
|
+
group: "Required",
|
|
22
|
+
placeholder: "Enter value...",
|
|
23
|
+
},
|
|
24
|
+
type: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Value type for parsing",
|
|
27
|
+
enum: ["string", "number", "boolean"] as const,
|
|
28
|
+
default: "string",
|
|
29
|
+
label: "Value Type",
|
|
30
|
+
order: 3,
|
|
31
|
+
group: "Options",
|
|
32
|
+
},
|
|
33
|
+
} as const satisfies OptionSchema;
|
|
34
|
+
|
|
35
|
+
export class AppSetCommand extends Command<typeof options> {
|
|
36
|
+
readonly name = "set";
|
|
37
|
+
override displayName = "Set App Config";
|
|
38
|
+
readonly description = "Set an application configuration value";
|
|
39
|
+
readonly options = options;
|
|
40
|
+
|
|
41
|
+
override readonly actionLabel = "Set Value";
|
|
42
|
+
|
|
43
|
+
override readonly examples = [
|
|
44
|
+
{ command: "config app set --key port --value 8080 --type number", description: "Set app port" },
|
|
45
|
+
{ command: "config app set --key debug --value false --type boolean", description: "Disable debug" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
override async execute(opts: OptionValues<typeof options>, execCtx: CommandExecutionContext): Promise<CommandResult> {
|
|
49
|
+
let parsedValue: string | number | boolean = opts.value;
|
|
50
|
+
|
|
51
|
+
// Parse value based on type
|
|
52
|
+
if (opts.type === "number") {
|
|
53
|
+
parsedValue = Number(opts.value);
|
|
54
|
+
if (isNaN(parsedValue)) {
|
|
55
|
+
AppContext.current.logger.error(`Invalid number value: "${opts.value}"`);
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
message: `Invalid number: "${opts.value}"`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
} else if (opts.type === "boolean") {
|
|
62
|
+
parsedValue = opts.value.toLowerCase() === "true";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
AppContext.current.logger.info(`Setting app.${opts.key} = ${JSON.stringify(parsedValue)}`);
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Simulate setting the value on the 5th iteration
|
|
69
|
+
await new Promise(resolve => {
|
|
70
|
+
let count = 0;
|
|
71
|
+
let interval = setInterval(() => {
|
|
72
|
+
count++;
|
|
73
|
+
AppContext.current.logger.info(`Applying configuration... (${count}/5)`);
|
|
74
|
+
if (count >= 5 || execCtx.signal.aborted) {
|
|
75
|
+
if (count < 5) {
|
|
76
|
+
AppContext.current.logger.warn("Configuration update aborted");
|
|
77
|
+
}
|
|
78
|
+
clearInterval(interval);
|
|
79
|
+
resolve(undefined);
|
|
80
|
+
}
|
|
81
|
+
}, 1000);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
AppContext.current.logger.info(`Successfully updated application configuration`);
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
data: { key: opts.key, value: parsedValue, type: opts.type },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override renderResult(result: CommandResult): string {
|
|
92
|
+
if (!result.success) return result.message || "Error";
|
|
93
|
+
const data = result.data as { key: string; value: string | number | boolean; type: string };
|
|
94
|
+
return `✓ Set app.${data.key} = ${JSON.stringify(data.value)} (${data.type})`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { UserConfigCommand } from "./user/index.ts";
|
|
2
|
+
import { AppConfigCommand } from "./app/index.ts";
|
|
3
|
+
import { Command, type CommandResult } from "../../../../src/core/command.ts";
|
|
4
|
+
|
|
5
|
+
export class ConfigCommand extends Command {
|
|
6
|
+
readonly name = "config";
|
|
7
|
+
override displayName = "Config";
|
|
8
|
+
readonly description = "Manage configuration (user and app settings)";
|
|
9
|
+
readonly options = {};
|
|
10
|
+
|
|
11
|
+
override readonly subCommands = [
|
|
12
|
+
new UserConfigCommand(),
|
|
13
|
+
new AppConfigCommand(),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
override readonly examples = [
|
|
17
|
+
{ command: "config user get --key name", description: "Get user name" },
|
|
18
|
+
{ command: "config user set --key theme --value dark", description: "Set user theme" },
|
|
19
|
+
{ command: "config app get --key port", description: "Get app port" },
|
|
20
|
+
{ command: "config app set --key debug --value true --type boolean", description: "Enable debug mode" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
override execute(): CommandResult {
|
|
24
|
+
console.log("Use 'config <category> <command>' to manage settings.");
|
|
25
|
+
console.log("Categories: user, app");
|
|
26
|
+
return { success: true };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Command, type CommandResult } from "../../../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../../../src/types/command";
|
|
4
|
+
|
|
5
|
+
const options = {
|
|
6
|
+
key: {
|
|
7
|
+
type: "string",
|
|
8
|
+
description: "Configuration key to get",
|
|
9
|
+
required: true,
|
|
10
|
+
label: "Key",
|
|
11
|
+
order: 1,
|
|
12
|
+
group: "Required",
|
|
13
|
+
placeholder: "e.g., name, email, theme",
|
|
14
|
+
},
|
|
15
|
+
} as const satisfies OptionSchema;
|
|
16
|
+
|
|
17
|
+
export class UserGetCommand extends Command<typeof options> {
|
|
18
|
+
readonly name = "get";
|
|
19
|
+
override displayName = "Get User Config";
|
|
20
|
+
readonly description = "Get a user configuration value";
|
|
21
|
+
readonly options = options;
|
|
22
|
+
|
|
23
|
+
override readonly actionLabel = "Get Value";
|
|
24
|
+
|
|
25
|
+
override readonly examples = [
|
|
26
|
+
{ command: "config user get --key name", description: "Get user name" },
|
|
27
|
+
{ command: "config user get --key email", description: "Get user email" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
override async execute(opts: OptionValues<typeof options>): Promise<CommandResult> {
|
|
31
|
+
// Simulated user config store
|
|
32
|
+
const userConfig: Record<string, string> = {
|
|
33
|
+
name: "John Doe",
|
|
34
|
+
email: "john@example.com",
|
|
35
|
+
theme: "dark",
|
|
36
|
+
language: "en",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const value = userConfig[opts.key];
|
|
40
|
+
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
AppContext.current.logger.warn(`Key "${opts.key}" not found in user configuration`);
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
message: `Key "${opts.key}" not found`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
AppContext.current.logger.info(`Retrieved user.${opts.key} = ${value}`);
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
data: { key: opts.key, value },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override renderResult(result: CommandResult): string {
|
|
57
|
+
if (!result.success) return result.message || "Error";
|
|
58
|
+
const data = result.data as { key: string; value: string };
|
|
59
|
+
return `user.${data.key} = "${data.value}"`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command, type CommandResult } from "../../../../../src/core/command.ts";
|
|
2
|
+
import { UserGetCommand } from "./get.ts";
|
|
3
|
+
import { UserSetCommand } from "./set.ts";
|
|
4
|
+
|
|
5
|
+
export class UserConfigCommand extends Command {
|
|
6
|
+
readonly name = "user";
|
|
7
|
+
override displayName = "User Settings";
|
|
8
|
+
readonly description = "Manage user configuration";
|
|
9
|
+
readonly options = {};
|
|
10
|
+
|
|
11
|
+
override readonly subCommands = [
|
|
12
|
+
new UserGetCommand(),
|
|
13
|
+
new UserSetCommand(),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
override execute(): CommandResult {
|
|
17
|
+
console.log("Use 'config user <command>' for user configuration.");
|
|
18
|
+
console.log("Available: get, set");
|
|
19
|
+
return { success: true };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { UserGetCommand, UserSetCommand };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Command, type CommandResult } from "../../../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../../../src/types/command";
|
|
4
|
+
|
|
5
|
+
const options = {
|
|
6
|
+
key: {
|
|
7
|
+
type: "string",
|
|
8
|
+
description: "Configuration key to set",
|
|
9
|
+
required: true,
|
|
10
|
+
label: "Key",
|
|
11
|
+
order: 1,
|
|
12
|
+
group: "Required",
|
|
13
|
+
placeholder: "e.g., name, email, theme",
|
|
14
|
+
},
|
|
15
|
+
value: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Value to set",
|
|
18
|
+
required: true,
|
|
19
|
+
label: "Value",
|
|
20
|
+
order: 2,
|
|
21
|
+
group: "Required",
|
|
22
|
+
placeholder: "Enter value...",
|
|
23
|
+
},
|
|
24
|
+
} as const satisfies OptionSchema;
|
|
25
|
+
|
|
26
|
+
export class UserSetCommand extends Command<typeof options> {
|
|
27
|
+
readonly name = "set";
|
|
28
|
+
override displayName = "Set User Config";
|
|
29
|
+
readonly description = "Set a user configuration value";
|
|
30
|
+
readonly options = options;
|
|
31
|
+
|
|
32
|
+
override readonly actionLabel = "Set Value";
|
|
33
|
+
|
|
34
|
+
override readonly examples = [
|
|
35
|
+
{ command: "config user set --key name --value 'Jane Doe'", description: "Set user name" },
|
|
36
|
+
{ command: "config user set --key theme --value light", description: "Set theme" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
override async execute(opts: OptionValues<typeof options>): Promise<CommandResult> {
|
|
40
|
+
AppContext.current.logger.info(`Setting user.${opts.key} = "${opts.value}"`);
|
|
41
|
+
|
|
42
|
+
// Simulate setting the value
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
44
|
+
|
|
45
|
+
AppContext.current.logger.info(`Successfully updated user configuration`);
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
data: { key: opts.key, value: opts.value },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override renderResult(result: CommandResult): string {
|
|
53
|
+
if (!result.success) return result.message || "Error";
|
|
54
|
+
const data = result.data as { key: string; value: string };
|
|
55
|
+
return `✓ Set user.${data.key} = "${data.value}"`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type CommandResult
|
|
7
|
-
} from "../../../src/index.ts";
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Command, type CommandResult } from "../../../src/core/command";
|
|
3
|
+
import { AppContext } from "../../../src/core/context";
|
|
4
|
+
import type { OptionSchema, OptionValues } from "../../../src/types/command";
|
|
5
|
+
import { JsonHighlight } from "../../../src/tui/components/JsonHighlight.tsx";
|
|
8
6
|
|
|
9
7
|
const greetOptions = {
|
|
10
8
|
name: {
|
|
@@ -20,7 +18,7 @@ const greetOptions = {
|
|
|
20
18
|
type: "boolean",
|
|
21
19
|
description: "Use uppercase",
|
|
22
20
|
alias: "l",
|
|
23
|
-
default:
|
|
21
|
+
default: true,
|
|
24
22
|
label: "Loud Mode",
|
|
25
23
|
order: 2,
|
|
26
24
|
group: "Options",
|
|
@@ -37,6 +35,7 @@ const greetOptions = {
|
|
|
37
35
|
|
|
38
36
|
export class GreetCommand extends Command<typeof greetOptions> {
|
|
39
37
|
readonly name = "greet";
|
|
38
|
+
override displayName = "Greet";
|
|
40
39
|
readonly description = "Greet someone with a friendly message";
|
|
41
40
|
readonly options = greetOptions;
|
|
42
41
|
|
|
@@ -47,16 +46,20 @@ export class GreetCommand extends Command<typeof greetOptions> {
|
|
|
47
46
|
{ command: "greet --name World --loud --times 3", description: "Loud greeting 3 times" },
|
|
48
47
|
];
|
|
49
48
|
|
|
50
|
-
override async execute(
|
|
49
|
+
override async execute(opts: OptionValues<typeof greetOptions>): Promise<CommandResult> {
|
|
51
50
|
const greeting = this.createGreeting(opts);
|
|
52
|
-
|
|
51
|
+
AppContext.current.logger.trace(greeting);
|
|
53
52
|
return {
|
|
54
53
|
success: true,
|
|
55
|
-
data: { greeting, timestamp: new Date().toISOString() },
|
|
54
|
+
data: { greeting, timestamp: new Date().toISOString(), meta: { loud: opts.loud, times: opts.times } },
|
|
56
55
|
message: greeting,
|
|
57
56
|
};
|
|
58
57
|
}
|
|
59
58
|
|
|
59
|
+
override renderResult(result: CommandResult): ReactNode {
|
|
60
|
+
return JsonHighlight({ value: result.data });
|
|
61
|
+
}
|
|
62
|
+
|
|
60
63
|
override getClipboardContent(result: CommandResult): string | undefined {
|
|
61
64
|
const data = result.data as { greeting?: string } | undefined;
|
|
62
65
|
return data?.greeting;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type OptionSchema,
|
|
5
|
-
type OptionValues,
|
|
6
|
-
type CommandResult
|
|
7
|
-
} from "../../../src/index.ts";
|
|
1
|
+
import { Command, type CommandResult } from "../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../src/types/command";
|
|
8
4
|
|
|
9
5
|
const mathOptions = {
|
|
10
6
|
operation: {
|
|
@@ -44,15 +40,16 @@ const mathOptions = {
|
|
|
44
40
|
|
|
45
41
|
export class MathCommand extends Command<typeof mathOptions> {
|
|
46
42
|
readonly name = "math";
|
|
43
|
+
override displayName = "Math Operations";
|
|
47
44
|
readonly description = "Perform basic math operations";
|
|
48
45
|
readonly options = mathOptions;
|
|
49
46
|
|
|
50
47
|
override readonly actionLabel = "Calculate";
|
|
51
48
|
|
|
52
|
-
override async execute(
|
|
49
|
+
override async execute(opts: OptionValues<typeof mathOptions>): Promise<CommandResult> {
|
|
53
50
|
const result = this.calculate(opts);
|
|
54
51
|
if (!result.success) {
|
|
55
|
-
|
|
52
|
+
AppContext.current.logger.error(result.message || "Calculation failed");
|
|
56
53
|
}
|
|
57
54
|
return result;
|
|
58
55
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type OptionSchema,
|
|
5
|
-
type OptionValues,
|
|
6
|
-
type CommandResult
|
|
7
|
-
} from "../../../src/index.ts";
|
|
1
|
+
import { Command, type CommandExecutionContext, type CommandResult } from "../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../src/types/command";
|
|
8
4
|
|
|
9
5
|
const statusOptions = {
|
|
10
6
|
detailed: {
|
|
@@ -12,21 +8,23 @@ const statusOptions = {
|
|
|
12
8
|
description: "Show detailed status",
|
|
13
9
|
default: false,
|
|
14
10
|
label: "Detailed",
|
|
11
|
+
alias: "d",
|
|
15
12
|
order: 1,
|
|
16
13
|
},
|
|
17
14
|
} as const satisfies OptionSchema;
|
|
18
15
|
|
|
19
16
|
export class StatusCommand extends Command<typeof statusOptions> {
|
|
20
17
|
readonly name = "status";
|
|
18
|
+
override displayName = "Status";
|
|
21
19
|
readonly description = "Show application status";
|
|
22
20
|
readonly options = statusOptions;
|
|
23
21
|
|
|
24
22
|
override readonly actionLabel = "Check Status";
|
|
25
23
|
override readonly immediateExecution = true; // No required fields
|
|
26
24
|
|
|
27
|
-
override async execute(
|
|
28
|
-
const result = await this.getStatus(opts);
|
|
29
|
-
|
|
25
|
+
override async execute(opts: OptionValues<typeof statusOptions>, execCtx : CommandExecutionContext): Promise<CommandResult> {
|
|
26
|
+
const result = await this.getStatus(opts, execCtx);
|
|
27
|
+
AppContext.current.logger.info(result.message || "Status check complete");
|
|
30
28
|
return result;
|
|
31
29
|
}
|
|
32
30
|
|
|
@@ -39,15 +37,28 @@ export class StatusCommand extends Command<typeof statusOptions> {
|
|
|
39
37
|
` Uptime: ${data.uptime}`,
|
|
40
38
|
` Memory: ${data.memory}`,
|
|
41
39
|
` Platform: ${data.platform}`,
|
|
42
|
-
`
|
|
40
|
+
` Bun: ${data.version}`,
|
|
43
41
|
].join("\n");
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
private async getStatus(opts: OptionValues<typeof statusOptions
|
|
44
|
+
private async getStatus(opts: OptionValues<typeof statusOptions>, execCtx: CommandExecutionContext): Promise<CommandResult> {
|
|
47
45
|
const detailed = opts.detailed as boolean;
|
|
48
46
|
|
|
49
47
|
// Simulate some async work
|
|
50
|
-
await new Promise(
|
|
48
|
+
await new Promise(resolve => {
|
|
49
|
+
let count = 0;
|
|
50
|
+
let interval = setInterval(() => {
|
|
51
|
+
count++;
|
|
52
|
+
AppContext.current.logger.info(`Applying configuration... (${count}/5)`);
|
|
53
|
+
if (count >= 5 || execCtx.signal.aborted) {
|
|
54
|
+
if (count < 5) {
|
|
55
|
+
AppContext.current.logger.warn("Status check aborted");
|
|
56
|
+
}
|
|
57
|
+
clearInterval(interval);
|
|
58
|
+
resolve(undefined);
|
|
59
|
+
}
|
|
60
|
+
}, 1000);
|
|
61
|
+
});
|
|
51
62
|
|
|
52
63
|
const memMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
53
64
|
const uptimeSec = Math.round(process.uptime());
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
* bun examples/tui-app/index.ts greet --name "World" --loud
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { TuiApplication } from "../../src/
|
|
16
|
-
import {
|
|
15
|
+
import { TuiApplication } from "../../src/tui/TuiApplication.tsx";
|
|
16
|
+
import { ConfigCommand } from "./commands/config/index.ts";
|
|
17
|
+
import { GreetCommand } from "./commands/greet.ts";
|
|
18
|
+
import { MathCommand } from "./commands/math.ts";
|
|
19
|
+
import { StatusCommand } from "./commands/status.ts";
|
|
17
20
|
|
|
18
21
|
class ExampleApp extends TuiApplication {
|
|
19
22
|
constructor() {
|
|
@@ -24,6 +27,7 @@ class ExampleApp extends TuiApplication {
|
|
|
24
27
|
new GreetCommand(),
|
|
25
28
|
new MathCommand(),
|
|
26
29
|
new StatusCommand(),
|
|
30
|
+
new ConfigCommand(),
|
|
27
31
|
],
|
|
28
32
|
enableTui: true,
|
|
29
33
|
});
|
|
@@ -31,4 +35,4 @@ class ExampleApp extends TuiApplication {
|
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
// Run the app
|
|
34
|
-
await new ExampleApp().run(
|
|
38
|
+
await new ExampleApp().run();
|
package/guides/01-hello-world.md
CHANGED
|
@@ -28,7 +28,12 @@ bun add @pablozaiden/terminatui
|
|
|
28
28
|
Create `src/greet.ts`:
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
Command,
|
|
33
|
+
type OptionSchema,
|
|
34
|
+
type CommandResult,
|
|
35
|
+
type CommandExecutionContext,
|
|
36
|
+
} from "@pablozaiden/terminatui";
|
|
32
37
|
|
|
33
38
|
const options = {
|
|
34
39
|
name: {
|
|
@@ -43,7 +48,7 @@ export class GreetCommand extends Command<typeof options> {
|
|
|
43
48
|
readonly description = "Greet someone";
|
|
44
49
|
readonly options = options;
|
|
45
50
|
|
|
46
|
-
execute(
|
|
51
|
+
execute(config: { name: string }, _execCtx: CommandExecutionContext): CommandResult {
|
|
47
52
|
console.log(`Hello, ${config.name}!`);
|
|
48
53
|
return { success: true };
|
|
49
54
|
}
|
|
@@ -19,7 +19,7 @@ myapp greet --name Alice --loud
|
|
|
19
19
|
Update `src/greet.ts`:
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
|
-
import { Command, type
|
|
22
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
23
23
|
|
|
24
24
|
const options = {
|
|
25
25
|
name: {
|
|
@@ -40,7 +40,7 @@ export class GreetCommand extends Command<typeof options> {
|
|
|
40
40
|
readonly description = "Greet someone";
|
|
41
41
|
readonly options = options;
|
|
42
42
|
|
|
43
|
-
execute(
|
|
43
|
+
execute(config: { name: string; loud: boolean }): CommandResult {
|
|
44
44
|
const message = `Hello, ${config.name}!`;
|
|
45
45
|
console.log(config.loud ? message.toUpperCase() : message);
|
|
46
46
|
return { success: true };
|
|
@@ -19,8 +19,7 @@ fileutil count --dir ./src --ext .ts
|
|
|
19
19
|
Create `src/commands/list.ts`:
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
|
-
import {
|
|
23
|
-
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
22
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
24
23
|
|
|
25
24
|
const options = {
|
|
26
25
|
dir: {
|
|
@@ -36,9 +35,9 @@ export class ListCommand extends Command<typeof options> {
|
|
|
36
35
|
readonly description = "List files in a directory";
|
|
37
36
|
readonly options = options;
|
|
38
37
|
|
|
39
|
-
execute(
|
|
38
|
+
async execute(config: { dir: string }): Promise<CommandResult> {
|
|
40
39
|
try {
|
|
41
|
-
const files =
|
|
40
|
+
const files = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: config.dir, onlyFiles: false }));
|
|
42
41
|
console.log(`Files in ${config.dir}:`);
|
|
43
42
|
files.forEach((file) => console.log(` ${file}`));
|
|
44
43
|
return { success: true, message: `Found ${files.length} files` };
|
|
@@ -54,8 +53,7 @@ export class ListCommand extends Command<typeof options> {
|
|
|
54
53
|
Create `src/commands/count.ts`:
|
|
55
54
|
|
|
56
55
|
```typescript
|
|
57
|
-
import {
|
|
58
|
-
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
56
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
59
57
|
|
|
60
58
|
const options = {
|
|
61
59
|
dir: {
|
|
@@ -76,9 +74,9 @@ export class CountCommand extends Command<typeof options> {
|
|
|
76
74
|
readonly description = "Count files in a directory";
|
|
77
75
|
readonly options = options;
|
|
78
76
|
|
|
79
|
-
execute(
|
|
77
|
+
async execute(config: { dir: string; ext?: string }): Promise<CommandResult> {
|
|
80
78
|
try {
|
|
81
|
-
let files =
|
|
79
|
+
let files = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: config.dir, onlyFiles: false }));
|
|
82
80
|
|
|
83
81
|
if (config.ext) {
|
|
84
82
|
files = files.filter((f) => f.endsWith(config.ext!));
|
package/guides/04-subcommands.md
CHANGED
|
@@ -17,7 +17,7 @@ dbctl db status
|
|
|
17
17
|
Create `src/commands/db/migrate.ts`:
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
|
-
import { Command, type
|
|
20
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
21
21
|
|
|
22
22
|
const options = {
|
|
23
23
|
target: {
|
|
@@ -37,7 +37,7 @@ export class MigrateCommand extends Command<typeof options> {
|
|
|
37
37
|
readonly description = "Run database migrations";
|
|
38
38
|
readonly options = options;
|
|
39
39
|
|
|
40
|
-
execute(
|
|
40
|
+
execute(config: { target: string; dry: boolean }): CommandResult {
|
|
41
41
|
ctx.logger.info(`Migrating to: ${config.target}`);
|
|
42
42
|
|
|
43
43
|
if (config.dry) {
|
|
@@ -55,7 +55,7 @@ export class MigrateCommand extends Command<typeof options> {
|
|
|
55
55
|
Create `src/commands/db/seed.ts`:
|
|
56
56
|
|
|
57
57
|
```typescript
|
|
58
|
-
import { Command, type
|
|
58
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
59
59
|
|
|
60
60
|
const options = {
|
|
61
61
|
file: {
|
|
@@ -71,7 +71,7 @@ export class SeedCommand extends Command<typeof options> {
|
|
|
71
71
|
readonly description = "Seed the database with data";
|
|
72
72
|
readonly options = options;
|
|
73
73
|
|
|
74
|
-
execute(
|
|
74
|
+
execute(config: { file: string }): CommandResult {
|
|
75
75
|
ctx.logger.info(`Seeding from: ${config.file}`);
|
|
76
76
|
console.log(`Loading seed data from ${config.file}...`);
|
|
77
77
|
console.log("Database seeded successfully!");
|
|
@@ -83,14 +83,14 @@ export class SeedCommand extends Command<typeof options> {
|
|
|
83
83
|
Create `src/commands/db/status.ts`:
|
|
84
84
|
|
|
85
85
|
```typescript
|
|
86
|
-
import { Command, type
|
|
86
|
+
import { Command, type CommandResult } from "@pablozaiden/terminatui";
|
|
87
87
|
|
|
88
88
|
export class StatusCommand extends Command {
|
|
89
89
|
readonly name = "status";
|
|
90
90
|
readonly description = "Show database status";
|
|
91
91
|
readonly options = {};
|
|
92
92
|
|
|
93
|
-
execute(
|
|
93
|
+
execute(): CommandResult {
|
|
94
94
|
console.log("Database Status:");
|
|
95
95
|
console.log(" Connected: Yes");
|
|
96
96
|
console.log(" Version: 1.2.3");
|
|
@@ -105,7 +105,7 @@ export class StatusCommand extends Command {
|
|
|
105
105
|
Create `src/commands/db/index.ts`:
|
|
106
106
|
|
|
107
107
|
```typescript
|
|
108
|
-
import { Command, type
|
|
108
|
+
import { Command, type CommandResult } from "@pablozaiden/terminatui";
|
|
109
109
|
import { MigrateCommand } from "./migrate";
|
|
110
110
|
import { SeedCommand } from "./seed";
|
|
111
111
|
import { StatusCommand } from "./status";
|
|
@@ -123,7 +123,7 @@ export class DbCommand extends Command {
|
|
|
123
123
|
];
|
|
124
124
|
|
|
125
125
|
// Parent command can have its own execute (optional)
|
|
126
|
-
execute(
|
|
126
|
+
execute(): CommandResult {
|
|
127
127
|
console.log("Use 'dbctl db <command>' for database operations.");
|
|
128
128
|
console.log("Available: migrate, seed, status");
|
|
129
129
|
return { success: true };
|