@robota-sdk/agent-ui-terminal 3.0.0-beta.81
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/LICENSE +661 -0
- package/README.md +27 -0
- package/dist/node/index.cjs +32 -0
- package/dist/node/index.d.cts +1066 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1066 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +33 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +93 -0
- package/src/App.tsx +146 -0
- package/src/AppPresentation.tsx +190 -0
- package/src/AppView.tsx +26 -0
- package/src/BackgroundTaskPanel.tsx +73 -0
- package/src/CjkTextInput.tsx +336 -0
- package/src/ConfirmPrompt.tsx +140 -0
- package/src/ContextWarningBanner.tsx +45 -0
- package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
- package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
- package/src/HistorySearchOverlay.tsx +150 -0
- package/src/InputArea.tsx +338 -0
- package/src/ListPicker.tsx +158 -0
- package/src/MenuSelect.tsx +159 -0
- package/src/MessageList.tsx +308 -0
- package/src/MultiSelectList.tsx +294 -0
- package/src/PendingActionPrompt.tsx +121 -0
- package/src/PermissionPrompt.tsx +186 -0
- package/src/PluginTUI.tsx +258 -0
- package/src/RoleLabel.tsx +72 -0
- package/src/SafeText.tsx +66 -0
- package/src/SessionEventNotices.tsx +27 -0
- package/src/SessionPicker.tsx +72 -0
- package/src/SessionStatusBar.tsx +76 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +284 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +455 -0
- package/src/TextPrompt.tsx +112 -0
- package/src/ThemePicker.tsx +366 -0
- package/src/ToolCommandOutput.tsx +48 -0
- package/src/ToolDiffBlock.tsx +44 -0
- package/src/TransportTUI.tsx +166 -0
- package/src/TuiInteractionChannel.ts +567 -0
- package/src/UpdateNotice.tsx +18 -0
- package/src/UsageSummaryEntry.tsx +49 -0
- package/src/WaveText.tsx +70 -0
- package/src/__tests__/ListPicker.test.tsx +188 -0
- package/src/__tests__/MenuSelect.test.tsx +127 -0
- package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
- package/src/__tests__/PluginTUI.test.tsx +167 -0
- package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
- package/src/__tests__/SupervisedSessionView-render.test.tsx +24 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +629 -0
- package/src/__tests__/TextPrompt.test.tsx +112 -0
- package/src/__tests__/TransportTUI.test.tsx +124 -0
- package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +284 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +642 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
- package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
- package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
- package/src/__tests__/background-task-panel.test.tsx +98 -0
- package/src/__tests__/background-task-row-format.test.ts +152 -0
- package/src/__tests__/channel-factory-integration.test.ts +150 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +67 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +78 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +186 -0
- package/src/__tests__/cjk-text-input.test.ts +191 -0
- package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
- package/src/__tests__/command-output-summary.test.ts +95 -0
- package/src/__tests__/command-result-handler.test.ts +171 -0
- package/src/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/__tests__/confirm-permission-flow.test.ts +149 -0
- package/src/__tests__/confirm-prompt.test.tsx +103 -0
- package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
- package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +45 -0
- package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
- package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
- package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
- package/src/__tests__/external-prompt-notice.test.tsx +100 -0
- package/src/__tests__/fixtures/command-handoff-driver.tsx +111 -0
- package/src/__tests__/fixtures/fake-editor.sh +7 -0
- package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
- package/src/__tests__/fork-attach.test.tsx +235 -0
- package/src/__tests__/helpers/terminal-profiles.ts +152 -0
- package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
- package/src/__tests__/history-clear-broadcast.test.ts +66 -0
- package/src/__tests__/history-search-overlay.test.tsx +329 -0
- package/src/__tests__/humanize-tool-name.test.ts +30 -0
- package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
- package/src/__tests__/input-area-flow.test.ts +199 -0
- package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
- package/src/__tests__/interaction-channel-charter.test.ts +13 -0
- package/src/__tests__/key-hint-consistency.test.tsx +249 -0
- package/src/__tests__/key-hint-footer.test.tsx +69 -0
- package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
- package/src/__tests__/message-list-rendering.test.tsx +473 -0
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
- package/src/__tests__/output-style-reaches-session.test.ts +35 -0
- package/src/__tests__/palette-consistency.test.ts +330 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
- package/src/__tests__/product-display-name.test.tsx +79 -0
- package/src/__tests__/prompt-queue.test.tsx +260 -0
- package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
- package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +342 -0
- package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
- package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
- package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
- package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
- package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +291 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +165 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +186 -0
- package/src/__tests__/pty/isolated-home.ts +64 -0
- package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
- package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
- package/src/__tests__/pty/pty-driver.ts +153 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +134 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +138 -0
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
- package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
- package/src/__tests__/pty/spawn-pty.test.ts +120 -0
- package/src/__tests__/pty/spawn-pty.ts +229 -0
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
- package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
- package/src/__tests__/pty-driver-dispose.test.ts +43 -0
- package/src/__tests__/real-cursor-positioning.test.tsx +263 -0
- package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
- package/src/__tests__/render-channel-options.test.ts +201 -0
- package/src/__tests__/render-lifecycle.test.ts +36 -0
- package/src/__tests__/render-markdown.test.ts +164 -0
- package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
- package/src/__tests__/safe-text-boundary.test.tsx +75 -0
- package/src/__tests__/screen-reader-input.test.tsx +106 -0
- package/src/__tests__/screen-reader-labels.test.ts +91 -0
- package/src/__tests__/screen-reader-menus.test.tsx +200 -0
- package/src/__tests__/screen-reader-pacing.test.ts +230 -0
- package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
- package/src/__tests__/screen-reader-render-options.test.ts +262 -0
- package/src/__tests__/screen-reader-stdout.test.ts +322 -0
- package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
- package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
- package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
- package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
- package/src/__tests__/selection-flow.test.ts +61 -0
- package/src/__tests__/session-event-notices.test.tsx +31 -0
- package/src/__tests__/session-init-poller.test.ts +102 -0
- package/src/__tests__/session-switch-channel.test.tsx +470 -0
- package/src/__tests__/shutdown-signal.test.ts +34 -0
- package/src/__tests__/status-activity.test.ts +71 -0
- package/src/__tests__/status-bar.test.tsx +282 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +177 -0
- package/src/__tests__/terminal-capabilities.test.ts +153 -0
- package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
- package/src/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/__tests__/theme-picker.test.tsx +345 -0
- package/src/__tests__/transport-boundary.test.ts +11 -0
- package/src/__tests__/tui-app-channel-port.test.ts +31 -0
- package/src/__tests__/tui-channel-init-failure.test.ts +62 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +139 -0
- package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
- package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
- package/src/__tests__/tui-state-manager.test.ts +451 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +136 -0
- package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
- package/src/attention/__tests__/attention-tracker.test.ts +103 -0
- package/src/attention/__tests__/countdown.test.ts +15 -0
- package/src/attention/__tests__/focus-input-filter.test.ts +92 -0
- package/src/attention/__tests__/interval-recap.test.ts +49 -0
- package/src/attention/attention-coordinator.ts +71 -0
- package/src/attention/attention-tracker.ts +124 -0
- package/src/attention/countdown.ts +24 -0
- package/src/attention/focus-input-filter.ts +131 -0
- package/src/attention/interval-recap.ts +148 -0
- package/src/attention/time-units.ts +6 -0
- package/src/attention-bell.ts +79 -0
- package/src/attributed-user-echo.ts +30 -0
- package/src/background-task-row-format.ts +99 -0
- package/src/command-interaction.ts +8 -0
- package/src/command-output-summary.ts +129 -0
- package/src/create-default-tui-cli-adapter.ts +45 -0
- package/src/execution-workspace-switcher-selection.ts +118 -0
- package/src/execution-workspace-view-model.ts +139 -0
- package/src/external-prompt-notice.tsx +38 -0
- package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
- package/src/flows/__tests__/defer-submit.test.ts +87 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +124 -0
- package/src/flows/background-focus-flow.ts +44 -0
- package/src/flows/cjk-cursor-motion.ts +75 -0
- package/src/flows/cjk-text-deletion.ts +53 -0
- package/src/flows/cjk-text-input-flow.ts +248 -0
- package/src/flows/cjk-text-input-types.ts +24 -0
- package/src/flows/confirm-prompt-flow.ts +90 -0
- package/src/flows/defer-submit.ts +67 -0
- package/src/flows/fork-attach-flow.ts +62 -0
- package/src/flows/input-area-flow.ts +188 -0
- package/src/flows/permission-prompt-flow.ts +108 -0
- package/src/flows/real-cursor-flow.ts +83 -0
- package/src/flows/selection-flow.ts +205 -0
- package/src/flows/session-init-poller.ts +76 -0
- package/src/flows/text-prompt-flow.ts +93 -0
- package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
- package/src/history-search/history-search-flow.ts +94 -0
- package/src/history-search/useHistorySearch.ts +336 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +74 -0
- package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
- package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
- package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
- package/src/hooks/command-result-handler.ts +44 -0
- package/src/hooks/side-effects-types.ts +45 -0
- package/src/hooks/useAppController.ts +267 -0
- package/src/hooks/useAppInputBindings.ts +145 -0
- package/src/hooks/useAppInteractionState.ts +81 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +74 -0
- package/src/hooks/useAppScreenState.ts +81 -0
- package/src/hooks/useAppSubmissionState.ts +45 -0
- package/src/hooks/useAppThemeState.ts +157 -0
- package/src/hooks/useAppWorkspaceState.ts +149 -0
- package/src/hooks/useAppearanceSettings.ts +30 -0
- package/src/hooks/useAutocomplete.ts +101 -0
- package/src/hooks/useBackgroundPanel.ts +58 -0
- package/src/hooks/useCountdownTick.ts +24 -0
- package/src/hooks/useExecutionDetailPage.ts +69 -0
- package/src/hooks/useForkAttach.ts +60 -0
- package/src/hooks/useInputAreaHistorySearch.ts +72 -0
- package/src/hooks/useInputAreaKeys.ts +114 -0
- package/src/hooks/useNumberedSelection.ts +76 -0
- package/src/hooks/usePluginCallbacks.ts +38 -0
- package/src/hooks/usePluginScreenData.ts +85 -0
- package/src/hooks/useRealCursorPosition.ts +125 -0
- package/src/hooks/useRenderMarkdown.ts +38 -0
- package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
- package/src/hooks/useSideEffects.ts +144 -0
- package/src/hooks/useStatusLineSettings.ts +50 -0
- package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
- package/src/hooks/useTuiChannel.ts +129 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +56 -0
- package/src/input-area-rules.tsx +71 -0
- package/src/json-value.ts +18 -0
- package/src/key-hint-footer.tsx +54 -0
- package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +111 -0
- package/src/keybindings/keybinding-catalogue.ts +171 -0
- package/src/keybindings/keybinding-registry.ts +303 -0
- package/src/keybindings/keybinding-syntax.ts +155 -0
- package/src/keybindings/keybindings-context.tsx +124 -0
- package/src/keybindings/node-keybindings-source.ts +189 -0
- package/src/numbered-list.tsx +113 -0
- package/src/plugin-tui-handlers.ts +163 -0
- package/src/product-display-name-context.tsx +30 -0
- package/src/render-markdown.ts +209 -0
- package/src/render.tsx +544 -0
- package/src/sanitize-terminal-text.ts +171 -0
- package/src/screen-reader-announcement.ts +52 -0
- package/src/screen-reader-context.tsx +32 -0
- package/src/screen-reader-labels.ts +64 -0
- package/src/screen-reader-pacing-context.tsx +47 -0
- package/src/screen-reader-pacing.ts +178 -0
- package/src/screen-reader-stdout.ts +328 -0
- package/src/shutdown-signal.ts +34 -0
- package/src/status-activity.ts +70 -0
- package/src/status-glyph.ts +78 -0
- package/src/terminal-capabilities-context.tsx +27 -0
- package/src/terminal-capabilities.ts +77 -0
- package/src/terminal-focus-reporting.ts +50 -0
- package/src/terminal-handoff-controller.ts +98 -0
- package/src/terminal-marks.ts +63 -0
- package/src/theme/__tests__/built-in-themes.test.ts +117 -0
- package/src/theme/__tests__/color-vision.test.ts +82 -0
- package/src/theme/__tests__/theme-context.test.tsx +146 -0
- package/src/theme/__tests__/theme-document.test.ts +222 -0
- package/src/theme/__tests__/theme-registry.test.ts +138 -0
- package/src/theme/__tests__/theme-styles.test.ts +233 -0
- package/src/theme/built-in-themes.ts +342 -0
- package/src/theme/color-vision.ts +171 -0
- package/src/theme/index.ts +45 -0
- package/src/theme/theme-context.tsx +81 -0
- package/src/theme/theme-contracts.ts +139 -0
- package/src/theme/theme-document.ts +278 -0
- package/src/theme/theme-registry.ts +127 -0
- package/src/theme/theme-styles.ts +177 -0
- package/src/tool-summary-status.ts +59 -0
- package/src/tui-app-channel-port.ts +83 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +172 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +163 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +140 -0
- package/src/tui-session-events.ts +82 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +118 -0
- package/src/tui-state-manager.ts +327 -0
- package/src/types.ts +19 -0
- package/src/use-terminal-title.ts +29 -0
- package/src/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/utils/edit-diff.ts +151 -0
- package/src/utils/input-top-border.ts +19 -0
- package/src/utils/paste-labels.ts +9 -0
- package/src/utils/tool-call-extractor.ts +92 -0
- package/src/utils/tool-diff-summary.ts +75 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-2002 — the TUI's one theme token model.
|
|
3
|
+
*
|
|
4
|
+
* Before this, five things decided a colour: `PALETTE` (Ink names), `ANSI` (raw SGR for the markdown
|
|
5
|
+
* pipeline), `marked-terminal`'s own chalk defaults, `cli-highlight`'s `DEFAULT_THEME`, and a handful
|
|
6
|
+
* of `chalk.<name>` calls. A theme that rewrote only the first two would leave a daltonized run with
|
|
7
|
+
* red/green code blocks, because `cli-highlight` falls back PER KEY to its own theme. So the model
|
|
8
|
+
* carries every group a renderer reads, and the style builder derives every encoding from it through
|
|
9
|
+
* chalk — which is exactly what Ink's own `colorize` does, so no mapping layer is invented.
|
|
10
|
+
*
|
|
11
|
+
* Values use INK'S colour grammar verbatim (`<chalk name> | #rrggbb | ansi256(n) | rgb(r,g,b)`), so a
|
|
12
|
+
* theme file, a built-in and a component all speak one language and the grammar doubles as the
|
|
13
|
+
* injection floor: no raw escape sequence can enter through a theme.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** A colour in Ink's grammar. Validated at the boundary, never parsed twice. */
|
|
17
|
+
export type TThemeColor = string;
|
|
18
|
+
|
|
19
|
+
/** Foreground and border roles every Ink component reads. */
|
|
20
|
+
export interface IThemeColors {
|
|
21
|
+
readonly text: {
|
|
22
|
+
readonly accent: TThemeColor;
|
|
23
|
+
readonly emphasis: TThemeColor;
|
|
24
|
+
readonly success: TThemeColor;
|
|
25
|
+
readonly warning: TThemeColor;
|
|
26
|
+
readonly error: TThemeColor;
|
|
27
|
+
readonly session: TThemeColor;
|
|
28
|
+
readonly muted: TThemeColor;
|
|
29
|
+
readonly onAccent: TThemeColor;
|
|
30
|
+
};
|
|
31
|
+
readonly border: {
|
|
32
|
+
readonly attention: TThemeColor;
|
|
33
|
+
readonly focused: TThemeColor;
|
|
34
|
+
readonly active: TThemeColor;
|
|
35
|
+
readonly muted: TThemeColor;
|
|
36
|
+
readonly error: TThemeColor;
|
|
37
|
+
};
|
|
38
|
+
readonly status: {
|
|
39
|
+
readonly running: TThemeColor;
|
|
40
|
+
readonly success: TThemeColor;
|
|
41
|
+
readonly error: TThemeColor;
|
|
42
|
+
readonly denied: TThemeColor;
|
|
43
|
+
readonly waiting: TThemeColor;
|
|
44
|
+
readonly cancelled: TThemeColor;
|
|
45
|
+
readonly idle: TThemeColor;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every key `marked-terminal` colours by default, including `html` — a key left out would keep that
|
|
51
|
+
* dependency's own gray under every theme.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately absent, because they carry no colour today and giving them one would change rendering
|
|
54
|
+
* rather than theme it: `strong` (`chalk.bold`), `em` (`chalk.italic`), `listitem` and `hr`
|
|
55
|
+
* (`chalk.reset`), and a diff's `diff `/`index ` metadata rows, which this package writes itself as
|
|
56
|
+
* dim over the inherited foreground.
|
|
57
|
+
*/
|
|
58
|
+
export interface IThemeMarkdown {
|
|
59
|
+
readonly heading: TThemeColor;
|
|
60
|
+
readonly firstHeading: TThemeColor;
|
|
61
|
+
readonly code: TThemeColor;
|
|
62
|
+
readonly codespan: TThemeColor;
|
|
63
|
+
readonly link: TThemeColor;
|
|
64
|
+
readonly href: TThemeColor;
|
|
65
|
+
readonly blockquote: TThemeColor;
|
|
66
|
+
readonly del: TThemeColor;
|
|
67
|
+
readonly html: TThemeColor;
|
|
68
|
+
readonly diffAdded: TThemeColor;
|
|
69
|
+
readonly diffRemoved: TThemeColor;
|
|
70
|
+
readonly diffAddedBackground: TThemeColor;
|
|
71
|
+
readonly diffRemovedBackground: TThemeColor;
|
|
72
|
+
readonly diffHunk: TThemeColor;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `cli-highlight`'s seventeen coloured keys, ALL REQUIRED. Its `colorizeNode` reads
|
|
77
|
+
* `theme[token] || DEFAULT_THEME[token] || plain`, so one missing key restores that dependency's
|
|
78
|
+
* red/green pair — the exact thing a daltonized theme exists to remove.
|
|
79
|
+
*/
|
|
80
|
+
export interface IThemeSyntax {
|
|
81
|
+
readonly keyword: TThemeColor;
|
|
82
|
+
readonly built_in: TThemeColor;
|
|
83
|
+
readonly type: TThemeColor;
|
|
84
|
+
readonly literal: TThemeColor;
|
|
85
|
+
readonly number: TThemeColor;
|
|
86
|
+
readonly regexp: TThemeColor;
|
|
87
|
+
readonly string: TThemeColor;
|
|
88
|
+
readonly class: TThemeColor;
|
|
89
|
+
readonly function: TThemeColor;
|
|
90
|
+
readonly comment: TThemeColor;
|
|
91
|
+
readonly doctag: TThemeColor;
|
|
92
|
+
readonly meta: TThemeColor;
|
|
93
|
+
readonly tag: TThemeColor;
|
|
94
|
+
readonly name: TThemeColor;
|
|
95
|
+
readonly attr: TThemeColor;
|
|
96
|
+
readonly addition: TThemeColor;
|
|
97
|
+
readonly deletion: TThemeColor;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The colour ramp of the package's one animation. Cadence is not themed. */
|
|
101
|
+
export interface IThemeMotion {
|
|
102
|
+
readonly wave: readonly [TThemeColor, TThemeColor, TThemeColor, TThemeColor];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type TThemeAppearance = 'dark' | 'light';
|
|
106
|
+
export type TThemeSource = 'built-in' | 'user' | 'plugin';
|
|
107
|
+
|
|
108
|
+
export interface ITuiTheme {
|
|
109
|
+
/** `dark`, `light`, `dark-daltonized`, `light-daltonized`, `custom:<slug>`, `custom:<plugin>:<slug>`. */
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly name: string;
|
|
112
|
+
readonly appearance: TThemeAppearance;
|
|
113
|
+
readonly source: TThemeSource;
|
|
114
|
+
readonly colors: IThemeColors;
|
|
115
|
+
readonly markdown: IThemeMarkdown;
|
|
116
|
+
readonly syntax: IThemeSyntax;
|
|
117
|
+
readonly motion: IThemeMotion;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The seventeen `syntax` keys, in the order `cli-highlight` declares them. */
|
|
121
|
+
export const THEME_SYNTAX_KEYS: readonly (keyof IThemeSyntax)[] = [
|
|
122
|
+
'keyword',
|
|
123
|
+
'built_in',
|
|
124
|
+
'type',
|
|
125
|
+
'literal',
|
|
126
|
+
'number',
|
|
127
|
+
'regexp',
|
|
128
|
+
'string',
|
|
129
|
+
'class',
|
|
130
|
+
'function',
|
|
131
|
+
'comment',
|
|
132
|
+
'doctag',
|
|
133
|
+
'meta',
|
|
134
|
+
'tag',
|
|
135
|
+
'name',
|
|
136
|
+
'attr',
|
|
137
|
+
'addition',
|
|
138
|
+
'deletion',
|
|
139
|
+
];
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-2002 — the theme file boundary.
|
|
3
|
+
*
|
|
4
|
+
* A theme file is `{ name?, base?, overrides? }`: a name, a built-in to start from, and a SPARSE map
|
|
5
|
+
* over the same token paths the built-ins use. It is refused WHOLE on the first unknown token or
|
|
6
|
+
* invalid value, with a `$.`-path naming what was wrong — the keybindings document's contract, for
|
|
7
|
+
* the same reason. A partially-applied theme is the state where a reader cannot tell which colours
|
|
8
|
+
* are theirs and which the base's, and a file that silently loses its third line is worse than a
|
|
9
|
+
* file that is skipped with its reason printed.
|
|
10
|
+
*
|
|
11
|
+
* The token paths are not listed here. They are WALKED from the base theme, so `ITuiTheme` gaining a
|
|
12
|
+
* key makes it overridable and `ITuiTheme` losing one makes it refused — there is no second
|
|
13
|
+
* declaration of the token model to drift from the first.
|
|
14
|
+
*
|
|
15
|
+
* Values go through `isThemeColor`, which is also the injection floor: a raw escape sequence is not
|
|
16
|
+
* in Ink's grammar, so it cannot enter through a theme.
|
|
17
|
+
*/
|
|
18
|
+
import { isJsonRecord } from '../json-value.js';
|
|
19
|
+
import { sanitizeTerminalText } from '../sanitize-terminal-text.js';
|
|
20
|
+
import { BUILT_IN_THEMES, DEFAULT_THEME_ID } from './built-in-themes.js';
|
|
21
|
+
import { isThemeColor, THEME_COLOR_GRAMMAR } from './theme-styles.js';
|
|
22
|
+
|
|
23
|
+
import type { IJsonRecord, TJsonValue } from '../json-value.js';
|
|
24
|
+
import type { ITuiTheme, TThemeSource } from './theme-contracts.js';
|
|
25
|
+
|
|
26
|
+
export interface IThemeDocumentInput {
|
|
27
|
+
/** The namespaced id the caller minted — `custom:<slug>` or `custom:<plugin>:<slug>`. */
|
|
28
|
+
readonly id: string;
|
|
29
|
+
/** The file's own name, for the `Skipped theme "<file>": …` line. */
|
|
30
|
+
readonly fileName: string;
|
|
31
|
+
readonly source: TThemeSource;
|
|
32
|
+
readonly text: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type TThemeDocumentResult =
|
|
36
|
+
{ readonly ok: true; readonly theme: ITuiTheme } | { readonly ok: false; readonly error: string };
|
|
37
|
+
|
|
38
|
+
const DOCUMENT_KEYS = new Set(['name', 'base', 'overrides']);
|
|
39
|
+
const MAX_NAME_LENGTH = 60;
|
|
40
|
+
// eslint-disable-next-line no-control-regex -- matching the control range IS the point here
|
|
41
|
+
const CONTROL_CHARACTER = /[\u0000-\u001F\u007F-\u009F]/u;
|
|
42
|
+
|
|
43
|
+
const CONTROL_CHARACTERS = new RegExp(CONTROL_CHARACTER.source, 'gu');
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Untrusted text on its way into a line a terminal will print — the one escaping policy, exported
|
|
47
|
+
* so the surfaces that build their OWN diagnostics use it rather than re-deriving it.
|
|
48
|
+
*
|
|
49
|
+
* A diagnostic is the one part of a refused file that reaches the terminal, and it quotes what was
|
|
50
|
+
* wrong — so without this the injection floor the grammar provides is defeated by the message that
|
|
51
|
+
* reports a violation of it. Escaped rather than stripped, because an author has to SEE what their
|
|
52
|
+
* file contains (`"\u001b[31m"`) instead of a value that looks fine.
|
|
53
|
+
*
|
|
54
|
+
* `JSON.stringify` alone is NOT enough, and that is the whole reason this is a function: it escapes
|
|
55
|
+
* U+0000–U+001F and leaves U+007F and the C1 range literal — including U+009B and U+009D, the
|
|
56
|
+
* single-byte spellings of CSI and OSC. A terminal accepts both spellings (`sanitize-terminal-text.ts`
|
|
57
|
+
* says so and handles both), so quoting only the 7-bit one leaves the 8-bit one's parameters
|
|
58
|
+
* standing as a live sequence.
|
|
59
|
+
*/
|
|
60
|
+
export function quoteThemeText(value: string): string {
|
|
61
|
+
return JSON.stringify(value).replace(
|
|
62
|
+
CONTROL_CHARACTERS,
|
|
63
|
+
(character) => `\\u${(character.codePointAt(0) ?? 0).toString(16).padStart(4, '0')}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The same escaping WITHOUT the surrounding quotes, for a field a caller already quotes — so a
|
|
69
|
+
* refused file does not print as `Skipped theme ""My Theme.json"": …`.
|
|
70
|
+
*/
|
|
71
|
+
export function escapeThemeText(value: string): string {
|
|
72
|
+
return quoteThemeText(value).slice(1, -1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Prose a surface did not write — a dependency's message, a path from the environment. Sanitized
|
|
77
|
+
* rather than quoted, because it is a sentence; `sanitizeTerminalText` removes both spellings of
|
|
78
|
+
* every sequence, and the tab/newline it keeps are flattened so one diagnostic stays one line.
|
|
79
|
+
*/
|
|
80
|
+
export function sanitizeThemeProse(message: string): string {
|
|
81
|
+
return sanitizeTerminalText(message).replaceAll('\n', ' ').replaceAll('\t', ' ').trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A path SEGMENT taken from the file. Plain keys read as themselves; anything else is quoted. */
|
|
85
|
+
function pathSegment(key: string): string {
|
|
86
|
+
return CONTROL_CHARACTER.test(key) ? quoteThemeText(key) : key;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function refuse(error: string): TThemeDocumentResult {
|
|
90
|
+
return { ok: false, error };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type TMergeResult =
|
|
94
|
+
| { readonly ok: true; readonly value: TJsonValue }
|
|
95
|
+
| { readonly ok: false; readonly error: string };
|
|
96
|
+
|
|
97
|
+
function mergeColor(override: TJsonValue, path: string): TMergeResult {
|
|
98
|
+
if (typeof override !== 'string')
|
|
99
|
+
return { ok: false, error: `${path}: expected a colour string` };
|
|
100
|
+
if (!isThemeColor(override)) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: `${path}: ${quoteThemeText(override)} is not a colour in Ink's grammar (${THEME_COLOR_GRAMMAR})`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, value: override };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mergeArray(base: readonly TJsonValue[], override: TJsonValue, path: string): TMergeResult {
|
|
110
|
+
if (!Array.isArray(override) || override.length !== base.length) {
|
|
111
|
+
return { ok: false, error: `${path}: expected exactly ${base.length} colours` };
|
|
112
|
+
}
|
|
113
|
+
const merged: TJsonValue[] = [];
|
|
114
|
+
for (const [index, item] of base.entries()) {
|
|
115
|
+
const result = mergeNode(item, override[index] ?? null, `${path}[${index}]`);
|
|
116
|
+
if (!result.ok) return result;
|
|
117
|
+
merged.push(result.value);
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, value: merged };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mergeRecord(base: IJsonRecord, override: TJsonValue, path: string): TMergeResult {
|
|
123
|
+
if (!isJsonRecord(override)) return { ok: false, error: `${path}: expected an object of tokens` };
|
|
124
|
+
const merged: IJsonRecord = { ...base };
|
|
125
|
+
for (const key of Object.keys(override)) {
|
|
126
|
+
// Own-property only: a JSON document can carry a literal `__proto__` key, and the base theme
|
|
127
|
+
// has no own property by that name, so the same check that catches a typo catches that too.
|
|
128
|
+
if (!Object.prototype.hasOwnProperty.call(base, key)) {
|
|
129
|
+
return { ok: false, error: `${path}.${pathSegment(key)} is not a theme token` };
|
|
130
|
+
}
|
|
131
|
+
const result = mergeNode(
|
|
132
|
+
base[key] ?? null,
|
|
133
|
+
override[key] ?? null,
|
|
134
|
+
`${path}.${pathSegment(key)}`,
|
|
135
|
+
);
|
|
136
|
+
if (!result.ok) return result;
|
|
137
|
+
merged[key] = result.value;
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, value: merged };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One node of the token tree, matched against the SHAPE of the base theme's node. */
|
|
143
|
+
function mergeNode(base: TJsonValue, override: TJsonValue, path: string): TMergeResult {
|
|
144
|
+
if (typeof base === 'string') return mergeColor(override, path);
|
|
145
|
+
if (Array.isArray(base)) return mergeArray(base, override, path);
|
|
146
|
+
if (isJsonRecord(base)) return mergeRecord(base, override, path);
|
|
147
|
+
return { ok: false, error: `${path} is not a theme token` };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type TThemeTokens = Pick<ITuiTheme, 'colors' | 'markdown' | 'syntax' | 'motion'>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The merged tree, read back as the token groups it was built from.
|
|
154
|
+
*
|
|
155
|
+
* A JSON round-trip rather than a type assertion, in both directions: the token interfaces have no
|
|
156
|
+
* index signature, so neither shape is assignable to the other and an assertion between them is
|
|
157
|
+
* what TypeScript rightly refuses. The conversion is sound by construction — `mergeNode` copies the
|
|
158
|
+
* BASE theme's shape and replaces only string leaves with strings the grammar accepted, so every key
|
|
159
|
+
* `ITuiTheme` declares is present and every leaf is a `TThemeColor`. An unknown key or a non-colour
|
|
160
|
+
* leaf is a refusal above this line and never reaches it.
|
|
161
|
+
*/
|
|
162
|
+
function asThemeTokens(value: TJsonValue): TThemeTokens {
|
|
163
|
+
return JSON.parse(JSON.stringify(value)) as TThemeTokens;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The four token groups, as JSON — the base for the walk above. */
|
|
167
|
+
function themeTokens(theme: ITuiTheme): IJsonRecord {
|
|
168
|
+
return JSON.parse(
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
colors: theme.colors,
|
|
171
|
+
markdown: theme.markdown,
|
|
172
|
+
syntax: theme.syntax,
|
|
173
|
+
motion: theme.motion,
|
|
174
|
+
}),
|
|
175
|
+
) as IJsonRecord;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readDocument(text: string): TMergeResult {
|
|
179
|
+
let parsed: TJsonValue;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(text) as TJsonValue;
|
|
182
|
+
} catch (cause) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: `$: ${cause instanceof Error ? sanitizeThemeProse(cause.message) : 'invalid JSON'}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (!isJsonRecord(parsed)) return { ok: false, error: '$: expected an object' };
|
|
189
|
+
for (const key of Object.keys(parsed)) {
|
|
190
|
+
if (!DOCUMENT_KEYS.has(key)) {
|
|
191
|
+
return { ok: false, error: `$.${pathSegment(key)} is not a theme token` };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { ok: true, value: parsed };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readName(document: IJsonRecord, fallback: string): TMergeResult {
|
|
198
|
+
const name = document.name;
|
|
199
|
+
// The FALLBACK is checked on the same terms. It is the id the loader minted, which is constrained
|
|
200
|
+
// where it is minted — but "the caller already made it safe" is the assumption that put an
|
|
201
|
+
// unchecked string on every list row once, and this is the function that renders the answer.
|
|
202
|
+
if (name === undefined) {
|
|
203
|
+
if (CONTROL_CHARACTER.test(fallback)) {
|
|
204
|
+
return { ok: false, error: '$.name: the minted id must not contain control characters' };
|
|
205
|
+
}
|
|
206
|
+
if (fallback.length > MAX_NAME_LENGTH) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
error: `$.name: the minted id must be at most ${MAX_NAME_LENGTH} characters`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return { ok: true, value: fallback };
|
|
213
|
+
}
|
|
214
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
215
|
+
return { ok: false, error: '$.name: expected a non-empty string' };
|
|
216
|
+
}
|
|
217
|
+
// A name is APPLIED, so unlike a diagnostic it is rendered on every `/theme list` row and every
|
|
218
|
+
// picker row for the whole session. A control character there is an escape the terminal acts on,
|
|
219
|
+
// and a newline breaks the box the row is drawn in — so the file is refused rather than the name
|
|
220
|
+
// repaired, which is this module's one policy.
|
|
221
|
+
if (CONTROL_CHARACTER.test(name)) {
|
|
222
|
+
return { ok: false, error: '$.name: must not contain control characters' };
|
|
223
|
+
}
|
|
224
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
225
|
+
return { ok: false, error: `$.name: must be at most ${MAX_NAME_LENGTH} characters` };
|
|
226
|
+
}
|
|
227
|
+
return { ok: true, value: name };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readBase(
|
|
231
|
+
document: IJsonRecord,
|
|
232
|
+
):
|
|
233
|
+
| { readonly ok: true; readonly theme: ITuiTheme }
|
|
234
|
+
| { readonly ok: false; readonly error: string } {
|
|
235
|
+
// `=== undefined`, not `??`: an explicit `null` is a value the file states, and "applied whole or
|
|
236
|
+
// refused whole" means it is refused rather than silently read as "unset".
|
|
237
|
+
const id = document.base === undefined ? DEFAULT_THEME_ID : document.base;
|
|
238
|
+
if (typeof id !== 'string') return { ok: false, error: '$.base: expected a built-in theme id' };
|
|
239
|
+
const base = BUILT_IN_THEMES.find((theme) => theme.id === id);
|
|
240
|
+
if (!base) return { ok: false, error: `$.base: ${quoteThemeText(id)} is not a built-in theme` };
|
|
241
|
+
return { ok: true, theme: base };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parse one theme file. The `id`, the `source` and the `appearance` are NOT the document's to
|
|
246
|
+
* decide: the id is minted from where the file was found so nothing can shadow a built-in, the
|
|
247
|
+
* source is where it was read from, and the appearance follows the base a theme chose to extend.
|
|
248
|
+
*/
|
|
249
|
+
export function parseThemeDocument(input: IThemeDocumentInput): TThemeDocumentResult {
|
|
250
|
+
const document = readDocument(input.text);
|
|
251
|
+
if (!document.ok) return refuse(document.error);
|
|
252
|
+
const fields = document.value as IJsonRecord;
|
|
253
|
+
|
|
254
|
+
const base = readBase(fields);
|
|
255
|
+
if (!base.ok) return refuse(base.error);
|
|
256
|
+
|
|
257
|
+
const name = readName(fields, input.id);
|
|
258
|
+
if (!name.ok) return refuse(name.error);
|
|
259
|
+
|
|
260
|
+
const overrides = fields.overrides === undefined ? {} : fields.overrides;
|
|
261
|
+
const tokens = mergeNode(themeTokens(base.theme), overrides, '$.overrides');
|
|
262
|
+
if (!tokens.ok) return refuse(tokens.error);
|
|
263
|
+
|
|
264
|
+
const { colors, markdown, syntax, motion } = asThemeTokens(tokens.value);
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
theme: {
|
|
268
|
+
id: input.id,
|
|
269
|
+
name: name.value as string,
|
|
270
|
+
appearance: base.theme.appearance,
|
|
271
|
+
source: input.source,
|
|
272
|
+
colors,
|
|
273
|
+
markdown,
|
|
274
|
+
syntax,
|
|
275
|
+
motion,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-2002 — the theme catalogue, and the port the command layer asks through.
|
|
3
|
+
*
|
|
4
|
+
* The registry is the only thing that turns an ID into a theme. Work unit 1 put the built-ins
|
|
5
|
+
* behind `listBuiltInThemes()`; work unit 3 adds user and plugin themes to the same list, and
|
|
6
|
+
* nothing downstream has to learn where a theme came from — a row carries its `source` so the
|
|
7
|
+
* picker and `/theme list` can SAY where it came from, which is a different question.
|
|
8
|
+
*
|
|
9
|
+
* An unknown id resolves to the default and reports the id it could not find. Resolving silently
|
|
10
|
+
* would make a removed plugin look like a theme that stopped working, and the id is the only thing
|
|
11
|
+
* that can name the cause.
|
|
12
|
+
*/
|
|
13
|
+
import { DEFAULT_THEME_ID, listBuiltInThemes, resolveTheme } from './built-in-themes.js';
|
|
14
|
+
|
|
15
|
+
// The registry's default source, re-exported from the module that USES it. The package's public
|
|
16
|
+
// surface takes this route rather than the theme barrel: the barrel re-exports `theme-context.tsx`,
|
|
17
|
+
// and a public re-export chain that reaches a `.tsx` is what `sdk-public-surface` refuses.
|
|
18
|
+
export { listBuiltInThemes } from './built-in-themes.js';
|
|
19
|
+
|
|
20
|
+
import type { ITuiTheme } from './theme-contracts.js';
|
|
21
|
+
import type {
|
|
22
|
+
IThemeAppearanceState,
|
|
23
|
+
IThemeCatalogueEntry,
|
|
24
|
+
IThemeCataloguePort,
|
|
25
|
+
TReducedMotionOverride,
|
|
26
|
+
} from '@robota-sdk/agent-interface-command';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A theme file that was found and refused. It is carried BESIDE the themes rather than among them,
|
|
30
|
+
* because a surface must be able to show the file without ever resolving to it — an id that can be
|
|
31
|
+
* listed and cannot be applied is the shape of the bug this avoids.
|
|
32
|
+
*/
|
|
33
|
+
export interface IThemeSkip {
|
|
34
|
+
/**
|
|
35
|
+
* The id the file WOULD have had, so a `/theme <id>` that fails has a visible reason — or, when
|
|
36
|
+
* the file's own NAME is what made an id impossible, the quoted name in its place. It is a label,
|
|
37
|
+
* not a key: two files can carry the same one.
|
|
38
|
+
*/
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly fileName: string;
|
|
41
|
+
/** The path-named diagnostic, verbatim from `parseThemeDocument`. */
|
|
42
|
+
readonly reason: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface IThemeResolution {
|
|
46
|
+
readonly theme: ITuiTheme;
|
|
47
|
+
/** The id that was asked for and not found. Absent ⇒ the request was satisfied. */
|
|
48
|
+
readonly unknownId?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface IThemeRegistry {
|
|
52
|
+
list(): readonly ITuiTheme[];
|
|
53
|
+
get(id: string): ITuiTheme | undefined;
|
|
54
|
+
/** The theme for an id, falling back to the default and NAMING what it could not find. */
|
|
55
|
+
resolve(id: string | undefined): IThemeResolution;
|
|
56
|
+
/** The theme files that were found and refused, with the reason each was refused for. */
|
|
57
|
+
skipped(): readonly IThemeSkip[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createThemeRegistry(
|
|
61
|
+
themes: readonly ITuiTheme[] = listBuiltInThemes(),
|
|
62
|
+
skipped: readonly IThemeSkip[] = [],
|
|
63
|
+
): IThemeRegistry {
|
|
64
|
+
const byId = new Map(themes.map((theme) => [theme.id, theme]));
|
|
65
|
+
return {
|
|
66
|
+
list: () => themes,
|
|
67
|
+
skipped: () => skipped,
|
|
68
|
+
get: (id) => byId.get(id),
|
|
69
|
+
resolve: (id) => {
|
|
70
|
+
if (id === undefined || id === DEFAULT_THEME_ID) {
|
|
71
|
+
return { theme: resolveTheme(byId.get(DEFAULT_THEME_ID)) };
|
|
72
|
+
}
|
|
73
|
+
const found = byId.get(id);
|
|
74
|
+
if (found) return { theme: found };
|
|
75
|
+
return { theme: resolveTheme(byId.get(DEFAULT_THEME_ID)), unknownId: id };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The line a run prints once when its persisted theme is not installed. */
|
|
81
|
+
export function formatUnknownThemeNotice(id: string): string {
|
|
82
|
+
return `Theme "${id}" is not installed — using "${DEFAULT_THEME_ID}".`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toEntry(theme: ITuiTheme): IThemeCatalogueEntry {
|
|
86
|
+
return {
|
|
87
|
+
id: theme.id,
|
|
88
|
+
name: theme.name,
|
|
89
|
+
appearance: theme.appearance,
|
|
90
|
+
source: theme.source,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface IThemeCataloguePortOptions {
|
|
95
|
+
readonly registry: IThemeRegistry;
|
|
96
|
+
/**
|
|
97
|
+
* Re-read on every call, not captured once: `/theme` reports what is PERSISTED, and the host
|
|
98
|
+
* applies a patch by writing the settings document. A snapshot would answer with the appearance
|
|
99
|
+
* the process started with, so the command would report the change it just made as not having
|
|
100
|
+
* happened.
|
|
101
|
+
*/
|
|
102
|
+
readonly readAppearance: () => IThemeAppearanceState['settings'];
|
|
103
|
+
/**
|
|
104
|
+
* The pin on reduced motion for this run, when something above the settings applied one — WHICH
|
|
105
|
+
* tier and WHAT it pinned, as one value. Two optional fields would let a caller supply the tier
|
|
106
|
+
* alone, and a default for the missing half is exactly the wrong answer this pair exists to stop:
|
|
107
|
+
* `--no-reduced-motion` is an override too and pins the opposite value.
|
|
108
|
+
*/
|
|
109
|
+
readonly reducedMotionPin?:
|
|
110
|
+
{ readonly tier: TReducedMotionOverride; readonly reducedMotion: boolean } | undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createThemeCataloguePort(options: IThemeCataloguePortOptions): IThemeCataloguePort {
|
|
114
|
+
return {
|
|
115
|
+
listThemes: () => options.registry.list().map(toEntry),
|
|
116
|
+
getTheme: (id) => {
|
|
117
|
+
const theme = options.registry.get(id);
|
|
118
|
+
return theme ? toEntry(theme) : undefined;
|
|
119
|
+
},
|
|
120
|
+
getAppearance: () => ({
|
|
121
|
+
settings: options.readAppearance(),
|
|
122
|
+
...(options.reducedMotionPin === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { reducedMotionPin: options.reducedMotionPin }),
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
}
|