@linxiraos/pi-tui 1.1.27 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -463
- package/README.md +7 -5
- package/dist/types/app-keybindings.d.ts +1 -1
- package/dist/types/apps/cleanse-board.d.ts +1 -1
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/apps/git/git-tui.d.ts +2 -2
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/apps/if-bench-board.d.ts +2 -2
- package/dist/types/apps/setup-model-picker.d.ts +1 -1
- package/dist/types/autocomplete.d.ts +5 -2
- package/dist/types/charts/chart-plan.d.ts +674 -0
- package/dist/types/charts/chart-svg.d.ts +59 -0
- package/dist/types/charts/table-data.d.ts +175 -0
- package/dist/types/chat/assistant-message.d.ts +5 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/figure-markdown.d.ts +42 -0
- package/dist/types/chat/messages.d.ts +1 -1
- package/dist/types/chat/svg-figure.d.ts +33 -0
- package/dist/types/chat/svg-source.d.ts +49 -0
- package/dist/types/chat/table-chart.d.ts +89 -0
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/chat/user-message.d.ts +15 -2
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +6 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/editor.d.ts +15 -4
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +12 -44
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/markdown.d.ts +36 -3
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/tree-view.d.ts +7 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/fuzzy.d.ts +29 -1
- package/dist/types/key-hint-format.d.ts +1 -1
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +2 -2
- package/dist/types/loop-watchdog.d.ts +8 -7
- package/dist/types/multiplexers/cmux.d.ts +16 -0
- package/dist/types/multiplexers/herdr.d.ts +26 -0
- package/dist/types/multiplexers/notify.d.ts +13 -0
- package/dist/types/multiplexers/orca.d.ts +12 -0
- package/dist/types/multiplexers/screen.d.ts +9 -0
- package/dist/types/multiplexers/tmux.d.ts +29 -0
- package/dist/types/multiplexers/types.d.ts +100 -0
- package/dist/types/multiplexers/wmux.d.ts +12 -0
- package/dist/types/multiplexers/zellij.d.ts +12 -0
- package/dist/types/native/backend.d.ts +3 -2
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +3 -4
- package/dist/types/native/overlay.d.ts +5 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
- package/dist/types/overlays/agent-hub-types.d.ts +9 -0
- package/dist/types/overlays/agent-hub.d.ts +2 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
- package/dist/types/overlays/agents-hub.d.ts +9 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -2
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/ask-dialog.d.ts +5 -2
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/jobs-panel.d.ts +1 -7
- package/dist/types/overlays/model-browser.d.ts +54 -7
- package/dist/types/overlays/model-hub.d.ts +23 -0
- package/dist/types/overlays/oauth-selector.d.ts +0 -1
- package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +6 -2
- package/dist/types/overlays/settings-defs.d.ts +1 -0
- package/dist/types/overlays/usage-dashboard.d.ts +8 -4
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer.d.ts +1 -1
- package/dist/types/prompt/custom-editor.d.ts +11 -8
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +6 -1
- package/dist/types/render/composite.d.ts +2 -0
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/setup/scenes/types.d.ts +1 -0
- package/dist/types/status-line/component.d.ts +3 -0
- package/dist/types/status-line/host.d.ts +4 -1
- package/dist/types/status-line/segments.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +14 -3
- package/dist/types/terminal-capabilities.d.ts +27 -35
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +22 -6
- package/dist/types/theme/active-symbols.d.ts +1 -1
- package/dist/types/theme/schema.d.ts +11 -2
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +7 -0
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/daemon.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +22 -3
- package/dist/types/tools/glob.d.ts +4 -0
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/output-meta.d.ts +11 -1
- package/dist/types/tools/renderer.d.ts +37 -2
- package/dist/types/tools/streaming-output.d.ts +20 -1
- package/dist/types/tools/task.d.ts +0 -4
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +31 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/package.json +13 -9
- package/src/app-keybindings.d.ts +1 -1
- package/src/app-keybindings.ts +1 -1
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/cleanse-board.ts +2 -2
- package/src/apps/cleanse-picker.ts +1 -1
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +26 -5
- package/src/apps/git/git-tui.ts +2 -2
- package/src/apps/git/help.ts +29 -20
- package/src/apps/if-bench-board.ts +2 -2
- package/src/apps/ps-top.ts +2 -2
- package/src/apps/setup-model-picker.ts +1 -1
- package/src/autocomplete.d.ts +5 -2
- package/src/autocomplete.ts +67 -43
- package/src/charts/chart-plan.d.ts +674 -0
- package/src/charts/chart-plan.ts +5081 -0
- package/src/charts/chart-svg.d.ts +59 -0
- package/src/charts/chart-svg.ts +4657 -0
- package/src/charts/table-data.d.ts +175 -0
- package/src/charts/table-data.ts +1478 -0
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.d.ts +5 -0
- package/src/chat/assistant-message.ts +219 -50
- package/src/chat/chat-transcript-builder.d.ts +2 -0
- package/src/chat/chat-transcript-builder.ts +5 -3
- package/src/chat/compaction-summary-message.d.ts +1 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/execution-shared.d.ts +6 -5
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/fence-figure.d.ts +42 -0
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/figure-markdown.d.ts +42 -0
- package/src/chat/figure-markdown.ts +174 -0
- package/src/chat/messages.d.ts +1 -1
- package/src/chat/messages.ts +1 -1
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/svg-figure.d.ts +33 -0
- package/src/chat/svg-figure.ts +399 -0
- package/src/chat/svg-source.d.ts +49 -0
- package/src/chat/svg-source.ts +188 -0
- package/src/chat/table-chart.d.ts +89 -0
- package/src/chat/table-chart.ts +294 -0
- package/src/chat/tool-execution.d.ts +2 -0
- package/src/chat/tool-execution.ts +80 -6
- package/src/chat/user-message.d.ts +15 -2
- package/src/chat/user-message.ts +55 -3
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +93 -13
- package/src/components/box.d.ts +1 -0
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.d.ts +1 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/editor.d.ts +15 -4
- package/src/components/editor.ts +294 -46
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +12 -44
- package/src/components/image.ts +47 -151
- package/src/components/key-value-list.d.ts +1 -0
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.d.ts +1 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.d.ts +1 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/markdown.d.ts +36 -3
- package/src/components/markdown.ts +433 -318
- package/src/components/scroll-view.d.ts +1 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.d.ts +1 -0
- package/src/components/section.ts +5 -0
- package/src/components/settings-list.ts +2 -0
- package/src/components/table.d.ts +1 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.d.ts +1 -0
- package/src/components/text.ts +7 -0
- package/src/components/tree-view.d.ts +7 -0
- package/src/components/tree-view.ts +38 -8
- package/src/components/truncated-text.d.ts +1 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/desktop-notify.ts +1 -1
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/key-hint-format.d.ts +1 -1
- package/src/key-hint-format.ts +1 -1
- package/src/keybinding-matchers.d.ts +3 -0
- package/src/keybinding-matchers.ts +6 -2
- package/src/keybindings.d.ts +1 -1
- package/src/keybindings.ts +1 -1
- package/src/kitty-graphics.d.ts +2 -2
- package/src/kitty-graphics.ts +4 -4
- package/src/loop-watchdog.d.ts +8 -7
- package/src/loop-watchdog.ts +18 -13
- package/src/multiplexers/cmux.d.ts +16 -0
- package/src/multiplexers/cmux.ts +29 -0
- package/src/multiplexers/herdr.d.ts +26 -0
- package/src/multiplexers/herdr.ts +64 -0
- package/src/multiplexers/notify.d.ts +13 -0
- package/src/multiplexers/notify.ts +24 -0
- package/src/multiplexers/orca.d.ts +12 -0
- package/src/multiplexers/orca.ts +16 -0
- package/src/multiplexers/screen.d.ts +9 -0
- package/src/multiplexers/screen.ts +15 -0
- package/src/multiplexers/tmux.d.ts +29 -0
- package/src/multiplexers/tmux.ts +40 -0
- package/src/multiplexers/types.d.ts +100 -0
- package/src/multiplexers/types.ts +110 -0
- package/src/multiplexers/wmux.d.ts +12 -0
- package/src/multiplexers/wmux.ts +16 -0
- package/src/multiplexers/zellij.d.ts +12 -0
- package/src/multiplexers/zellij.ts +24 -0
- package/src/native/backend.d.ts +3 -2
- package/src/native/backend.ts +174 -18
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +3 -4
- package/src/native/encode.ts +58 -31
- package/src/native/overlay.d.ts +5 -0
- package/src/native/overlay.ts +13 -0
- package/src/native/reconcile.ts +4 -0
- package/src/overlays/agent-hub-projection.d.ts +6 -5
- package/src/overlays/agent-hub-projection.ts +124 -13
- package/src/overlays/agent-hub-types.d.ts +9 -0
- package/src/overlays/agent-hub-types.ts +6 -0
- package/src/overlays/agent-hub.ts +44 -29
- package/src/overlays/agent-transcript-viewer.ts +91 -23
- package/src/overlays/agents-hub.ts +28 -8
- package/src/overlays/annotation-overlay.ts +293 -90
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/ask-dialog.ts +7 -10
- package/src/overlays/btw-history-panel.ts +12 -1
- package/src/overlays/copy-selector.ts +30 -8
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/jobs-panel.ts +28 -46
- package/src/overlays/model-browser.ts +341 -110
- package/src/overlays/model-hub.ts +300 -49
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/plan-review-overlay.ts +194 -70
- package/src/overlays/plugin-settings.ts +3 -3
- package/src/overlays/report-panel.ts +2 -2
- package/src/overlays/rewind-selector.ts +86 -17
- package/src/overlays/session-observer-registry.d.ts +2 -0
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/session-selector.ts +64 -26
- package/src/overlays/settings-defs.ts +4 -3
- package/src/overlays/settings-selector.ts +33 -8
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/overlays/stats-notice.ts +1 -1
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +100 -50
- package/src/overlays/usage-display.ts +10 -4
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer.ts +4 -15
- package/src/prompt/custom-editor.ts +54 -26
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +30 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/model-mention-autocomplete.ts +14 -14
- package/src/prompt/tips.txt +6 -5
- package/src/prompt/welcome.ts +55 -12
- package/src/prompt/word-completion.ts +38 -5
- package/src/render/composite.d.ts +2 -0
- package/src/render/composite.ts +64 -0
- package/src/render/output-pane.d.ts +1 -0
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.d.ts +1 -0
- package/src/render/render-utils.ts +11 -0
- package/src/render/tool-card.d.ts +6 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.d.ts +2 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/setup/scenes/model.ts +2 -1
- package/src/setup/scenes/types.ts +1 -0
- package/src/status-line/component.ts +292 -78
- package/src/status-line/host.ts +4 -1
- package/src/status-line/segments.ts +40 -22
- package/src/status-line/types.ts +15 -3
- package/src/terminal-capabilities.d.ts +27 -35
- package/src/terminal-capabilities.ts +75 -189
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +22 -6
- package/src/terminal.ts +139 -37
- package/src/theme/active-symbols.d.ts +1 -1
- package/src/theme/active-symbols.ts +1 -1
- package/src/theme/schema-validation.ts +1 -0
- package/src/theme/schema.d.ts +11 -2
- package/src/theme/schema.ts +14 -1
- package/src/theme/session-color.ts +1 -7
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +6 -1
- package/src/theme/theme-class.d.ts +7 -0
- package/src/theme/theme-class.ts +26 -1
- package/src/theme/theme-schema.json +5 -1
- package/src/theme/theme.d.ts +1 -1
- package/src/theme/theme.ts +8 -4
- package/src/theme/tui-adapters.d.ts +3 -0
- package/src/theme/tui-adapters.ts +68 -0
- package/src/tmux.d.ts +11 -4
- package/src/tmux.ts +47 -11
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/bash.ts +92 -42
- package/src/tools/daemon.d.ts +1 -1
- package/src/tools/daemon.ts +1 -1
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/edit.ts +142 -120
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.d.ts +22 -3
- package/src/tools/eval.ts +364 -208
- package/src/tools/fetch.ts +5 -6
- package/src/tools/glob.d.ts +4 -0
- package/src/tools/glob.ts +33 -65
- package/src/tools/grep.ts +81 -48
- package/src/tools/index.ts +2 -0
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +111 -68
- package/src/tools/native-view.d.ts +47 -0
- package/src/tools/native-view.ts +102 -1
- package/src/tools/output-meta.d.ts +11 -1
- package/src/tools/output-meta.ts +23 -3
- package/src/tools/read.ts +7 -5
- package/src/tools/renderer.d.ts +37 -2
- package/src/tools/renderer.ts +42 -2
- package/src/tools/streaming-output.d.ts +20 -1
- package/src/tools/streaming-output.ts +149 -50
- package/src/tools/task.d.ts +0 -4
- package/src/tools/task.ts +112 -59
- package/src/tools/todo.ts +119 -30
- package/src/tools/wait.ts +34 -24
- package/src/tools/web-search.ts +1 -1
- package/src/tools/write.d.ts +9 -2
- package/src/tools/write.ts +147 -95
- package/src/tools/yield.d.ts +22 -0
- package/src/tools/yield.ts +225 -0
- package/src/tui.d.ts +31 -0
- package/src/tui.ts +309 -160
- package/src/utils.d.ts +6 -0
- package/src/utils.ts +19 -0
- package/src/vim.d.ts +19 -5
- package/src/vim.ts +333 -18
- package/src/windows-input-mode.ts +8 -0
package/src/tui.ts
CHANGED
|
@@ -29,13 +29,14 @@ import { col } from "./native/describe";
|
|
|
29
29
|
import { TSP_PREFIX, type TspHello } from "./native/encode";
|
|
30
30
|
import type { DescribeContext, NativeNode, NativeScreen, NativeSurfaceProvider, NativeUiEvent } from "./native/node";
|
|
31
31
|
import { STDOUT_BACKLOG_CLEAR_BYTES, setAltScreenActive, type Terminal } from "./terminal";
|
|
32
|
+
import { classifyTerminalMultiplexerModule, terminalMultiplexerSessions } from "./terminal-multiplexer";
|
|
32
33
|
import {
|
|
33
34
|
encodeKittyDeleteAllImages,
|
|
34
35
|
encodeKittyDeleteImage,
|
|
36
|
+
encodeKittyDeletePlacement,
|
|
35
37
|
encodeKittyPlacementLine,
|
|
36
38
|
ImageProtocol,
|
|
37
39
|
isImageProtocolForced,
|
|
38
|
-
isInsideHerdr,
|
|
39
40
|
isInsideTerminalMultiplexer,
|
|
40
41
|
parseKittyDirectPlacementLine,
|
|
41
42
|
setCellDimensions,
|
|
@@ -44,16 +45,14 @@ import {
|
|
|
44
45
|
synchronizedOutputUserOverride,
|
|
45
46
|
TERMINAL,
|
|
46
47
|
} from "./terminal-capabilities";
|
|
47
|
-
import {
|
|
48
|
+
import { compositeLineAt } from "./render/composite";
|
|
48
49
|
import {
|
|
49
50
|
Ellipsis,
|
|
50
|
-
extractSegments,
|
|
51
51
|
getWidthConfigEpoch,
|
|
52
52
|
isOsc66Line,
|
|
53
53
|
normalizeTerminalOutput,
|
|
54
54
|
osc66MaxScale,
|
|
55
55
|
sliceByColumn,
|
|
56
|
-
sliceWithWidth,
|
|
57
56
|
truncateToWidth,
|
|
58
57
|
visibleWidth,
|
|
59
58
|
} from "./utils";
|
|
@@ -200,6 +199,10 @@ export interface TUIStartOptions {
|
|
|
200
199
|
* Paint without owning stdin: the terminal stays in cooked mode (kernel
|
|
201
200
|
* echo + line editing at the hardware cursor) until {@link TUI.enableInput}
|
|
202
201
|
* switches to raw input and replays the kernel-buffered keystrokes.
|
|
202
|
+
*
|
|
203
|
+
* A terminal expected to speak TSP needs raw input from the start, so it
|
|
204
|
+
* gets it; its keystrokes are held instead (TSP events and the cell-size
|
|
205
|
+
* reply still apply) until {@link TUI.releaseHeldInput} replays them.
|
|
203
206
|
*/
|
|
204
207
|
deferInput?: boolean;
|
|
205
208
|
}
|
|
@@ -318,6 +321,16 @@ export interface Component {
|
|
|
318
321
|
* Called when theme changes or when component needs to re-render from scratch.
|
|
319
322
|
*/
|
|
320
323
|
invalidate?(): void;
|
|
324
|
+
/**
|
|
325
|
+
* Optional hook to drop memoized render output (rows, parse and wrap state)
|
|
326
|
+
* that the next `render()` can rebuild from state the component keeps.
|
|
327
|
+
* Unlike {@link invalidate}, it must not rebuild anything eagerly: no
|
|
328
|
+
* renderer or extension callbacks, no image conversions, and no child
|
|
329
|
+
* replacement or disposal. The next render must return the same rows it
|
|
330
|
+
* would have returned without the release. Components without it are
|
|
331
|
+
* simply not released.
|
|
332
|
+
*/
|
|
333
|
+
releaseRenderCaches?(): void;
|
|
321
334
|
/**
|
|
322
335
|
* Optional hook to set whether this component ignores tight layout mode.
|
|
323
336
|
*/
|
|
@@ -548,6 +561,7 @@ export class Container implements Component {
|
|
|
548
561
|
clear(): void {
|
|
549
562
|
this.children = [];
|
|
550
563
|
this.#memoLines = undefined;
|
|
564
|
+
this.#memoChildLines = [];
|
|
551
565
|
}
|
|
552
566
|
|
|
553
567
|
/** Dispose every child, then detach it from this container. */
|
|
@@ -557,12 +571,23 @@ export class Container implements Component {
|
|
|
557
571
|
}
|
|
558
572
|
|
|
559
573
|
invalidate(): void {
|
|
574
|
+
// The per-child refs pin every row the children last rendered; dropping
|
|
575
|
+
// only the concatenation would keep all of them reachable.
|
|
560
576
|
this.#memoLines = undefined;
|
|
577
|
+
this.#memoChildLines = [];
|
|
561
578
|
for (const child of this.children) {
|
|
562
579
|
child.invalidate?.();
|
|
563
580
|
}
|
|
564
581
|
}
|
|
565
582
|
|
|
583
|
+
releaseRenderCaches(): void {
|
|
584
|
+
this.#memoLines = undefined;
|
|
585
|
+
this.#memoChildLines = [];
|
|
586
|
+
for (const child of this.children) {
|
|
587
|
+
child.releaseRenderCaches?.();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
566
591
|
/**
|
|
567
592
|
* Propagate teardown to children. Call when the container's children are
|
|
568
593
|
* being permanently discarded (not when they are detached for reuse — use
|
|
@@ -621,6 +646,13 @@ interface HardwareCursorState {
|
|
|
621
646
|
visible: boolean;
|
|
622
647
|
}
|
|
623
648
|
|
|
649
|
+
/** A cursor-positioned image owned by a mutable viewport, never by history. */
|
|
650
|
+
interface LiveImagePlacement {
|
|
651
|
+
imageId: number;
|
|
652
|
+
placementId: number;
|
|
653
|
+
line: string;
|
|
654
|
+
}
|
|
655
|
+
|
|
624
656
|
interface PreparedLine {
|
|
625
657
|
raw: string;
|
|
626
658
|
width: number;
|
|
@@ -960,8 +992,8 @@ export class TUI extends Container {
|
|
|
960
992
|
// settle window, leaving only Unicode placeholder cells. Hold the first image
|
|
961
993
|
// paint until that window has passed; later images render normally.
|
|
962
994
|
static readonly #GHOSTTY_INITIAL_IMAGE_DELAY_MS = 100;
|
|
963
|
-
#hardwareCursorRow = 0; //
|
|
964
|
-
#hardwareCursorState: HardwareCursorState | null = null;
|
|
995
|
+
#hardwareCursorRow = 0; // Normal-buffer cursor row, retained while the alternate buffer is active.
|
|
996
|
+
#hardwareCursorState: HardwareCursorState | null = null; // Current buffer's cursor paint/dedupe state.
|
|
965
997
|
#sixelProbePendingGraphics = false;
|
|
966
998
|
#sixelProbeBuffer = "";
|
|
967
999
|
#sixelProbeTimeout?: NodeJS.Timeout;
|
|
@@ -975,6 +1007,9 @@ export class TUI extends Container {
|
|
|
975
1007
|
// Caps how many inline images render as live graphics; older ones fall back
|
|
976
1008
|
// to text via a purge + full redraw. Cap is configured by the host app.
|
|
977
1009
|
#imageBudget = new ImageBudget(DEFAULT_MAX_INLINE_IMAGES, () => this.requestRender());
|
|
1010
|
+
#screenImagePlacements = new Map<number, LiveImagePlacement>();
|
|
1011
|
+
#altImagePlacements = new Map<number, LiveImagePlacement>();
|
|
1012
|
+
#nextImagePlacementId = 1;
|
|
978
1013
|
#ghosttyInitialImageDelayDone = false;
|
|
979
1014
|
#ghosttyInitialImageDelayTimer: RenderTimer | undefined;
|
|
980
1015
|
#ghosttyImageReadyAtMs = 0;
|
|
@@ -988,6 +1023,17 @@ export class TUI extends Container {
|
|
|
988
1023
|
#cancelPostmortemRestore?: () => void;
|
|
989
1024
|
/** True between a `deferInput` start() and enableInput(). */
|
|
990
1025
|
#inputDeferred = false;
|
|
1026
|
+
/**
|
|
1027
|
+
* Keystrokes held since a TSP `deferInput` start, replayed by
|
|
1028
|
+
* releaseHeldInput(); undefined when not holding.
|
|
1029
|
+
*/
|
|
1030
|
+
#heldInput: string[] | undefined;
|
|
1031
|
+
/**
|
|
1032
|
+
* The component focused when holding began. Only its keystrokes are held:
|
|
1033
|
+
* a dialog that takes focus meanwhile (a startup hook's select or confirm)
|
|
1034
|
+
* gets its input live.
|
|
1035
|
+
*/
|
|
1036
|
+
#heldFocus: Component | null = null;
|
|
991
1037
|
// Always-on event-loop lag probe. The high default threshold keeps it quiet;
|
|
992
1038
|
// it only logs `ui.loop-blocked` (with the current loop phase) when a frame
|
|
993
1039
|
// budget is genuinely starved. Armed in start(), disarmed in stop().
|
|
@@ -1023,6 +1069,11 @@ export class TUI extends Container {
|
|
|
1023
1069
|
#resizeInPlaceActive = false;
|
|
1024
1070
|
#resizeScrollbackMode: ResizeScrollbackMode = TUI.#initialResizeScrollbackMode();
|
|
1025
1071
|
#resizeReplaySize: string | undefined;
|
|
1072
|
+
// The resize borrow's entry (live-viewport erase, CSI ?1049h, keyboard
|
|
1073
|
+
// push), held for the first resize frame's synchronized update. Written on
|
|
1074
|
+
// its own, the terminal can present the blank alternate screen for a frame
|
|
1075
|
+
// before the resize frame lands.
|
|
1076
|
+
#pendingAltEnter = "";
|
|
1026
1077
|
// Holds an alternate-screen exit until its replacement full paint can emit it
|
|
1027
1078
|
// atomically. It must survive a deferred Ghostty image frame.
|
|
1028
1079
|
#pendingAltExit = "";
|
|
@@ -1218,6 +1269,8 @@ export class TUI extends Container {
|
|
|
1218
1269
|
|
|
1219
1270
|
#purgeInlineImages(): void {
|
|
1220
1271
|
const transmittedIds = this.#imageBudget.takeAllTransmittedIds();
|
|
1272
|
+
this.#screenImagePlacements.clear();
|
|
1273
|
+
this.#altImagePlacements.clear();
|
|
1221
1274
|
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
1222
1275
|
for (const id of transmittedIds) {
|
|
1223
1276
|
this.terminal.write(encodeKittyDeleteImage(id));
|
|
@@ -1505,6 +1558,12 @@ export class TUI extends Container {
|
|
|
1505
1558
|
// `hello` query must go out now to confirm the surface.
|
|
1506
1559
|
const nativeExpected = this.terminal.tspExpected === true;
|
|
1507
1560
|
this.#inputDeferred = options?.deferInput === true && !nativeExpected;
|
|
1561
|
+
// A restart (a startup dialog's external editor stops and restarts the
|
|
1562
|
+
// TUI) keeps an existing hold: those keys still belong to the editor.
|
|
1563
|
+
if (options?.deferInput === true && nativeExpected) {
|
|
1564
|
+
this.#heldInput = [];
|
|
1565
|
+
this.#heldFocus = this.#focusedComponent;
|
|
1566
|
+
}
|
|
1508
1567
|
this.#watchdog.start();
|
|
1509
1568
|
this.#ghosttyInitialImageDelayDone = false;
|
|
1510
1569
|
this.#ghosttyImageReadyAtMs = this.#renderScheduler.now() + TUI.#GHOSTTY_INITIAL_IMAGE_DELAY_MS;
|
|
@@ -1518,12 +1577,19 @@ export class TUI extends Container {
|
|
|
1518
1577
|
this.terminal.onPrivateModeReport?.((mode, supported, confirmed = true, status) => {
|
|
1519
1578
|
if (mode !== 2026 || !confirmed) return;
|
|
1520
1579
|
if (synchronizedOutputUserOverride() !== null) return;
|
|
1521
|
-
// Herdr's Ghostty
|
|
1522
|
-
// reports unrecognized (status 0). Other
|
|
1523
|
-
// still disable: status 4 is permanently
|
|
1524
|
-
// callback (`status` omitted) is a
|
|
1525
|
-
// custom Terminal that does not
|
|
1526
|
-
|
|
1580
|
+
// Some multiplexer VTEs (Herdr's Ghostty pane) honor DEC 2026 even when
|
|
1581
|
+
// DECRQM is unanswered or reports unrecognized (status 0). Other
|
|
1582
|
+
// confirmed unsupported reports still disable: status 4 is permanently
|
|
1583
|
+
// reset, and a three-argument callback (`status` omitted) is a
|
|
1584
|
+
// definitive unsupported from a custom Terminal that does not
|
|
1585
|
+
// distinguish DECRPM codes.
|
|
1586
|
+
if (
|
|
1587
|
+
!supported &&
|
|
1588
|
+
status === 0 &&
|
|
1589
|
+
terminalMultiplexerSessions().some(multiplexer => multiplexer.honorsSynchronizedOutput)
|
|
1590
|
+
) {
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1527
1593
|
this.#setSynchronizedOutput(supported);
|
|
1528
1594
|
});
|
|
1529
1595
|
// Icons painted before the Glyph Protocol registration landed may sit in
|
|
@@ -1533,6 +1599,13 @@ export class TUI extends Container {
|
|
|
1533
1599
|
this.invalidate();
|
|
1534
1600
|
this.requestRender(true);
|
|
1535
1601
|
});
|
|
1602
|
+
this.terminal.onSixelSupport?.(supported => {
|
|
1603
|
+
if (!supported || this.#stopped || this.#nativeLive || this.terminal.tspExpected) return;
|
|
1604
|
+
if (isImageProtocolForced()) return;
|
|
1605
|
+
// Keep consuming an already-requested XTSMGRAPHICS reply until it
|
|
1606
|
+
// arrives or times out, even though DA1 has established support.
|
|
1607
|
+
this.#enableSixelProtocol();
|
|
1608
|
+
});
|
|
1536
1609
|
this.terminal.onTspHello?.(hello => this.#onTspHello(hello));
|
|
1537
1610
|
this.terminal.start(
|
|
1538
1611
|
data => this.#handleInput(data),
|
|
@@ -1746,7 +1819,7 @@ export class TUI extends Container {
|
|
|
1746
1819
|
this.#resizeSettleTimer?.cancel();
|
|
1747
1820
|
this.#resizeSettleTimer = undefined;
|
|
1748
1821
|
if (this.#altActive || this.#resizeAltActive) {
|
|
1749
|
-
this.terminal.write(`${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
1822
|
+
this.terminal.write(`${this.#takePendingAltEnter()}${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
1750
1823
|
setAltScreenActive(false);
|
|
1751
1824
|
this.#altActive = false;
|
|
1752
1825
|
this.#resizeAltActive = false;
|
|
@@ -1835,7 +1908,7 @@ export class TUI extends Container {
|
|
|
1835
1908
|
resizeInPlaceOverride() !== false &&
|
|
1836
1909
|
this.#resizeScrollbackMode === "rebuild" &&
|
|
1837
1910
|
this.#synchronizedOutputEnabled &&
|
|
1838
|
-
|
|
1911
|
+
classifyTerminalMultiplexerModule()?.altRestoreEndsSynchronizedOutput === true
|
|
1839
1912
|
);
|
|
1840
1913
|
}
|
|
1841
1914
|
|
|
@@ -1900,7 +1973,9 @@ export class TUI extends Container {
|
|
|
1900
1973
|
this.#resizeSettleTimer?.cancel();
|
|
1901
1974
|
this.#forgetHardwareCursorState();
|
|
1902
1975
|
this.#recordHardwareCursorHidden();
|
|
1903
|
-
|
|
1976
|
+
const erase = this.#liveViewportResizeErase();
|
|
1977
|
+
if (erase !== "") {
|
|
1978
|
+
this.terminal.write(erase);
|
|
1904
1979
|
// The erase parked the hardware cursor on the viewport's top row;
|
|
1905
1980
|
// snapshot the parked offset so the settled probe anchors there.
|
|
1906
1981
|
this.#parkedViewportOffset = 0;
|
|
@@ -1935,13 +2010,14 @@ export class TUI extends Container {
|
|
|
1935
2010
|
* cursor-relative addressing would start rows late; fall back to the same
|
|
1936
2011
|
* bottom-preserving bound as resize-anchor recovery.
|
|
1937
2012
|
*
|
|
1938
|
-
* Both erase paths leave the cursor on the viewport's top row. Returns
|
|
1939
|
-
*
|
|
1940
|
-
* re-layout and blanks pulled-back
|
|
2013
|
+
* Both erase paths leave the cursor on the viewport's top row. Returns the
|
|
2014
|
+
* erase sequence, or "" when there is nothing to erase (multiplexers skip
|
|
2015
|
+
* it: an immediate erase races the pane re-layout and blanks pulled-back
|
|
2016
|
+
* committed rows).
|
|
1941
2017
|
*/
|
|
1942
|
-
#
|
|
2018
|
+
#liveViewportResizeErase(): string {
|
|
1943
2019
|
if (!this.#hasEverRendered || this.#providerWindow.length === 0 || isInsideTerminalMultiplexer()) {
|
|
1944
|
-
return
|
|
2020
|
+
return "";
|
|
1945
2021
|
}
|
|
1946
2022
|
if (this.terminal.rows < this.#previousHeight) {
|
|
1947
2023
|
const staleRows = this.#reflowedRowCount(
|
|
@@ -1951,13 +2027,11 @@ export class TUI extends Container {
|
|
|
1951
2027
|
this.terminal.columns,
|
|
1952
2028
|
);
|
|
1953
2029
|
const top = Math.max(0, Math.min(this.#providerViewportTop, this.terminal.rows - staleRows));
|
|
1954
|
-
|
|
1955
|
-
} else {
|
|
1956
|
-
const up = this.#reflowedRowCount(this.#providerWindow, 0, this.#parkedViewportOffset, this.terminal.columns);
|
|
1957
|
-
const eraseBelow = this.#eraseBelowCursorRow(this.terminal.columns, this.terminal.rows);
|
|
1958
|
-
this.terminal.write(`\x1b[?25l${up > 0 ? `\x1b[${up}A` : ""}${eraseBelow}`);
|
|
2030
|
+
return `\x1b[?25l${this.#eraseBelowRow(top, this.terminal.rows)}`;
|
|
1959
2031
|
}
|
|
1960
|
-
|
|
2032
|
+
const up = this.#reflowedRowCount(this.#providerWindow, 0, this.#parkedViewportOffset, this.terminal.columns);
|
|
2033
|
+
const eraseBelow = this.#eraseBelowCursorRow(this.terminal.columns, this.terminal.rows);
|
|
2034
|
+
return `\x1b[?25l${up > 0 ? `\x1b[${up}A` : ""}${eraseBelow}`;
|
|
1961
2035
|
}
|
|
1962
2036
|
|
|
1963
2037
|
/**
|
|
@@ -1988,8 +2062,12 @@ export class TUI extends Container {
|
|
|
1988
2062
|
this.#altPreparedRows = [];
|
|
1989
2063
|
this.#forgetHardwareCursorState();
|
|
1990
2064
|
this.#recordHardwareCursorHidden();
|
|
1991
|
-
// Blank the live region
|
|
1992
|
-
// committed rows into scrollback. The
|
|
2065
|
+
// Blank the live region as the borrow enters so a reflow-driven scroll
|
|
2066
|
+
// can only push committed rows into scrollback. The erase is computed
|
|
2067
|
+
// against this SIGWINCH's geometry but rides in the first resize frame's
|
|
2068
|
+
// synchronized update with the buffer switch (#pendingAltEnter), so
|
|
2069
|
+
// neither the blanked normal screen nor the empty alternate one is ever
|
|
2070
|
+
// presented. The pre-erase window is stashed for
|
|
1993
2071
|
// the settled CPR probe: its reflowed row count bounds the anchor to
|
|
1994
2072
|
// `height - staleRows`, so a mis-parked cursor (a single-step tmux zoom
|
|
1995
2073
|
// re-lays the pane before SIGWINCH delivery, moving the park target
|
|
@@ -1999,8 +2077,9 @@ export class TUI extends Container {
|
|
|
1999
2077
|
this.#resizeProbeWindow = this.#providerWindow;
|
|
2000
2078
|
this.#resizeProbeOffset = this.#parkedViewportOffset;
|
|
2001
2079
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2080
|
+
const erase = this.#liveViewportResizeErase();
|
|
2081
|
+
if (erase !== "") {
|
|
2082
|
+
// The erase parks the cursor on the viewport's top row, so the
|
|
2004
2083
|
// parked offset no longer applies; carrying a stale nonzero offset
|
|
2005
2084
|
// into the probe would anchor the settled repaint above the real
|
|
2006
2085
|
// viewport top and overwrite visible committed rows.
|
|
@@ -2023,7 +2102,7 @@ export class TUI extends Container {
|
|
|
2023
2102
|
}
|
|
2024
2103
|
this.#noteAltBufferToggle();
|
|
2025
2104
|
this.#imageBudget.beginAltScreenLifecycle();
|
|
2026
|
-
this
|
|
2105
|
+
this.#pendingAltEnter = `${erase}\x1b[?1049h${this.#keyboardEnhancementEnter()}`;
|
|
2027
2106
|
}
|
|
2028
2107
|
this.#resizeSettleTimer?.cancel();
|
|
2029
2108
|
this.#resizeSettleTimer = this.#renderScheduler.scheduleRender(() => {
|
|
@@ -2062,6 +2141,8 @@ export class TUI extends Container {
|
|
|
2062
2141
|
this.#prepareResizeReplay(this.terminal.columns, this.terminal.rows);
|
|
2063
2142
|
}
|
|
2064
2143
|
if (this.#clearScrollbackOnNextRender) {
|
|
2144
|
+
// An entry no resize frame carried yet rides ahead of this exit in
|
|
2145
|
+
// the rebuild frame (see #renderProviderFrame).
|
|
2065
2146
|
this.#pendingAltExit = exitSequence;
|
|
2066
2147
|
this.#resizeAltExitFused = true;
|
|
2067
2148
|
this.requestRender(true);
|
|
@@ -2069,10 +2150,22 @@ export class TUI extends Container {
|
|
|
2069
2150
|
}
|
|
2070
2151
|
}
|
|
2071
2152
|
this.#noteAltBufferToggle();
|
|
2072
|
-
this.terminal.write(exitSequence);
|
|
2153
|
+
this.terminal.write(this.#takePendingAltEnter() + exitSequence);
|
|
2073
2154
|
setAltScreenActive(false);
|
|
2074
2155
|
this.#beginResizeAnchorProbe();
|
|
2075
2156
|
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Claim the resize borrow's entry for the write that carries it. A borrow
|
|
2159
|
+
* that ends before any frame painted (output backpressure deferred them)
|
|
2160
|
+
* still owes its entry, so the exit write prepends it: one write, never a
|
|
2161
|
+
* bare buffer switch the terminal could present.
|
|
2162
|
+
*/
|
|
2163
|
+
#takePendingAltEnter(): string {
|
|
2164
|
+
const enter = this.#pendingAltEnter;
|
|
2165
|
+
this.#pendingAltEnter = "";
|
|
2166
|
+
return enter;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2076
2169
|
/**
|
|
2077
2170
|
* Recover the reflowed viewport anchor after the resize settle window ends.
|
|
2078
2171
|
* The terminal reflowed the restored normal buffer during the drag, so
|
|
@@ -2200,7 +2293,7 @@ export class TUI extends Container {
|
|
|
2200
2293
|
// an intact bottom anchor still need no replay or additional wait.
|
|
2201
2294
|
if (
|
|
2202
2295
|
this.#resizeScrollbackMode === "rebuild" &&
|
|
2203
|
-
|
|
2296
|
+
classifyTerminalMultiplexerModule()?.growsBeforeSigwinch === true &&
|
|
2204
2297
|
!this.#resizeRepaintsInPlace() &&
|
|
2205
2298
|
this.#frameProvider?.beginHistoryReplay &&
|
|
2206
2299
|
this.#resizeBurstGrew &&
|
|
@@ -2321,6 +2414,35 @@ export class TUI extends Container {
|
|
|
2321
2414
|
this.terminal.enableInput?.();
|
|
2322
2415
|
this.#querySixelSupport();
|
|
2323
2416
|
this.#queryCellSize();
|
|
2417
|
+
// The probes went out over the painted frame; a terminal that could not
|
|
2418
|
+
// parse one left its bytes on the cursor row (see Terminal.enableInput).
|
|
2419
|
+
this.requestRender(true);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
/**
|
|
2423
|
+
* Replay the keystrokes held since a TSP `deferInput` start through the
|
|
2424
|
+
* normal input path, then deliver input live. Call once the app's key
|
|
2425
|
+
* handlers are installed so a hotkey pressed during startup still fires.
|
|
2426
|
+
* Only keys typed while the start-time focus owner had focus are held. The
|
|
2427
|
+
* hold survives a stop/start until released. Idempotent; no-op when nothing
|
|
2428
|
+
* is held.
|
|
2429
|
+
*/
|
|
2430
|
+
releaseHeldInput(): void {
|
|
2431
|
+
const held = this.#heldInput;
|
|
2432
|
+
if (held === undefined) return;
|
|
2433
|
+
this.#heldInput = undefined;
|
|
2434
|
+
if (this.#stopped) return;
|
|
2435
|
+
for (const data of held) this.#handleInput(data);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
/**
|
|
2439
|
+
* Hand held-key ownership from `previous` to `next` when the app replaces
|
|
2440
|
+
* the component focused at start (a swapped-in custom editor), so keys
|
|
2441
|
+
* typed into the replacement stay queued behind the held ones instead of
|
|
2442
|
+
* overtaking them. No-op unless `previous` owns the held keys.
|
|
2443
|
+
*/
|
|
2444
|
+
replaceHeldFocus(previous: Component, next: Component): void {
|
|
2445
|
+
if (this.#heldInput !== undefined && this.#heldFocus === previous) this.#heldFocus = next;
|
|
2324
2446
|
}
|
|
2325
2447
|
|
|
2326
2448
|
addStartListener(listener: StartListener): () => void {
|
|
@@ -2346,17 +2468,19 @@ export class TUI extends Container {
|
|
|
2346
2468
|
// PI_FORCE_IMAGE_PROTOCOL choice — including its `off` kill switch — wins
|
|
2347
2469
|
// over the probe.
|
|
2348
2470
|
if (TERMINAL.imageProtocol) return;
|
|
2471
|
+
// A Tern surface (live, or about to open optimistically at start) sends
|
|
2472
|
+
// images through TSP; this also keeps held startup input free of probe
|
|
2473
|
+
// listeners.
|
|
2474
|
+
if (this.#nativeLive || this.terminal.tspExpected) return;
|
|
2349
2475
|
if (isImageProtocolForced()) return;
|
|
2350
2476
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
2351
2477
|
|
|
2352
2478
|
this.#clearSixelProbeState();
|
|
2353
2479
|
this.#sixelProbePendingGraphics = true;
|
|
2354
2480
|
this.#sixelProbeUnsubscribe = this.addInputListener(data => this.#handleSixelProbeInput(data));
|
|
2355
|
-
// XTSMGRAPHICS item 2 reports the
|
|
2356
|
-
//
|
|
2357
|
-
//
|
|
2358
|
-
// composer (#8542): those bytes never reach an input listener, so this probe
|
|
2359
|
-
// cannot read them.
|
|
2481
|
+
// XTSMGRAPHICS item 2 reports the maximum SIXEL geometry. DA1 attribute 4
|
|
2482
|
+
// arrives through onSixelSupport instead: ProcessTerminal consumes the
|
|
2483
|
+
// reply bytes so they cannot leak into application input.
|
|
2360
2484
|
this.terminal.write("\x1b[?2;1;0S");
|
|
2361
2485
|
this.#sixelProbeTimeout = setTimeout(() => {
|
|
2362
2486
|
this.#finishSixelProbe(false);
|
|
@@ -2440,7 +2564,11 @@ export class TUI extends Container {
|
|
|
2440
2564
|
|
|
2441
2565
|
#finishSixelProbe(supported: boolean): void {
|
|
2442
2566
|
this.#clearSixelProbeState();
|
|
2443
|
-
if (
|
|
2567
|
+
if (supported) this.#enableSixelProtocol();
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
#enableSixelProtocol(): void {
|
|
2571
|
+
if (TERMINAL.imageProtocol) return;
|
|
2444
2572
|
|
|
2445
2573
|
setTerminalImageProtocol(ImageProtocol.Sixel);
|
|
2446
2574
|
this.#queryCellSize();
|
|
@@ -2534,7 +2662,7 @@ export class TUI extends Container {
|
|
|
2534
2662
|
this.#cancelResizeProbe();
|
|
2535
2663
|
if (this.#resizeAltActive) {
|
|
2536
2664
|
this.#resizeAltActive = false;
|
|
2537
|
-
this.terminal.write(`${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
2665
|
+
this.terminal.write(`${this.#takePendingAltEnter()}${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
2538
2666
|
setAltScreenActive(false);
|
|
2539
2667
|
}
|
|
2540
2668
|
if (this.#altActive || this.#pendingAltExit) {
|
|
@@ -2545,7 +2673,7 @@ export class TUI extends Container {
|
|
|
2545
2673
|
// so the final OFF goes last or the shell keeps reporting.
|
|
2546
2674
|
const mouseExit = this.#mouseTracking !== "off" ? MOUSE_TRACKING_OFF : "";
|
|
2547
2675
|
const exitSequence = this.#pendingAltExit
|
|
2548
|
-
? `${this.#pendingAltExit}${mouseExit}`
|
|
2676
|
+
? `${this.#takePendingAltEnter()}${this.#pendingAltExit}${mouseExit}`
|
|
2549
2677
|
: `${mouseExit}${this.#keyboardEnhancementExit()}\x1b[?1049l`;
|
|
2550
2678
|
this.terminal.write(exitSequence);
|
|
2551
2679
|
setAltScreenActive(false);
|
|
@@ -2831,6 +2959,11 @@ export class TUI extends Container {
|
|
|
2831
2959
|
}
|
|
2832
2960
|
if (data.length === 0) return;
|
|
2833
2961
|
|
|
2962
|
+
if (this.#heldInput !== undefined && this.#focusedComponent === this.#heldFocus) {
|
|
2963
|
+
this.#holdInput(data);
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2834
2967
|
// If focused component is an overlay, verify it's still visible (visibility can change due to
|
|
2835
2968
|
// terminal resize or visible() callback). Runs before the capture preflight below, which must
|
|
2836
2969
|
// target the effective focus owner, not a hidden overlay.
|
|
@@ -2910,6 +3043,18 @@ export class TUI extends Container {
|
|
|
2910
3043
|
}
|
|
2911
3044
|
}
|
|
2912
3045
|
|
|
3046
|
+
/**
|
|
3047
|
+
* Queue a keystroke typed before the app installed its key handlers; input
|
|
3048
|
+
* listeners see it once, on replay. The cell-size reply is a terminal report,
|
|
3049
|
+
* consumed now. Ctrl+C/Ctrl+D release the queue so a stalled startup stays
|
|
3050
|
+
* interruptible.
|
|
3051
|
+
*/
|
|
3052
|
+
#holdInput(data: string): void {
|
|
3053
|
+
if (this.#consumeCellSizeResponse(data)) return;
|
|
3054
|
+
this.#heldInput!.push(data);
|
|
3055
|
+
if (matchesKey(data, "ctrl+c") || matchesKey(data, "ctrl+d")) this.releaseHeldInput();
|
|
3056
|
+
}
|
|
3057
|
+
|
|
2913
3058
|
#consumeCellSizeResponse(data: string): boolean {
|
|
2914
3059
|
// Response format: ESC [ 6 ; height ; width t
|
|
2915
3060
|
const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/);
|
|
@@ -3108,99 +3253,56 @@ export class TUI extends Container {
|
|
|
3108
3253
|
if (idx < 0 || idx >= result.length) continue;
|
|
3109
3254
|
const truncatedOverlayLine =
|
|
3110
3255
|
visibleWidth(overlayLines[i]) > width ? sliceByColumn(overlayLines[i], 0, width, true) : overlayLines[i];
|
|
3111
|
-
result[idx] =
|
|
3256
|
+
result[idx] = compositeLineAt(result[idx], truncatedOverlayLine, col, width, termWidth);
|
|
3112
3257
|
}
|
|
3113
3258
|
}
|
|
3114
3259
|
return result;
|
|
3115
3260
|
}
|
|
3116
3261
|
|
|
3117
|
-
/**
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3262
|
+
/**
|
|
3263
|
+
* Remove placements before their viewport cells are replaced. History uses
|
|
3264
|
+
* separate placement ids, so deleting a live copy cannot erase scrollback.
|
|
3265
|
+
*/
|
|
3266
|
+
#retireImagePlacements(
|
|
3267
|
+
placements: Map<number, LiveImagePlacement>,
|
|
3268
|
+
lines: readonly string[],
|
|
3269
|
+
top: number,
|
|
3270
|
+
repaint: boolean,
|
|
3124
3271
|
): string {
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
// Single pass through baseLine extracts both before and after segments
|
|
3135
|
-
const afterStart = startCol + overlayWidth;
|
|
3136
|
-
const base = extractSegments(baseLine, startCol, afterStart, totalWidth - afterStart, true);
|
|
3137
|
-
|
|
3138
|
-
// Extract overlay with width tracking (strict=true to exclude wide chars at boundary)
|
|
3139
|
-
const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true);
|
|
3140
|
-
|
|
3141
|
-
// Pad segments to target widths
|
|
3142
|
-
const beforePad = Math.max(0, startCol - base.beforeWidth);
|
|
3143
|
-
const overlayPad = Math.max(0, overlayWidth - overlay.width);
|
|
3144
|
-
const actualBeforeWidth = Math.max(startCol, base.beforeWidth);
|
|
3145
|
-
const actualOverlayWidth = Math.max(overlayWidth, overlay.width);
|
|
3146
|
-
const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
|
|
3147
|
-
const afterPad = Math.max(0, afterTarget - base.afterWidth);
|
|
3148
|
-
|
|
3149
|
-
// Compose result
|
|
3150
|
-
const r = SEGMENT_RESET;
|
|
3151
|
-
const result =
|
|
3152
|
-
base.before +
|
|
3153
|
-
" ".repeat(beforePad) +
|
|
3154
|
-
r +
|
|
3155
|
-
overlay.text +
|
|
3156
|
-
" ".repeat(overlayPad) +
|
|
3157
|
-
r +
|
|
3158
|
-
base.after +
|
|
3159
|
-
" ".repeat(afterPad);
|
|
3160
|
-
|
|
3161
|
-
// CRITICAL: Always verify and truncate to terminal width.
|
|
3162
|
-
// This is the final safeguard against width overflow which would crash the TUI.
|
|
3163
|
-
// Width tracking can drift from actual visible width due to:
|
|
3164
|
-
// - Complex ANSI/OSC sequences (hyperlinks, colors)
|
|
3165
|
-
// - Wide characters at segment boundaries
|
|
3166
|
-
// - Edge cases in segment extraction
|
|
3167
|
-
const resultWidth = visibleWidth(result);
|
|
3168
|
-
if (resultWidth <= totalWidth) {
|
|
3169
|
-
return result;
|
|
3170
|
-
}
|
|
3171
|
-
// Truncate with strict=true to ensure we don't exceed totalWidth
|
|
3172
|
-
return sliceByColumn(result, 0, totalWidth, true);
|
|
3272
|
+
let output = "";
|
|
3273
|
+
for (const [row, placement] of placements) {
|
|
3274
|
+
if (!repaint && lines[row - top] === placement.line) continue;
|
|
3275
|
+
output += encodeKittyDeletePlacement(placement.imageId, placement.placementId);
|
|
3276
|
+
placements.delete(row);
|
|
3277
|
+
}
|
|
3278
|
+
return output;
|
|
3173
3279
|
}
|
|
3174
3280
|
|
|
3175
|
-
/**
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
*/
|
|
3183
|
-
#imageLineSequence(line: string, screenRow: number, frameRow: number, committedTo: number): string {
|
|
3281
|
+
/** Clip a direct placement and record its live owner; history placements have no mutable owner. */
|
|
3282
|
+
#imageLineSequence(
|
|
3283
|
+
line: string,
|
|
3284
|
+
screenRow: number,
|
|
3285
|
+
placements: Map<number, LiveImagePlacement> | undefined,
|
|
3286
|
+
placementRow: number,
|
|
3287
|
+
): string {
|
|
3184
3288
|
if (screenRow < 0) return line;
|
|
3185
3289
|
const parsed = parseKittyDirectPlacementLine(line);
|
|
3186
3290
|
if (!parsed) return line;
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3291
|
+
const previous = placements?.get(placementRow);
|
|
3292
|
+
const placementId = previous?.placementId ?? this.#nextImagePlacementId++;
|
|
3293
|
+
const erase = previous ? encodeKittyDeletePlacement(previous.imageId, previous.placementId) : "";
|
|
3294
|
+
placements?.set(placementRow, { imageId: parsed.imageId, placementId, line });
|
|
3295
|
+
return (
|
|
3296
|
+
erase +
|
|
3297
|
+
encodeKittyPlacementLine({
|
|
3298
|
+
imageId: parsed.imageId,
|
|
3299
|
+
placementId,
|
|
3300
|
+
columns: parsed.columns,
|
|
3301
|
+
rows: parsed.rows,
|
|
3302
|
+
screenRow,
|
|
3303
|
+
imageHeightPx: this.#imageBudget.getPlacementGeometry(parsed.imageId)?.heightPx ?? 0,
|
|
3304
|
+
})
|
|
3194
3305
|
);
|
|
3195
|
-
if (!placement) return line;
|
|
3196
|
-
return encodeKittyPlacementLine({
|
|
3197
|
-
imageId: parsed.imageId,
|
|
3198
|
-
placementId: placement.placementId,
|
|
3199
|
-
columns: parsed.columns,
|
|
3200
|
-
rows: parsed.rows,
|
|
3201
|
-
screenRow,
|
|
3202
|
-
imageHeightPx: placement.heightPx,
|
|
3203
|
-
});
|
|
3204
3306
|
}
|
|
3205
3307
|
|
|
3206
3308
|
#terminalLine(line: PreparedLine): string {
|
|
@@ -3462,7 +3564,7 @@ export class TUI extends Container {
|
|
|
3462
3564
|
// resize in rebuild mode): erase native history and the viewport,
|
|
3463
3565
|
// then repaint from row zero.
|
|
3464
3566
|
const destructiveReset = this.#clearScrollbackOnNextRender;
|
|
3465
|
-
const compactReplay = destructiveReset &&
|
|
3567
|
+
const compactReplay = destructiveReset && classifyTerminalMultiplexerModule()?.expandsRepPadding === true;
|
|
3466
3568
|
if (destructiveReset) {
|
|
3467
3569
|
this.#providerViewportTop = 0;
|
|
3468
3570
|
this.#providerWindow = [];
|
|
@@ -3490,12 +3592,13 @@ export class TUI extends Container {
|
|
|
3490
3592
|
}
|
|
3491
3593
|
const newTop = Math.max(0, Math.min(startTop + historyRows.length, height - rows)) + anchorPull;
|
|
3492
3594
|
const pendingAltExit = this.#pendingAltExit;
|
|
3493
|
-
|
|
3595
|
+
// A fused resize exit whose borrow never painted still owes its entry.
|
|
3596
|
+
let buffer = this.#paintBeginSequence + (pendingAltExit ? this.#takePendingAltEnter() : "") + pendingAltExit;
|
|
3494
3597
|
const renewSync =
|
|
3495
3598
|
destructiveReset &&
|
|
3496
3599
|
this.#resizeScrollbackMode === "rebuild" &&
|
|
3497
3600
|
this.#synchronizedOutputEnabled &&
|
|
3498
|
-
|
|
3601
|
+
classifyTerminalMultiplexerModule()?.expiresSynchronizedOutput === true;
|
|
3499
3602
|
let syncBytes = Buffer.byteLength(buffer);
|
|
3500
3603
|
const append = (sequence: string): void => {
|
|
3501
3604
|
buffer += sequence;
|
|
@@ -3516,7 +3619,7 @@ export class TUI extends Container {
|
|
|
3516
3619
|
// reset forgot are named explicitly here — their tracking is gone, so
|
|
3517
3620
|
// nothing downstream could find them again.
|
|
3518
3621
|
for (const id of this.#imageBudget.takeResetPurgeIds()) buffer += encodeKittyDeleteImage(id);
|
|
3519
|
-
this.#
|
|
3622
|
+
this.#screenImagePlacements.clear();
|
|
3520
3623
|
}
|
|
3521
3624
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
3522
3625
|
for (const id of this.#imageBudget.takePurgeIds()) buffer += encodeKittyDeleteImage(id);
|
|
@@ -3539,7 +3642,6 @@ export class TUI extends Container {
|
|
|
3539
3642
|
// image the terminal no longer had and every inline image vanished
|
|
3540
3643
|
// after a settled width resize.
|
|
3541
3644
|
if (destructiveReset) buffer += `${LINE_TERMINATOR}\x1b[H\x1b[2J\x1b[3J`;
|
|
3542
|
-
for (const sequence of this.#imageBudget.takeTransmits()) append(sequence);
|
|
3543
3645
|
const diffable =
|
|
3544
3646
|
anchorPull === 0 &&
|
|
3545
3647
|
geometryStable &&
|
|
@@ -3548,6 +3650,8 @@ export class TUI extends Container {
|
|
|
3548
3650
|
!this.#forceViewportRepaintOnNextRender &&
|
|
3549
3651
|
!destructiveReset &&
|
|
3550
3652
|
this.#providerWindow.length > 0;
|
|
3653
|
+
buffer += this.#retireImagePlacements(this.#screenImagePlacements, prepared.lines, newTop, !diffable);
|
|
3654
|
+
for (const sequence of this.#imageBudget.takeTransmits()) append(sequence);
|
|
3551
3655
|
if (diffable) {
|
|
3552
3656
|
for (let index = 0; index < rows; index++) {
|
|
3553
3657
|
if (
|
|
@@ -3566,9 +3670,8 @@ export class TUI extends Container {
|
|
|
3566
3670
|
current,
|
|
3567
3671
|
width,
|
|
3568
3672
|
newTop + index,
|
|
3569
|
-
-1,
|
|
3570
|
-
-1,
|
|
3571
3673
|
this.#osc66SpacerGlyphWidth(prepared.lines, index),
|
|
3674
|
+
{ imagePlacements: this.#screenImagePlacements },
|
|
3572
3675
|
)}`;
|
|
3573
3676
|
}
|
|
3574
3677
|
if (this.#providerWindow.length > rows && newTop + rows < height) {
|
|
@@ -3603,8 +3706,6 @@ export class TUI extends Container {
|
|
|
3603
3706
|
preparedHistory.rows[index]!,
|
|
3604
3707
|
width,
|
|
3605
3708
|
Math.min(screenRow, height - 1),
|
|
3606
|
-
-1,
|
|
3607
|
-
-1,
|
|
3608
3709
|
this.#osc66SpacerGlyphWidth(preparedHistory.lines, index),
|
|
3609
3710
|
{ blankRow: compactReplay },
|
|
3610
3711
|
),
|
|
@@ -3618,10 +3719,12 @@ export class TUI extends Container {
|
|
|
3618
3719
|
prepared.rows[index]!,
|
|
3619
3720
|
width,
|
|
3620
3721
|
Math.min(screenRow, height - 1),
|
|
3621
|
-
-1,
|
|
3622
|
-
-1,
|
|
3623
3722
|
this.#osc66SpacerGlyphWidth(prepared.lines, index),
|
|
3624
|
-
{
|
|
3723
|
+
{
|
|
3724
|
+
blankRow: compactReplay,
|
|
3725
|
+
imagePlacements: index < replayViewportRows ? undefined : this.#screenImagePlacements,
|
|
3726
|
+
imageRow: newTop + index,
|
|
3727
|
+
},
|
|
3625
3728
|
),
|
|
3626
3729
|
);
|
|
3627
3730
|
screenRow++;
|
|
@@ -4257,10 +4360,8 @@ export class TUI extends Container {
|
|
|
4257
4360
|
line: PreparedLine,
|
|
4258
4361
|
width: number,
|
|
4259
4362
|
screenRow = -1,
|
|
4260
|
-
frameRow = -1,
|
|
4261
|
-
committedTo = -1,
|
|
4262
4363
|
spacerGlyphWidth = -1,
|
|
4263
|
-
options?: { blankRow?: boolean },
|
|
4364
|
+
options?: { blankRow?: boolean; imagePlacements?: Map<number, LiveImagePlacement>; imageRow?: number },
|
|
4264
4365
|
): string {
|
|
4265
4366
|
// End every rewrite at column zero. ConPTY can materialize a pending
|
|
4266
4367
|
// wrap before a following cursor-addressing sequence even while DECAWM is
|
|
@@ -4277,7 +4378,9 @@ export class TUI extends Container {
|
|
|
4277
4378
|
if (spacerGlyphWidth >= 0) {
|
|
4278
4379
|
rewrite = spacerGlyphWidth >= width ? "" : `${SEGMENT_RESET}\x1b[${spacerGlyphWidth}C${ERASE_TO_END_OF_LINE}`;
|
|
4279
4380
|
} else if (line.isImage) {
|
|
4280
|
-
rewrite =
|
|
4381
|
+
rewrite =
|
|
4382
|
+
ERASE_LINE +
|
|
4383
|
+
this.#imageLineSequence(line.line, screenRow, options?.imagePlacements, options?.imageRow ?? screenRow);
|
|
4281
4384
|
} else {
|
|
4282
4385
|
const terminalLine = this.#terminalLine(line);
|
|
4283
4386
|
if (options?.blankRow) {
|
|
@@ -4352,9 +4455,9 @@ export class TUI extends Container {
|
|
|
4352
4455
|
|
|
4353
4456
|
/**
|
|
4354
4457
|
* Compose and paint a single fullscreen overlay frame on the alt buffer.
|
|
4355
|
-
* Cursor markers are stripped
|
|
4356
|
-
*
|
|
4357
|
-
*
|
|
4458
|
+
* Cursor markers are stripped from the frame, but only a focused cursor-mode
|
|
4459
|
+
* component owned by the fullscreen overlay may position the hardware cursor.
|
|
4460
|
+
* The normal transcript is never composited into the alternate buffer.
|
|
4358
4461
|
*/
|
|
4359
4462
|
#renderAltFrame(width: number, height: number): void {
|
|
4360
4463
|
// oxlint-disable-next-line unicorn/no-new-array -- alt-frame length preallocation
|
|
@@ -4364,8 +4467,18 @@ export class TUI extends Container {
|
|
|
4364
4467
|
this.#imageBudget.beginPass(false, true);
|
|
4365
4468
|
lines = this.#compositeOverlaysIntoWindow(base, width, height);
|
|
4366
4469
|
} while (this.#imageBudget.endPass());
|
|
4367
|
-
const
|
|
4368
|
-
this.#
|
|
4470
|
+
const markers: { row: number; col: number }[] = [];
|
|
4471
|
+
const prepared = this.#prepareLinesArray(lines, width, this.#altPreparedRows, height, markers);
|
|
4472
|
+
const topOverlay = this.#getTopmostVisibleOverlay();
|
|
4473
|
+
const focused = this.#focusedComponent;
|
|
4474
|
+
const acceptsTerminalCursor =
|
|
4475
|
+
topOverlay !== undefined &&
|
|
4476
|
+
focused !== null &&
|
|
4477
|
+
isFocusable(focused) &&
|
|
4478
|
+
focused.focused &&
|
|
4479
|
+
typeof focused.setUseTerminalCursor === "function" &&
|
|
4480
|
+
isOverlayFocusTarget(topOverlay.component, focused);
|
|
4481
|
+
this.#emitAltFrame(prepared, width, height, true, acceptsTerminalCursor ? (markers[0] ?? null) : null);
|
|
4369
4482
|
}
|
|
4370
4483
|
|
|
4371
4484
|
/**
|
|
@@ -4373,9 +4486,15 @@ export class TUI extends Container {
|
|
|
4373
4486
|
* previous frame, or every row when the height changed, a repaint is forced,
|
|
4374
4487
|
* or a changed frame holds OSC 66 text before or after. Emits only
|
|
4375
4488
|
* sync-output brackets, cursor moves, and per-row rewrites — never ED3 or
|
|
4376
|
-
* any native-scrollback byte.
|
|
4489
|
+
* any native-scrollback byte.
|
|
4377
4490
|
*/
|
|
4378
|
-
#emitAltFrame(
|
|
4491
|
+
#emitAltFrame(
|
|
4492
|
+
prepared: PreparedLines,
|
|
4493
|
+
width: number,
|
|
4494
|
+
height: number,
|
|
4495
|
+
notifyPaint: boolean,
|
|
4496
|
+
cursorPosition: { row: number; col: number } | null = null,
|
|
4497
|
+
): void {
|
|
4379
4498
|
// The pass that composed this frame ran with `altScreen`, so the normal
|
|
4380
4499
|
// screen's own placements behind it are not treated as retired.
|
|
4381
4500
|
this.#imageBudget.limitResidentImages();
|
|
@@ -4384,17 +4503,14 @@ export class TUI extends Container {
|
|
|
4384
4503
|
// frame resolve against loaded data. The normal-screen path flushes these
|
|
4385
4504
|
// ahead of its paint; without this, an image first shown inside a
|
|
4386
4505
|
// fullscreen overlay (e.g. the settings shape preview) would render as
|
|
4387
|
-
// blank placeholder cells until the overlay closed.
|
|
4506
|
+
// blank placeholder cells until the overlay closed. A pending resize-borrow
|
|
4507
|
+
// entry leads: the image commands address the alternate buffer's store.
|
|
4508
|
+
let prelude = this.#takePendingAltEnter();
|
|
4388
4509
|
const purgeIds = this.#imageBudget.takePurgeIds();
|
|
4389
4510
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
4390
|
-
for (const id of purgeIds)
|
|
4391
|
-
}
|
|
4392
|
-
const imageTransmits = this.#imageBudget.takeTransmits();
|
|
4393
|
-
if (imageTransmits.length > 0) {
|
|
4394
|
-
let transmitBuffer = "";
|
|
4395
|
-
for (const seq of imageTransmits) transmitBuffer += seq;
|
|
4396
|
-
this.terminal.write(transmitBuffer);
|
|
4511
|
+
for (const id of purgeIds) prelude += encodeKittyDeleteImage(id);
|
|
4397
4512
|
}
|
|
4513
|
+
for (const seq of this.#imageBudget.takeTransmits()) prelude += seq;
|
|
4398
4514
|
// A forced repaint (resetDisplay, requestRender(true)) rewrites every row
|
|
4399
4515
|
// even when the cached frame is byte-identical: the redraw gesture must
|
|
4400
4516
|
// repair a corrupted modal. So does a changed frame with OSC 66 text in
|
|
@@ -4411,6 +4527,7 @@ export class TUI extends Container {
|
|
|
4411
4527
|
prepared.rows.some((_row, r) =>
|
|
4412
4528
|
this.#rowNeedsRewrite(this.#altPreviousLines, this.#altPreparedRows, prepared.lines, prepared.rows, r),
|
|
4413
4529
|
));
|
|
4530
|
+
prelude += this.#retireImagePlacements(this.#altImagePlacements, prepared.lines, 0, full);
|
|
4414
4531
|
let rowsBuffer = "";
|
|
4415
4532
|
for (let r = 0; r < height; r++) {
|
|
4416
4533
|
if (full) {
|
|
@@ -4426,17 +4543,44 @@ export class TUI extends Container {
|
|
|
4426
4543
|
prepared.rows[r]!,
|
|
4427
4544
|
width,
|
|
4428
4545
|
r,
|
|
4429
|
-
-1,
|
|
4430
|
-
-1,
|
|
4431
4546
|
this.#osc66SpacerGlyphWidth(prepared.lines, r),
|
|
4547
|
+
{ imagePlacements: this.#altImagePlacements },
|
|
4432
4548
|
);
|
|
4433
4549
|
}
|
|
4434
4550
|
this.#altPreviousLines = prepared.lines;
|
|
4435
4551
|
this.#altPreparedRows = prepared.rows;
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4552
|
+
const target = this.#targetHardwareCursorState(cursorPosition, height);
|
|
4553
|
+
const previousCursor = this.#hardwareCursorState;
|
|
4554
|
+
const cursorChanged =
|
|
4555
|
+
target === null
|
|
4556
|
+
? previousCursor?.visible === true
|
|
4557
|
+
: previousCursor === null ||
|
|
4558
|
+
previousCursor.row !== target.row ||
|
|
4559
|
+
previousCursor.col !== target.col ||
|
|
4560
|
+
previousCursor.visible !== target.visible;
|
|
4561
|
+
const placeCursor = target !== null && (rowsBuffer !== "" || cursorChanged);
|
|
4562
|
+
const hideCursor = target === null && previousCursor?.visible === true;
|
|
4563
|
+
if (rowsBuffer === "" && !placeCursor && !hideCursor) {
|
|
4564
|
+
if (prelude !== "") this.terminal.write(prelude);
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
let cursorBuffer = "";
|
|
4569
|
+
if (placeCursor && target !== null) {
|
|
4570
|
+
cursorBuffer = `\x1b[${target.row + 1};${target.col + 1}H${target.visible ? "\x1b[?25h" : "\x1b[?25l"}`;
|
|
4571
|
+
} else if (hideCursor) {
|
|
4572
|
+
cursorBuffer = "\x1b[?25l";
|
|
4573
|
+
}
|
|
4574
|
+
this.terminal.write(
|
|
4575
|
+
`${this.#paintBeginSequence}${prelude}${full ? "\x1b[H" : ""}${rowsBuffer}${cursorBuffer}${this.#paintEndSequence}`,
|
|
4576
|
+
);
|
|
4577
|
+
this.#debugPaint = {
|
|
4578
|
+
lines: prepared.lines,
|
|
4579
|
+
windowTop: 0,
|
|
4580
|
+
altScreen: true,
|
|
4581
|
+
...(target === null ? {} : { cursor: { x: target.col, y: target.row, visible: target.visible } }),
|
|
4582
|
+
};
|
|
4583
|
+
if (notifyPaint && rowsBuffer !== "") {
|
|
4440
4584
|
this.#notifyPaint({
|
|
4441
4585
|
history: [],
|
|
4442
4586
|
viewport: prepared.lines,
|
|
@@ -4446,6 +4590,11 @@ export class TUI extends Container {
|
|
|
4446
4590
|
rows: height,
|
|
4447
4591
|
});
|
|
4448
4592
|
}
|
|
4449
|
-
if (full) this.#fullRedrawCount += 1;
|
|
4593
|
+
if (full && rowsBuffer !== "") this.#fullRedrawCount += 1;
|
|
4594
|
+
// DEC 1049 restores the normal-buffer cursor on exit. Track this buffer's
|
|
4595
|
+
// visibility and marker position without replacing that saved normal row,
|
|
4596
|
+
// which stop() and a late native-surface handshake use after the restore.
|
|
4597
|
+
if (target) this.#hardwareCursorState = target;
|
|
4598
|
+
else this.#recordHardwareCursorHidden();
|
|
4450
4599
|
}
|
|
4451
4600
|
}
|