@pablozaiden/terminatui 0.3.0-beta-1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +10 -3
  2. package/src/__tests__/configOnChange.test.ts +63 -0
  3. package/src/builtins/version.ts +1 -1
  4. package/src/index.ts +22 -0
  5. package/src/tui/adapters/ink/InkRenderer.tsx +4 -0
  6. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +4 -0
  7. package/src/tui/adapters/types.ts +1 -0
  8. package/src/tui/screens/ConfigScreen.tsx +6 -1
  9. package/src/tui/screens/ResultsScreen.tsx +9 -1
  10. package/src/tui/screens/RunningScreen.tsx +1 -1
  11. package/.devcontainer/devcontainer.json +0 -19
  12. package/.devcontainer/install-prerequisites.sh +0 -49
  13. package/.github/workflows/copilot-setup-steps.yml +0 -32
  14. package/.github/workflows/pull-request.yml +0 -27
  15. package/.github/workflows/release-npm-package.yml +0 -81
  16. package/AGENTS.md +0 -43
  17. package/CLAUDE.md +0 -1
  18. package/bun.lock +0 -321
  19. package/examples/tui-app/commands/config/app/get.ts +0 -62
  20. package/examples/tui-app/commands/config/app/index.ts +0 -23
  21. package/examples/tui-app/commands/config/app/set.ts +0 -96
  22. package/examples/tui-app/commands/config/index.ts +0 -28
  23. package/examples/tui-app/commands/config/user/get.ts +0 -61
  24. package/examples/tui-app/commands/config/user/index.ts +0 -23
  25. package/examples/tui-app/commands/config/user/set.ts +0 -57
  26. package/examples/tui-app/commands/greet.ts +0 -78
  27. package/examples/tui-app/commands/math.ts +0 -111
  28. package/examples/tui-app/commands/status.ts +0 -86
  29. package/examples/tui-app/index.ts +0 -38
  30. package/guides/01-hello-world.md +0 -101
  31. package/guides/02-adding-options.md +0 -103
  32. package/guides/03-multiple-commands.md +0 -161
  33. package/guides/04-subcommands.md +0 -206
  34. package/guides/05-interactive-tui.md +0 -209
  35. package/guides/06-config-validation.md +0 -256
  36. package/guides/07-async-cancellation.md +0 -334
  37. package/guides/08-complete-application.md +0 -507
  38. package/guides/README.md +0 -78
  39. package/tsconfig.json +0 -25
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "@pablozaiden/terminatui",
3
- "version": "0.3.0-beta-1",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal UI and Command Line Application Framework",
5
5
  "repository": {
6
6
  "url": "https://github.com/PabloZaiden/terminatui"
7
7
  },
8
8
  "type": "module",
9
- "main": "src/index.ts",
10
- "types": "src/index.ts",
9
+ "files": [
10
+ "src/**/*"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "default": "./src/index.ts"
16
+ }
17
+ },
11
18
  "scripts": {
12
19
  "build": "bunx tsc --noEmit",
13
20
  "test": "bun test",
@@ -0,0 +1,63 @@
1
+ import { test, expect } from "bun:test";
2
+ import { Command } from "../core/command.ts";
3
+ import type { OptionSchema } from "../types/command.ts";
4
+
5
+ class TestCommand extends Command<typeof TestCommand.Options> {
6
+ static readonly Options = {
7
+ a: { type: "string" as const, description: "a" },
8
+ b: { type: "string" as const, description: "b" },
9
+ } as const satisfies OptionSchema;
10
+
11
+ readonly name = "my";
12
+ readonly description = "my";
13
+ readonly options = TestCommand.Options;
14
+
15
+ public readonly onChangeCalls: Array<[
16
+ string,
17
+ unknown,
18
+ Record<string, unknown>
19
+ ]> = [];
20
+
21
+ override execute(): void {}
22
+
23
+ override onConfigChange(
24
+ key: string,
25
+ value: unknown,
26
+ allValues: Record<string, unknown>
27
+ ) {
28
+ this.onChangeCalls.push([key, value, allValues]);
29
+ if (key === "a") {
30
+ return { b: "derived" };
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ applyTuiConfigChange(
36
+ key: string,
37
+ value: unknown,
38
+ values: Record<string, unknown>
39
+ ): Record<string, unknown> {
40
+ let nextValues: Record<string, unknown> = { ...values, [key]: value };
41
+
42
+ const updates = this.onConfigChange?.(key, value, nextValues);
43
+ if (updates && typeof updates === "object") {
44
+ nextValues = { ...nextValues, ...updates };
45
+ }
46
+
47
+ return nextValues;
48
+ }
49
+ }
50
+
51
+ test("onConfigChange merges returned updates", () => {
52
+ const command = new TestCommand();
53
+
54
+ const next = command.applyTuiConfigChange("a", "new", {
55
+ a: "old",
56
+ b: "oldb",
57
+ });
58
+
59
+ expect(command.onChangeCalls.length).toBe(1);
60
+ expect(command.onChangeCalls[0]?.[0]).toBe("a");
61
+ expect(command.onChangeCalls[0]?.[1]).toBe("new");
62
+ expect(next).toEqual({ a: "new", b: "derived" });
63
+ });
@@ -18,7 +18,7 @@ interface VersionConfig {
18
18
  * Format version string with optional commit hash.
19
19
  * If commitHash is empty or undefined, shows "(dev)".
20
20
  */
21
- function formatVersion(version: string, commitHash?: string): string {
21
+ export function formatVersion(version: string, commitHash?: string): string {
22
22
  const hashPart = commitHash && commitHash.length > 0
23
23
  ? commitHash.substring(0, 7)
24
24
  : "(dev)";
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ export * from "./builtins/help.ts";
2
+ export * from "./builtins/settings.ts";
3
+ export * from "./builtins/version.ts";
4
+
5
+ export * from "./cli/parser.ts";
6
+ export * from "./cli/output/colors.ts";
7
+
8
+ export * from "./core/application.ts";
9
+ export * from "./core/command.ts";
10
+ export * from "./core/context.ts";
11
+ export * from "./core/help.ts";
12
+ export * from "./core/knownCommands.ts";
13
+ export * from "./core/logger.ts";
14
+ export * from "./core/registry.ts";
15
+
16
+ export * from "./tui/TuiApplication.tsx";
17
+ export * from "./tui/TuiRoot.tsx";
18
+ export * from "./tui/registry.ts";
19
+ export * from "./tui/theme.ts";
20
+ export * from "./types/command.ts";
21
+
22
+ export * from "./tui/components/JsonHighlight.tsx";
@@ -26,6 +26,10 @@ export class InkRenderer implements Renderer {
26
26
  private instance: ReturnType<typeof render> | null = null;
27
27
  private activeKeyboardAdapter: Renderer["keyboard"] | null = null;
28
28
 
29
+ public supportCustomRendering(): boolean {
30
+ return false;
31
+ }
32
+
29
33
  public keyboard: Renderer["keyboard"] = {
30
34
  setActiveHandler: (id, handler) => {
31
35
  return this.activeKeyboardAdapter?.setActiveHandler(id, handler) ?? (() => {});
@@ -27,6 +27,10 @@ export class OpenTuiRenderer implements Renderer {
27
27
 
28
28
  private activeKeyboardAdapter: Renderer["keyboard"] | null = null;
29
29
 
30
+ public supportCustomRendering(): boolean {
31
+ return true;
32
+ }
33
+
30
34
  public keyboard: Renderer["keyboard"] = {
31
35
  setActiveHandler: (id, handler) => {
32
36
  return this.activeKeyboardAdapter?.setActiveHandler(id, handler) ?? (() => {});
@@ -64,6 +64,7 @@ export interface Renderer {
64
64
  initialize: () => Promise<void>;
65
65
  render: (node: ReactNode) => void;
66
66
  destroy: () => void;
67
+ supportCustomRendering: () => boolean;
67
68
 
68
69
  keyboard: KeyboardAdapter;
69
70
  components: RendererComponents;
@@ -116,7 +116,12 @@ export class ConfigScreen extends ScreenBase {
116
116
  currentValue: values[fieldKey],
117
117
  fieldConfigs: derivedFieldConfigs,
118
118
  onSubmit: (value: unknown) => {
119
- const nextValues = { ...values, [fieldKey]: value };
119
+ let nextValues = { ...values, [fieldKey]: value };
120
+ const updates = command.onConfigChange?.(fieldKey, value, nextValues);
121
+ if (updates) {
122
+ nextValues = { ...nextValues, ...updates };
123
+ }
124
+
120
125
  navigation.replace<ConfigParams>(ConfigScreen.Id, { ...params, values: nextValues });
121
126
  navigation.closeModal();
122
127
  },
@@ -5,6 +5,7 @@ import { ResultsPanel } from "../components/ResultsPanel.tsx";
5
5
  import { useClipboardProvider } from "../hooks/useClipboardProvider.ts";
6
6
  import { type ScreenComponent } from "../registry.ts";
7
7
  import { ScreenBase } from "./ScreenBase.ts";
8
+ import { useRenderer } from "../context/RendererContext.tsx";
8
9
 
9
10
  /**
10
11
  * Screen state stored in navigation params.
@@ -36,6 +37,13 @@ export class ResultsScreen extends ScreenBase {
36
37
 
37
38
  const { result, command } = params;
38
39
 
40
+ const renderer = useRenderer();
41
+
42
+ let renderFunction = undefined;
43
+ if (renderer.supportCustomRendering()) {
44
+ renderFunction = command.renderResult;
45
+ }
46
+
39
47
  // Register clipboard provider for this screen
40
48
  useClipboardProvider(
41
49
  useCallback(() => {
@@ -52,7 +60,7 @@ export class ResultsScreen extends ScreenBase {
52
60
  result={result as CommandResult | null}
53
61
  error={null}
54
62
  focused={true}
55
- renderResult={command.renderResult}
63
+ renderResult={renderFunction}
56
64
  />
57
65
  );
58
66
  }
@@ -55,7 +55,7 @@ export class RunningScreen extends ScreenBase {
55
55
  <Panel
56
56
  flexDirection="column"
57
57
  flex={1}
58
- title={`Running ${command.displayName ?? command.name}`}
58
+ title={`${command.displayName ?? command.name}`}
59
59
  padding={1}
60
60
  focused
61
61
  >
@@ -1,19 +0,0 @@
1
- {
2
- "name": "Ubuntu",
3
- "image": "mcr.microsoft.com/devcontainers/base:noble",
4
- "features": {
5
- "ghcr.io/devcontainers/features/github-cli:1": {},
6
- "ghcr.io/devcontainers/features/azure-cli:latest": {},
7
- "ghcr.io/devcontainers/features/docker-outside-of-docker": {}
8
- },
9
- "postCreateCommand": "./.devcontainer/install-prerequisites.sh",
10
- "customizations": {
11
- "vscode": {
12
- "extensions": [
13
- "oven.bun-vscode",
14
- "ms-azuretools.vscode-docker",
15
- "SanjulaGanepola.github-local-actions"
16
- ]
17
- }
18
- }
19
- }
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- set -e
4
-
5
- # if bun is not installed, install it
6
- if ! command -v bun &> /dev/null
7
- then
8
- echo "Bun not found, installing..."
9
-
10
- curl -fsSL https://bun.com/install | bash
11
-
12
- # manual fix for missing package.json issue
13
- if [ ! -f "$HOME/.bun/install/global/package.json" ]; then
14
- echo "Creating missing package.json for bun global..."
15
- mkdir -p "$HOME/.bun/install/global"
16
- echo '{}' > "$HOME/.bun/install/global/package.json"
17
- fi
18
-
19
-
20
- # add bun to PATH
21
- export PATH="$HOME/.bun/bin:$PATH"
22
- fi
23
-
24
- # if there is no symlink for node, create it
25
- if ! command -v node &> /dev/null
26
- then
27
- # discover the bun binary location
28
- bunBinary=$(which bun)
29
- echo "Bun binary located at: $bunBinary"
30
- echo "Creating symlink for node to bun..."
31
- mkdir -p $HOME/.local/bin
32
- ln -sf "$bunBinary" "$HOME/.local/bin/node"
33
- fi
34
-
35
- export BUN_INSTALL_BIN=$HOME/.bun/bin
36
- export BUN_INSTALL_GLOBAL_DIR=$HOME/.bun/global
37
-
38
- # Ensure dirs exist and are owned by the current user
39
- mkdir -p "$BUN_INSTALL_BIN" "$BUN_INSTALL_GLOBAL_DIR"
40
- export PATH="$HOME/.bun/bin:$HOME/.bun/global/bin:${PATH}"
41
-
42
- # add the paths to bashrc and zshrc
43
- echo 'export PATH="$HOME/.bun/bin:$HOME/.bun/global/bin:${PATH}" ' >> $HOME/.bashrc
44
- echo 'export PATH="$HOME/.bun/bin:$HOME/.bun/global/bin:${PATH}" '>> $HOME/.zshrc
45
-
46
- # ensure $HOME/.local/bin is in PATH for current session and future shells
47
- export PATH="$HOME/.local/bin:$PATH"
48
- echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
49
- echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.zshrc"
@@ -1,32 +0,0 @@
1
- name: "Copilot Setup Steps"
2
-
3
- on:
4
- workflow_dispatch:
5
- push:
6
- paths:
7
- - .github/workflows/copilot-setup-steps.yml
8
- pull_request:
9
- paths:
10
- - .github/workflows/copilot-setup-steps.yml
11
-
12
- jobs:
13
- # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
14
- copilot-setup-steps:
15
- runs-on: ubuntu-latest
16
-
17
- permissions:
18
- contents: read
19
-
20
- steps:
21
- - name: Checkout repository
22
- uses: actions/checkout@v6
23
-
24
- - name: Install prerequisites
25
- run: |
26
- chmod +x ./.devcontainer/install-prerequisites.sh
27
- ./.devcontainer/install-prerequisites.sh
28
- echo "$HOME/.bun/bin" >> $GITHUB_PATH
29
- echo "$HOME/.bun/global/bin" >> $GITHUB_PATH
30
-
31
- - name: Install dependencies
32
- run: bun install
@@ -1,27 +0,0 @@
1
- name: Pull Request
2
-
3
- on:
4
- pull_request:
5
- branches: [ "main" ]
6
-
7
- jobs:
8
- test:
9
- runs-on: ubuntu-latest
10
- environment: PR
11
-
12
- steps:
13
- - name: Checkout repository
14
- uses: actions/checkout@v6
15
-
16
- - name: Install prerequisites
17
- run: |
18
- chmod +x .devcontainer/install-prerequisites.sh
19
- ./.devcontainer/install-prerequisites.sh
20
- echo "$HOME/.bun/bin" >> $GITHUB_PATH
21
- echo "$HOME/.bun/global/bin" >> $GITHUB_PATH
22
-
23
- - name: Install dependencies
24
- run: bun install
25
-
26
- - name: Run tests
27
- run: bun test
@@ -1,81 +0,0 @@
1
- name: Release NPM Package
2
-
3
- on:
4
- release:
5
- types: [published]
6
- workflow_dispatch:
7
- inputs:
8
- tag:
9
- description: 'Release tag (e.g., v1.2.3)'
10
- required: true
11
-
12
- jobs:
13
- publish:
14
- name: Publish Package
15
- runs-on: ubuntu-latest
16
- permissions:
17
- contents: read
18
- id-token: write
19
- steps:
20
- - name: Determine release tag
21
- id: get_tag
22
- run: |
23
- echo "Event name: ${{ github.event_name }}"
24
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
25
- TAG="${{ github.event.inputs.tag }}"
26
- else
27
- TAG="${{ github.event.release.tag_name }}"
28
- fi
29
-
30
- # if the tag is empty, exit with error
31
- if [ -z "$TAG" ]; then
32
- echo "Error: Tag is empty"
33
- exit 1
34
- fi
35
-
36
- # Strip 'v' prefix if present
37
- VERSION="${TAG#v}"
38
-
39
- echo "Using tag: $TAG"
40
- echo "Using version: $VERSION"
41
-
42
- echo "tag=$TAG" >> $GITHUB_OUTPUT
43
- echo "version=$VERSION" >> $GITHUB_OUTPUT
44
-
45
- - name: Checkout repository
46
- uses: actions/checkout@v6
47
- with:
48
- ref: ${{ steps.get_tag.outputs.tag }}
49
-
50
- - name: Install bun
51
- run: |
52
- curl -fsSL https://bun.com/install | bash
53
- echo "$HOME/.bun/bin" >> $GITHUB_PATH
54
-
55
- - uses: actions/setup-node@v4
56
- with:
57
- node-version: '24'
58
- registry-url: 'https://registry.npmjs.org'
59
-
60
- - name: Set package version
61
- run: |
62
- VERSION="${{ steps.get_tag.outputs.version }}"
63
- jq --arg version "$VERSION" '.version = $version' package.json > package.json.tmp && mv package.json.tmp package.json
64
-
65
- - name: Verify package version
66
- run: |
67
- EXPECTED_VERSION="${{ steps.get_tag.outputs.version }}"
68
- ACTUAL_VERSION=$(jq -r '.version' package.json)
69
- if [ "$EXPECTED_VERSION" != "$ACTUAL_VERSION" ]; then
70
- echo "Error: Version mismatch. Expected $EXPECTED_VERSION, got $ACTUAL_VERSION"
71
- exit 1
72
- fi
73
- echo "Version verified: $ACTUAL_VERSION"
74
-
75
- - name: Install dependencies
76
- run: bun install
77
- - name: Build package
78
- run: bun run build
79
-
80
- - name: Publish Package
81
- run: npm publish --no-build
package/AGENTS.md DELETED
@@ -1,43 +0,0 @@
1
- ## General guidelines
2
-
3
- - Never use `git` operations. That's up to the user.
4
- - Always prefer simplicity, usability and top level type safety over cleverness.
5
- - Don't create index.ts files that re-export things from other files. Always import directly from the file you need.
6
- - Prefer classes over standalone functions when it makes sense.
7
- - Before doing something, check the patterns used in the rest of the codebase.
8
- - Never use `import("...")` dynamic imports. Always use static imports (unless absolutely necessary).
9
-
10
- ## Bun specifics
11
- This is a Bun-only project. Never check if something might not be supported in another environment. You can assume Bun is always available.
12
-
13
- Always use Bun features and APIs where possible.
14
-
15
- - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
16
- - Use `bun test` instead of `jest` or `vitest`
17
- - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
18
- - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
19
- - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
20
- - Use `bunx <package> <command>` instead of `npx <package> <command>`
21
- - Bun automatically loads .env, so don't use dotenv.
22
-
23
- ## APIs
24
-
25
- - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
26
- - Bun.$`ls` instead of execa.
27
-
28
- ## Testing
29
-
30
- Always run `bun run build` before running tests, to make sure there are no build errors.
31
- Use `bun run test` to run all the tests.
32
-
33
- Always run `bun run test` when you think you are done making changes.
34
-
35
- ```ts#index.test.ts
36
- import { test, expect } from "bun:test";
37
-
38
- test("hello world", () => {
39
- expect(1).toBe(1);
40
- });
41
- ```
42
-
43
- For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
package/CLAUDE.md DELETED
@@ -1 +0,0 @@
1
- @AGENTS.md