@linxiraos/pi-tui 1.1.26 → 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 +51 -491
- package/README.md +13 -7
- package/dist/types/active-terminal.d.ts +30 -0
- package/dist/types/app-keybindings.d.ts +7 -2
- 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 +5 -4
- 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 +11 -1
- package/dist/types/chat/chat-transcript-builder.d.ts +3 -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 +17 -4
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/status-notice.d.ts +15 -3
- 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 +23 -5
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +26 -44
- package/dist/types/components/input.d.ts +2 -1
- 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/loader.d.ts +5 -6
- package/dist/types/components/markdown.d.ts +47 -5
- 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 +9 -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/apply.d.ts +4 -3
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +8 -6
- package/dist/types/native/node.d.ts +36 -5
- package/dist/types/native/overlay.d.ts +8 -3
- package/dist/types/native/settle.d.ts +4 -3
- 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 +3 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +12 -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/btw-history-panel.d.ts +13 -1
- package/dist/types/overlays/copy-selector.d.ts +1 -0
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/hub-frame.d.ts +4 -1
- 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 +37 -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/report-panel.d.ts +68 -0
- package/dist/types/overlays/rewind-selector.d.ts +1 -0
- 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/thinking-selector.d.ts +2 -2
- package/dist/types/overlays/usage-dashboard.d.ts +12 -8
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer-cache.d.ts +0 -16
- package/dist/types/prompt/composer.d.ts +18 -3
- package/dist/types/prompt/custom-editor.d.ts +25 -13
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +5 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +7 -2
- 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/space-hold.d.ts +40 -25
- package/dist/types/status-line/component.d.ts +3 -0
- package/dist/types/status-line/context-usage.d.ts +7 -5
- 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 +41 -36
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +23 -16
- 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/thinking.d.ts +1 -1
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/bash.d.ts +7 -2
- 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 +2 -3
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +36 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/dist/types/windows-input-mode.d.ts +33 -1
- package/package.json +13 -9
- package/src/active-terminal.d.ts +30 -0
- package/src/active-terminal.ts +80 -0
- package/src/app-keybindings.d.ts +7 -2
- package/src/app-keybindings.ts +7 -2
- 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 +96 -23
- package/src/apps/git/git-tui.ts +23 -16
- package/src/apps/git/help.ts +29 -20
- package/src/apps/git/sidebar.ts +8 -3
- 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/bracketed-paste.ts +10 -3
- 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 +11 -1
- package/src/chat/assistant-message.ts +245 -54
- package/src/chat/chat-transcript-builder.d.ts +3 -0
- package/src/chat/chat-transcript-builder.ts +9 -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 +86 -7
- package/src/chat/user-message.d.ts +17 -4
- package/src/chat/user-message.ts +71 -8
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/status-notice.ts +25 -7
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +162 -19
- 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 +23 -5
- package/src/components/editor.ts +316 -47
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +26 -44
- package/src/components/image.ts +115 -152
- package/src/components/input.d.ts +2 -1
- package/src/components/input.ts +17 -7
- 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/loader.d.ts +5 -6
- package/src/components/loader.ts +10 -17
- package/src/components/markdown.d.ts +47 -5
- package/src/components/markdown.ts +786 -303
- 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 +3 -2
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/hotkeys-markdown.ts +9 -2
- package/src/key-hint-format.d.ts +9 -1
- package/src/key-hint-format.ts +26 -11
- 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 +11 -5
- 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/apply.d.ts +4 -3
- package/src/native/apply.ts +4 -3
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +199 -22
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +8 -6
- package/src/native/encode.ts +76 -34
- package/src/native/node.d.ts +36 -5
- package/src/native/node.ts +31 -5
- package/src/native/overlay.d.ts +8 -3
- package/src/native/overlay.ts +17 -4
- package/src/native/reconcile.ts +16 -4
- package/src/native/settle.d.ts +4 -3
- package/src/native/settle.ts +4 -3
- 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 +48 -29
- package/src/overlays/agent-transcript-viewer.ts +93 -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 +10 -17
- package/src/overlays/btw-history-panel.ts +136 -39
- package/src/overlays/copy-selector.d.ts +1 -0
- package/src/overlays/copy-selector.ts +32 -8
- package/src/overlays/extensions/extension-dashboard.ts +4 -4
- 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/hub-frame.ts +19 -1
- package/src/overlays/jobs-panel.ts +33 -52
- package/src/overlays/model-browser.ts +367 -122
- package/src/overlays/model-hub.ts +435 -59
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/pause-screen.ts +2 -2
- package/src/overlays/plan-review-overlay.ts +208 -70
- package/src/overlays/plugin-settings.ts +3 -3
- package/src/overlays/report-panel.ts +191 -0
- package/src/overlays/rewind-selector.ts +88 -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 +5 -4
- 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/thinking-selector.ts +11 -10
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +117 -58
- package/src/overlays/usage-display.ts +10 -4
- package/src/overlays/usage-row.ts +22 -8
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer-cache.ts +7 -86
- package/src/prompt/composer.ts +37 -26
- package/src/prompt/custom-editor.ts +282 -105
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +32 -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/queued-messages.ts +6 -4
- package/src/prompt/tips.txt +13 -9
- package/src/prompt/welcome.ts +64 -46
- 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 +16 -1
- 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/space-hold.d.ts +40 -25
- package/src/space-hold.ts +156 -52
- package/src/status-line/component.ts +292 -78
- package/src/status-line/context-usage.ts +21 -46
- 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 +41 -36
- package/src/terminal-capabilities.ts +137 -213
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +23 -16
- package/src/terminal.ts +203 -99
- 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 +11 -4
- 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/thinking.d.ts +1 -1
- package/src/thinking.ts +1 -1
- 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.d.ts +7 -2
- package/src/tools/bash.ts +116 -47
- 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 +154 -50
- package/src/tools/task.d.ts +2 -3
- package/src/tools/task.ts +115 -62
- 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 +36 -0
- package/src/tui.ts +666 -217
- 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.d.ts +33 -1
- package/src/windows-input-mode.ts +212 -6
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,15 +45,14 @@ import {
|
|
|
44
45
|
synchronizedOutputUserOverride,
|
|
45
46
|
TERMINAL,
|
|
46
47
|
} from "./terminal-capabilities";
|
|
48
|
+
import { compositeLineAt } from "./render/composite";
|
|
47
49
|
import {
|
|
48
50
|
Ellipsis,
|
|
49
|
-
extractSegments,
|
|
50
51
|
getWidthConfigEpoch,
|
|
51
52
|
isOsc66Line,
|
|
52
53
|
normalizeTerminalOutput,
|
|
53
54
|
osc66MaxScale,
|
|
54
55
|
sliceByColumn,
|
|
55
|
-
sliceWithWidth,
|
|
56
56
|
truncateToWidth,
|
|
57
57
|
visibleWidth,
|
|
58
58
|
} from "./utils";
|
|
@@ -88,6 +88,9 @@ const MIN_MAIN_AREA_COLUMNS = 64;
|
|
|
88
88
|
const HIDE_CURSOR = "\x1b[?25l";
|
|
89
89
|
const SYNC_OUTPUT_BEGIN = "\x1b[?2026h";
|
|
90
90
|
const SYNC_OUTPUT_END = "\x1b[?2026l";
|
|
91
|
+
// tmux expires synchronized output after one second. Renew during large
|
|
92
|
+
// replays as the output queue drains, at complete sequence/row boundaries.
|
|
93
|
+
const SYNC_OUTPUT_RENEW_BYTES = 16 * 1024;
|
|
91
94
|
const DISABLE_AUTOWRAP = "\x1b[?7l";
|
|
92
95
|
const ENABLE_AUTOWRAP = "\x1b[?7h";
|
|
93
96
|
const PAINT_BEGIN = `${HIDE_CURSOR}${SYNC_OUTPUT_BEGIN}${DISABLE_AUTOWRAP}`;
|
|
@@ -196,6 +199,10 @@ export interface TUIStartOptions {
|
|
|
196
199
|
* Paint without owning stdin: the terminal stays in cooked mode (kernel
|
|
197
200
|
* echo + line editing at the hardware cursor) until {@link TUI.enableInput}
|
|
198
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.
|
|
199
206
|
*/
|
|
200
207
|
deferInput?: boolean;
|
|
201
208
|
}
|
|
@@ -292,6 +299,12 @@ export interface Component {
|
|
|
292
299
|
/** User actions on nodes this component described (toggle, select, activate, custom). */
|
|
293
300
|
handleNativeEvent?(event: NativeUiEvent): void;
|
|
294
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Pure preflight for focused input that must precede TUI-wide input listeners
|
|
304
|
+
* and debug shortcuts. A true result routes the event to this component first.
|
|
305
|
+
*/
|
|
306
|
+
capturesInput?(data: string): boolean;
|
|
307
|
+
|
|
295
308
|
/**
|
|
296
309
|
* Optional handler for keyboard input when component has focus
|
|
297
310
|
*/
|
|
@@ -308,6 +321,16 @@ export interface Component {
|
|
|
308
321
|
* Called when theme changes or when component needs to re-render from scratch.
|
|
309
322
|
*/
|
|
310
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;
|
|
311
334
|
/**
|
|
312
335
|
* Optional hook to set whether this component ignores tight layout mode.
|
|
313
336
|
*/
|
|
@@ -538,6 +561,7 @@ export class Container implements Component {
|
|
|
538
561
|
clear(): void {
|
|
539
562
|
this.children = [];
|
|
540
563
|
this.#memoLines = undefined;
|
|
564
|
+
this.#memoChildLines = [];
|
|
541
565
|
}
|
|
542
566
|
|
|
543
567
|
/** Dispose every child, then detach it from this container. */
|
|
@@ -547,12 +571,23 @@ export class Container implements Component {
|
|
|
547
571
|
}
|
|
548
572
|
|
|
549
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.
|
|
550
576
|
this.#memoLines = undefined;
|
|
577
|
+
this.#memoChildLines = [];
|
|
551
578
|
for (const child of this.children) {
|
|
552
579
|
child.invalidate?.();
|
|
553
580
|
}
|
|
554
581
|
}
|
|
555
582
|
|
|
583
|
+
releaseRenderCaches(): void {
|
|
584
|
+
this.#memoLines = undefined;
|
|
585
|
+
this.#memoChildLines = [];
|
|
586
|
+
for (const child of this.children) {
|
|
587
|
+
child.releaseRenderCaches?.();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
556
591
|
/**
|
|
557
592
|
* Propagate teardown to children. Call when the container's children are
|
|
558
593
|
* being permanently discarded (not when they are detached for reuse — use
|
|
@@ -611,6 +646,13 @@ interface HardwareCursorState {
|
|
|
611
646
|
visible: boolean;
|
|
612
647
|
}
|
|
613
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
|
+
|
|
614
656
|
interface PreparedLine {
|
|
615
657
|
raw: string;
|
|
616
658
|
width: number;
|
|
@@ -817,6 +859,16 @@ export class TUI extends Container {
|
|
|
817
859
|
// shrink-then-regrow that never exceeds the pre-burst height (see the
|
|
818
860
|
// CPR-timeout fallback in #resolveResizeAnchor).
|
|
819
861
|
#resizeBurstPull = 0;
|
|
862
|
+
// Whether any step of the burst left the committed width / geometry. The
|
|
863
|
+
// terminal reflowed the normal buffer at every intermediate step, so a drag
|
|
864
|
+
// that returns to its starting size still shredded retained history and
|
|
865
|
+
// pushed unerased live rows into it; the settled refresh must key on the
|
|
866
|
+
// whole burst, not on the net change (see #prepareResizeReplay).
|
|
867
|
+
#resizeBurstWidthChanged = false;
|
|
868
|
+
#resizeBurstResized = false;
|
|
869
|
+
// A shrink can discard live rows below the cursor and push others into
|
|
870
|
+
// scrollback. Rebuild must repair that even when the burst ends taller.
|
|
871
|
+
#resizeBurstShrank = false;
|
|
820
872
|
// Geometry epoch: bumped on every resize transaction entry, so each CSI 6n
|
|
821
873
|
// request records the geometry it was parked under.
|
|
822
874
|
#geometryEpoch = 0;
|
|
@@ -846,6 +898,12 @@ export class TUI extends Container {
|
|
|
846
898
|
#preparedLineMemo = new Map<string, PreparedLine>();
|
|
847
899
|
#preparedLineMemoSpare = new Map<string, PreparedLine>();
|
|
848
900
|
#previousFrameLength = 0;
|
|
901
|
+
// Rows a transiently grown frame scrolled off the top without any history
|
|
902
|
+
// transaction committing them (see #emitPlanFrame). The shrink that follows
|
|
903
|
+
// the transient chrome row out pulls exactly these rows back, restoring the
|
|
904
|
+
// pre-growth screen; any append, replay, reset, resize, or overlay frame
|
|
905
|
+
// zeroes the count — those scrolls are committed or superseded.
|
|
906
|
+
#transientScrollPushed = 0;
|
|
849
907
|
#previousWidth = 0;
|
|
850
908
|
#previousHeight = 0;
|
|
851
909
|
// Physical width of the last provider frame. Scrollback reflow follows the
|
|
@@ -915,6 +973,16 @@ export class TUI extends Container {
|
|
|
915
973
|
static readonly #RESIZE_VIEWPORT_SETTLE_MS = 120;
|
|
916
974
|
/** Longest wait for a CPR reply before the settled repaint falls back. */
|
|
917
975
|
static readonly #RESIZE_PROBE_TIMEOUT_MS = 200;
|
|
976
|
+
/**
|
|
977
|
+
* Smallest settled tmux replay that is preceded by a `Rebuilding…` notice.
|
|
978
|
+
* The notice can only be published ahead of the replay (tmux holds the
|
|
979
|
+
* pane's synchronized update until the replay ends), so it stays up for
|
|
980
|
+
* exactly as long as tmux takes to ingest the replay. tmux next-3.9 on
|
|
981
|
+
* Apple silicon ingests a real transcript replay at ~14 MiB/s, so this is
|
|
982
|
+
* about half a second there. A smaller replay finishes before the notice
|
|
983
|
+
* can be read, and showing it would only flash a status row.
|
|
984
|
+
*/
|
|
985
|
+
static readonly #RESIZE_REBUILD_NOTICE_MIN_BYTES = 7 * 1024 * 1024;
|
|
918
986
|
#inputRenderGraceUntilMs = 0;
|
|
919
987
|
// A scale-`s` OSC 66 heading reserves `s - 1` rows, and the protocol
|
|
920
988
|
// caps `s` at 7. This bounds spacer lookups and supplies enough context
|
|
@@ -924,8 +992,8 @@ export class TUI extends Container {
|
|
|
924
992
|
// settle window, leaving only Unicode placeholder cells. Hold the first image
|
|
925
993
|
// paint until that window has passed; later images render normally.
|
|
926
994
|
static readonly #GHOSTTY_INITIAL_IMAGE_DELAY_MS = 100;
|
|
927
|
-
#hardwareCursorRow = 0; //
|
|
928
|
-
#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.
|
|
929
997
|
#sixelProbePendingGraphics = false;
|
|
930
998
|
#sixelProbeBuffer = "";
|
|
931
999
|
#sixelProbeTimeout?: NodeJS.Timeout;
|
|
@@ -939,6 +1007,9 @@ export class TUI extends Container {
|
|
|
939
1007
|
// Caps how many inline images render as live graphics; older ones fall back
|
|
940
1008
|
// to text via a purge + full redraw. Cap is configured by the host app.
|
|
941
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;
|
|
942
1013
|
#ghosttyInitialImageDelayDone = false;
|
|
943
1014
|
#ghosttyInitialImageDelayTimer: RenderTimer | undefined;
|
|
944
1015
|
#ghosttyImageReadyAtMs = 0;
|
|
@@ -952,6 +1023,17 @@ export class TUI extends Container {
|
|
|
952
1023
|
#cancelPostmortemRestore?: () => void;
|
|
953
1024
|
/** True between a `deferInput` start() and enableInput(). */
|
|
954
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;
|
|
955
1037
|
// Always-on event-loop lag probe. The high default threshold keeps it quiet;
|
|
956
1038
|
// it only logs `ui.loop-blocked` (with the current loop phase) when a frame
|
|
957
1039
|
// budget is genuinely starved. Armed in start(), disarmed in stop().
|
|
@@ -982,15 +1064,25 @@ export class TUI extends Container {
|
|
|
982
1064
|
#altToggleColumns = 0;
|
|
983
1065
|
#altToggleRows = 0;
|
|
984
1066
|
#altToggleEchoPending = false;
|
|
985
|
-
// True while an in-place resize
|
|
986
|
-
//
|
|
987
|
-
// resolves, so ordinary paints are dropped until then.
|
|
1067
|
+
// True while an in-place resize waits for its settled rebuild (tmux) or
|
|
1068
|
+
// anchor recovery (Warp): ordinary paints must not use the stale anchor.
|
|
988
1069
|
#resizeInPlaceActive = false;
|
|
989
1070
|
#resizeScrollbackMode: ResizeScrollbackMode = TUI.#initialResizeScrollbackMode();
|
|
990
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 = "";
|
|
991
1077
|
// Holds an alternate-screen exit until its replacement full paint can emit it
|
|
992
1078
|
// atomically. It must survive a deferred Ghostty image frame.
|
|
993
1079
|
#pendingAltExit = "";
|
|
1080
|
+
// True while #pendingAltExit holds the resize borrow's own exit: the
|
|
1081
|
+
// settle fused it into the destructive rebuild, so the terminal is still
|
|
1082
|
+
// on the borrowed alt buffer until that frame is written. A SIGWINCH or a
|
|
1083
|
+
// fullscreen overlay arriving first adopts the buffer instead of entering
|
|
1084
|
+
// it again (see #settleResizeAltPaint).
|
|
1085
|
+
#resizeAltExitFused = false;
|
|
994
1086
|
|
|
995
1087
|
// Tern Surface Protocol backend, created when the terminal answers the
|
|
996
1088
|
// `hello` probe, or at start when the environment names Tern. Kept across
|
|
@@ -1177,6 +1269,8 @@ export class TUI extends Container {
|
|
|
1177
1269
|
|
|
1178
1270
|
#purgeInlineImages(): void {
|
|
1179
1271
|
const transmittedIds = this.#imageBudget.takeAllTransmittedIds();
|
|
1272
|
+
this.#screenImagePlacements.clear();
|
|
1273
|
+
this.#altImagePlacements.clear();
|
|
1180
1274
|
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
1181
1275
|
for (const id of transmittedIds) {
|
|
1182
1276
|
this.terminal.write(encodeKittyDeleteImage(id));
|
|
@@ -1464,6 +1558,12 @@ export class TUI extends Container {
|
|
|
1464
1558
|
// `hello` query must go out now to confirm the surface.
|
|
1465
1559
|
const nativeExpected = this.terminal.tspExpected === true;
|
|
1466
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
|
+
}
|
|
1467
1567
|
this.#watchdog.start();
|
|
1468
1568
|
this.#ghosttyInitialImageDelayDone = false;
|
|
1469
1569
|
this.#ghosttyImageReadyAtMs = this.#renderScheduler.now() + TUI.#GHOSTTY_INITIAL_IMAGE_DELAY_MS;
|
|
@@ -1477,12 +1577,19 @@ export class TUI extends Container {
|
|
|
1477
1577
|
this.terminal.onPrivateModeReport?.((mode, supported, confirmed = true, status) => {
|
|
1478
1578
|
if (mode !== 2026 || !confirmed) return;
|
|
1479
1579
|
if (synchronizedOutputUserOverride() !== null) return;
|
|
1480
|
-
// Herdr's Ghostty
|
|
1481
|
-
// reports unrecognized (status 0). Other
|
|
1482
|
-
// still disable: status 4 is permanently
|
|
1483
|
-
// callback (`status` omitted) is a
|
|
1484
|
-
// custom Terminal that does not
|
|
1485
|
-
|
|
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
|
+
}
|
|
1486
1593
|
this.#setSynchronizedOutput(supported);
|
|
1487
1594
|
});
|
|
1488
1595
|
// Icons painted before the Glyph Protocol registration landed may sit in
|
|
@@ -1492,6 +1599,13 @@ export class TUI extends Container {
|
|
|
1492
1599
|
this.invalidate();
|
|
1493
1600
|
this.requestRender(true);
|
|
1494
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
|
+
});
|
|
1495
1609
|
this.terminal.onTspHello?.(hello => this.#onTspHello(hello));
|
|
1496
1610
|
this.terminal.start(
|
|
1497
1611
|
data => this.#handleInput(data),
|
|
@@ -1516,7 +1630,7 @@ export class TUI extends Container {
|
|
|
1516
1630
|
return;
|
|
1517
1631
|
}
|
|
1518
1632
|
this.#cancelResizeProbe();
|
|
1519
|
-
if (this.#
|
|
1633
|
+
if (this.#resizeAvoidsAltBuffer()) this.#beginResizeInPlacePaint();
|
|
1520
1634
|
else this.#beginResizeAltPaint(true);
|
|
1521
1635
|
return;
|
|
1522
1636
|
}
|
|
@@ -1545,7 +1659,7 @@ export class TUI extends Container {
|
|
|
1545
1659
|
this.requestRender(true);
|
|
1546
1660
|
return;
|
|
1547
1661
|
}
|
|
1548
|
-
if (this.#
|
|
1662
|
+
if (this.#resizeAvoidsAltBuffer()) {
|
|
1549
1663
|
this.#beginResizeInPlacePaint();
|
|
1550
1664
|
return;
|
|
1551
1665
|
}
|
|
@@ -1705,7 +1819,7 @@ export class TUI extends Container {
|
|
|
1705
1819
|
this.#resizeSettleTimer?.cancel();
|
|
1706
1820
|
this.#resizeSettleTimer = undefined;
|
|
1707
1821
|
if (this.#altActive || this.#resizeAltActive) {
|
|
1708
|
-
this.terminal.write(`${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
1822
|
+
this.terminal.write(`${this.#takePendingAltEnter()}${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
1709
1823
|
setAltScreenActive(false);
|
|
1710
1824
|
this.#altActive = false;
|
|
1711
1825
|
this.#resizeAltActive = false;
|
|
@@ -1760,11 +1874,11 @@ export class TUI extends Container {
|
|
|
1760
1874
|
};
|
|
1761
1875
|
}
|
|
1762
1876
|
/**
|
|
1763
|
-
* Whether a resize repaints the visible window in place
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
1766
|
-
*
|
|
1767
|
-
*
|
|
1877
|
+
* Whether a resize only repaints the visible window in place, without history
|
|
1878
|
+
* replay. Warp re-reports its size on alt-buffer toggles, so borrowing there
|
|
1879
|
+
* self-sustains. Inside a multiplexer the mux owns the grid and consumes the
|
|
1880
|
+
* toggles itself, so an inherited Warp marker must not suppress its replay.
|
|
1881
|
+
* tmux's synchronized rebuild is selected separately in #resizeAvoidsAltBuffer.
|
|
1768
1882
|
*
|
|
1769
1883
|
* A ConPTY host is excluded for the same reason as a multiplexer: conhost owns
|
|
1770
1884
|
* the grid the application writes to. Measured on conhost, resizing the
|
|
@@ -1783,6 +1897,21 @@ export class TUI extends Container {
|
|
|
1783
1897
|
return Bun.env.TERM_PROGRAM?.toLowerCase() === "warpterminal";
|
|
1784
1898
|
}
|
|
1785
1899
|
|
|
1900
|
+
#resizeAvoidsAltBuffer(): boolean {
|
|
1901
|
+
if (this.#resizeRepaintsInPlace()) return true;
|
|
1902
|
+
// Unlike Warp's viewport-only repaint, tmux still rebuilds history.
|
|
1903
|
+
// Restoring an alternate buffer schedules a tmux redraw that ends its
|
|
1904
|
+
// synchronized update early, exposing the rest of a long replay.
|
|
1905
|
+
return (
|
|
1906
|
+
!this.#resizeAltActive &&
|
|
1907
|
+
!this.#pendingAltExit &&
|
|
1908
|
+
resizeInPlaceOverride() !== false &&
|
|
1909
|
+
this.#resizeScrollbackMode === "rebuild" &&
|
|
1910
|
+
this.#synchronizedOutputEnabled &&
|
|
1911
|
+
classifyTerminalMultiplexerModule()?.altRestoreEndsSynchronizedOutput === true
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1786
1915
|
#noteAltBufferToggle(): void {
|
|
1787
1916
|
this.#altToggleColumns = this.terminal.columns;
|
|
1788
1917
|
this.#altToggleRows = this.terminal.rows;
|
|
@@ -1816,17 +1945,23 @@ export class TUI extends Container {
|
|
|
1816
1945
|
#trackResizeBurst(): void {
|
|
1817
1946
|
const burstLastHeight = this.#resizeBurstLastHeight ?? this.#previousHeight;
|
|
1818
1947
|
if (this.terminal.rows > burstLastHeight) this.#resizeBurstGrew = true;
|
|
1948
|
+
if (this.terminal.rows < burstLastHeight) this.#resizeBurstShrank = true;
|
|
1819
1949
|
this.#resizeBurstLastHeight = this.terminal.rows;
|
|
1820
1950
|
this.#resizeBurstPull += Math.max(0, this.terminal.rows - burstLastHeight);
|
|
1951
|
+
// Latch against the physical width: with the sidebar reserving columns,
|
|
1952
|
+
// `#previousWidth` holds the composed main-area width, which would arm
|
|
1953
|
+
// the burst on every sidebar toggle.
|
|
1954
|
+
if (this.terminal.columns !== this.#previousPhysicalWidth) this.#resizeBurstWidthChanged = true;
|
|
1955
|
+
if (this.terminal.columns !== this.#previousPhysicalWidth || this.terminal.rows !== this.#previousHeight) {
|
|
1956
|
+
this.#resizeBurstResized = true;
|
|
1957
|
+
}
|
|
1821
1958
|
this.#geometryEpoch++;
|
|
1822
1959
|
}
|
|
1823
1960
|
|
|
1824
1961
|
/**
|
|
1825
|
-
* Coalesced
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1828
|
-
* snapshots the live window and runs the CPR anchor probe, and the single
|
|
1829
|
-
* settled repaint lands on the recovered anchor with no ED3 rewrap.
|
|
1962
|
+
* Coalesced resize without borrowing the alt buffer. Once quiet, tmux can
|
|
1963
|
+
* rebuild directly under synchronized output; viewport-only repaints recover
|
|
1964
|
+
* their anchor with CPR. The existing screen remains visible while waiting.
|
|
1830
1965
|
*/
|
|
1831
1966
|
#beginResizeInPlacePaint(): void {
|
|
1832
1967
|
if (this.#altActive) {
|
|
@@ -1838,7 +1973,9 @@ export class TUI extends Container {
|
|
|
1838
1973
|
this.#resizeSettleTimer?.cancel();
|
|
1839
1974
|
this.#forgetHardwareCursorState();
|
|
1840
1975
|
this.#recordHardwareCursorHidden();
|
|
1841
|
-
|
|
1976
|
+
const erase = this.#liveViewportResizeErase();
|
|
1977
|
+
if (erase !== "") {
|
|
1978
|
+
this.terminal.write(erase);
|
|
1842
1979
|
// The erase parked the hardware cursor on the viewport's top row;
|
|
1843
1980
|
// snapshot the parked offset so the settled probe anchors there.
|
|
1844
1981
|
this.#parkedViewportOffset = 0;
|
|
@@ -1846,6 +1983,12 @@ export class TUI extends Container {
|
|
|
1846
1983
|
this.#resizeSettleTimer = this.#renderScheduler.scheduleRender(() => {
|
|
1847
1984
|
this.#resizeSettleTimer = undefined;
|
|
1848
1985
|
if (this.#stopped) return;
|
|
1986
|
+
this.#prepareResizeReplay(this.terminal.columns, this.terminal.rows);
|
|
1987
|
+
if (this.#clearScrollbackOnNextRender) {
|
|
1988
|
+
this.#resizeInPlaceActive = false;
|
|
1989
|
+
this.requestRender();
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1849
1992
|
this.#resizeProbeWindow = this.#providerWindow;
|
|
1850
1993
|
this.#resizeProbeOffset = this.#parkedViewportOffset;
|
|
1851
1994
|
this.#beginResizeAnchorProbe();
|
|
@@ -1867,13 +2010,14 @@ export class TUI extends Container {
|
|
|
1867
2010
|
* cursor-relative addressing would start rows late; fall back to the same
|
|
1868
2011
|
* bottom-preserving bound as resize-anchor recovery.
|
|
1869
2012
|
*
|
|
1870
|
-
* Both erase paths leave the cursor on the viewport's top row. Returns
|
|
1871
|
-
*
|
|
1872
|
-
* 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).
|
|
1873
2017
|
*/
|
|
1874
|
-
#
|
|
2018
|
+
#liveViewportResizeErase(): string {
|
|
1875
2019
|
if (!this.#hasEverRendered || this.#providerWindow.length === 0 || isInsideTerminalMultiplexer()) {
|
|
1876
|
-
return
|
|
2020
|
+
return "";
|
|
1877
2021
|
}
|
|
1878
2022
|
if (this.terminal.rows < this.#previousHeight) {
|
|
1879
2023
|
const staleRows = this.#reflowedRowCount(
|
|
@@ -1883,13 +2027,11 @@ export class TUI extends Container {
|
|
|
1883
2027
|
this.terminal.columns,
|
|
1884
2028
|
);
|
|
1885
2029
|
const top = Math.max(0, Math.min(this.#providerViewportTop, this.terminal.rows - staleRows));
|
|
1886
|
-
|
|
1887
|
-
} else {
|
|
1888
|
-
const up = this.#reflowedRowCount(this.#providerWindow, 0, this.#parkedViewportOffset, this.terminal.columns);
|
|
1889
|
-
const eraseBelow = this.#eraseBelowCursorRow(this.terminal.columns, this.terminal.rows);
|
|
1890
|
-
this.terminal.write(`\x1b[?25l${up > 0 ? `\x1b[${up}A` : ""}${eraseBelow}`);
|
|
2030
|
+
return `\x1b[?25l${this.#eraseBelowRow(top, this.terminal.rows)}`;
|
|
1891
2031
|
}
|
|
1892
|
-
|
|
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}`;
|
|
1893
2035
|
}
|
|
1894
2036
|
|
|
1895
2037
|
/**
|
|
@@ -1905,15 +2047,27 @@ export class TUI extends Container {
|
|
|
1905
2047
|
return;
|
|
1906
2048
|
}
|
|
1907
2049
|
this.#trackResizeBurst();
|
|
1908
|
-
if (!this.#resizeAltActive) {
|
|
2050
|
+
if (!this.#resizeAltActive && this.#resizeAltExitFused) {
|
|
2051
|
+
// The settled rebuild has not been written yet, so its fused exit never
|
|
2052
|
+
// left the borrowed buffer: resume the borrow there. The live window was
|
|
2053
|
+
// already stashed and the rebuild stays latched, so there is nothing to
|
|
2054
|
+
// snapshot or erase, and entering again would stack another alt switch.
|
|
2055
|
+
this.#resizeAltActive = true;
|
|
2056
|
+
this.#pendingAltExit = "";
|
|
2057
|
+
this.#resizeAltExitFused = false;
|
|
2058
|
+
} else if (!this.#resizeAltActive) {
|
|
1909
2059
|
this.#resizeAltActive = true;
|
|
1910
2060
|
setAltScreenActive(true);
|
|
1911
2061
|
this.#altPreviousLines = [];
|
|
1912
2062
|
this.#altPreparedRows = [];
|
|
1913
2063
|
this.#forgetHardwareCursorState();
|
|
1914
2064
|
this.#recordHardwareCursorHidden();
|
|
1915
|
-
// Blank the live region
|
|
1916
|
-
// 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
|
|
1917
2071
|
// the settled CPR probe: its reflowed row count bounds the anchor to
|
|
1918
2072
|
// `height - staleRows`, so a mis-parked cursor (a single-step tmux zoom
|
|
1919
2073
|
// re-lays the pane before SIGWINCH delivery, moving the park target
|
|
@@ -1923,8 +2077,9 @@ export class TUI extends Container {
|
|
|
1923
2077
|
this.#resizeProbeWindow = this.#providerWindow;
|
|
1924
2078
|
this.#resizeProbeOffset = this.#parkedViewportOffset;
|
|
1925
2079
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
2080
|
+
const erase = this.#liveViewportResizeErase();
|
|
2081
|
+
if (erase !== "") {
|
|
2082
|
+
// The erase parks the cursor on the viewport's top row, so the
|
|
1928
2083
|
// parked offset no longer applies; carrying a stale nonzero offset
|
|
1929
2084
|
// into the probe would anchor the settled repaint above the real
|
|
1930
2085
|
// viewport top and overwrite visible committed rows.
|
|
@@ -1947,23 +2102,70 @@ export class TUI extends Container {
|
|
|
1947
2102
|
}
|
|
1948
2103
|
this.#noteAltBufferToggle();
|
|
1949
2104
|
this.#imageBudget.beginAltScreenLifecycle();
|
|
1950
|
-
this
|
|
2105
|
+
this.#pendingAltEnter = `${erase}\x1b[?1049h${this.#keyboardEnhancementEnter()}`;
|
|
1951
2106
|
}
|
|
1952
2107
|
this.#resizeSettleTimer?.cancel();
|
|
1953
2108
|
this.#resizeSettleTimer = this.#renderScheduler.scheduleRender(() => {
|
|
1954
2109
|
this.#resizeSettleTimer = undefined;
|
|
1955
2110
|
if (this.#stopped || !this.#resizeAltActive) return;
|
|
1956
|
-
this.#
|
|
1957
|
-
this.#suppressResizeUntil = this.#renderScheduler.now() + 100;
|
|
1958
|
-
this.#noteAltBufferToggle();
|
|
1959
|
-
this.terminal.write(`${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
1960
|
-
setAltScreenActive(false);
|
|
1961
|
-
this.#altPreviousLines = [];
|
|
1962
|
-
this.#altPreparedRows = [];
|
|
1963
|
-
this.#beginResizeAnchorProbe();
|
|
2111
|
+
this.#settleResizeAltPaint();
|
|
1964
2112
|
}, TUI.#RESIZE_VIEWPORT_SETTLE_MS);
|
|
1965
2113
|
this.requestRender(true);
|
|
1966
2114
|
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* End the alt-buffer borrow once the resize settles.
|
|
2118
|
+
*
|
|
2119
|
+
* A settle that rebuilds history never needs the viewport anchor: the
|
|
2120
|
+
* destructive reset erases the screen and history and repaints from row
|
|
2121
|
+
* zero. Restoring the normal buffer on its own write would only expose the
|
|
2122
|
+
* reflowed stale screen for the CPR round trip, then clear it and stream
|
|
2123
|
+
* the replay — the one flash a large resize (a tmux zoom) still showed. So
|
|
2124
|
+
* that settle latches the rebuild now and fuses the restore into the same
|
|
2125
|
+
* synchronized write as ED2+ED3 and the replay: the terminal goes straight
|
|
2126
|
+
* from the resize frame to the rebuilt screen.
|
|
2127
|
+
*
|
|
2128
|
+
* Every other settle restores the normal buffer and probes the reflowed
|
|
2129
|
+
* anchor, which the non-destructive repaint depends on.
|
|
2130
|
+
*/
|
|
2131
|
+
#settleResizeAltPaint(): void {
|
|
2132
|
+
this.#resizeAltActive = false;
|
|
2133
|
+
this.#suppressResizeUntil = this.#renderScheduler.now() + 100;
|
|
2134
|
+
this.#altPreviousLines = [];
|
|
2135
|
+
this.#altPreparedRows = [];
|
|
2136
|
+
const exitSequence = `${this.#keyboardEnhancementExit()}\x1b[?1049l`;
|
|
2137
|
+
// Only the provider plan frame emits a pending exit, and an exit already
|
|
2138
|
+
// pending (a fused overlay close) owns that slot.
|
|
2139
|
+
if (this.#frameProvider !== undefined && this.#pendingAltExit === "") {
|
|
2140
|
+
if (this.#resizeScrollbackMode === "rebuild") {
|
|
2141
|
+
this.#prepareResizeReplay(this.terminal.columns, this.terminal.rows);
|
|
2142
|
+
}
|
|
2143
|
+
if (this.#clearScrollbackOnNextRender) {
|
|
2144
|
+
// An entry no resize frame carried yet rides ahead of this exit in
|
|
2145
|
+
// the rebuild frame (see #renderProviderFrame).
|
|
2146
|
+
this.#pendingAltExit = exitSequence;
|
|
2147
|
+
this.#resizeAltExitFused = true;
|
|
2148
|
+
this.requestRender(true);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
this.#noteAltBufferToggle();
|
|
2153
|
+
this.terminal.write(this.#takePendingAltEnter() + exitSequence);
|
|
2154
|
+
setAltScreenActive(false);
|
|
2155
|
+
this.#beginResizeAnchorProbe();
|
|
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
|
+
|
|
1967
2169
|
/**
|
|
1968
2170
|
* Recover the reflowed viewport anchor after the resize settle window ends.
|
|
1969
2171
|
* The terminal reflowed the restored normal buffer during the drag, so
|
|
@@ -2083,6 +2285,23 @@ export class TUI extends Container {
|
|
|
2083
2285
|
let top: number;
|
|
2084
2286
|
if (isInsideTerminalMultiplexer()) {
|
|
2085
2287
|
if (reportedRow !== undefined) {
|
|
2288
|
+
// tmux can grow its grid before SIGWINCH reaches the app. A queued
|
|
2289
|
+
// retirement frame can then overwrite pulled-down history at the old
|
|
2290
|
+
// coordinates and park the cursor there. If a previously full viewport
|
|
2291
|
+
// no longer reaches the bottom, replay its semantic history instead of
|
|
2292
|
+
// trusting that potentially damaged physical copy. Ordinary grows with
|
|
2293
|
+
// an intact bottom anchor still need no replay or additional wait.
|
|
2294
|
+
if (
|
|
2295
|
+
this.#resizeScrollbackMode === "rebuild" &&
|
|
2296
|
+
classifyTerminalMultiplexerModule()?.growsBeforeSigwinch === true &&
|
|
2297
|
+
!this.#resizeRepaintsInPlace() &&
|
|
2298
|
+
this.#frameProvider?.beginHistoryReplay &&
|
|
2299
|
+
this.#resizeBurstGrew &&
|
|
2300
|
+
this.#providerViewportTop + probe.window.length >= this.#previousHeight &&
|
|
2301
|
+
reportedTop + staleRows < height
|
|
2302
|
+
) {
|
|
2303
|
+
this.#prepareForcedRender(true);
|
|
2304
|
+
}
|
|
2086
2305
|
// The parked cursor's reply is exact under multiplexer clipping:
|
|
2087
2306
|
// discards leave the cursor in place, pushes only occur after
|
|
2088
2307
|
// everything below it is discarded (the bottom row IS the
|
|
@@ -2195,6 +2414,35 @@ export class TUI extends Container {
|
|
|
2195
2414
|
this.terminal.enableInput?.();
|
|
2196
2415
|
this.#querySixelSupport();
|
|
2197
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;
|
|
2198
2446
|
}
|
|
2199
2447
|
|
|
2200
2448
|
addStartListener(listener: StartListener): () => void {
|
|
@@ -2220,17 +2468,19 @@ export class TUI extends Container {
|
|
|
2220
2468
|
// PI_FORCE_IMAGE_PROTOCOL choice — including its `off` kill switch — wins
|
|
2221
2469
|
// over the probe.
|
|
2222
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;
|
|
2223
2475
|
if (isImageProtocolForced()) return;
|
|
2224
2476
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
2225
2477
|
|
|
2226
2478
|
this.#clearSixelProbeState();
|
|
2227
2479
|
this.#sixelProbePendingGraphics = true;
|
|
2228
2480
|
this.#sixelProbeUnsubscribe = this.addInputListener(data => this.#handleSixelProbeInput(data));
|
|
2229
|
-
// XTSMGRAPHICS item 2 reports the
|
|
2230
|
-
//
|
|
2231
|
-
//
|
|
2232
|
-
// composer (#8542): those bytes never reach an input listener, so this probe
|
|
2233
|
-
// 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.
|
|
2234
2484
|
this.terminal.write("\x1b[?2;1;0S");
|
|
2235
2485
|
this.#sixelProbeTimeout = setTimeout(() => {
|
|
2236
2486
|
this.#finishSixelProbe(false);
|
|
@@ -2314,7 +2564,11 @@ export class TUI extends Container {
|
|
|
2314
2564
|
|
|
2315
2565
|
#finishSixelProbe(supported: boolean): void {
|
|
2316
2566
|
this.#clearSixelProbeState();
|
|
2317
|
-
if (
|
|
2567
|
+
if (supported) this.#enableSixelProtocol();
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
#enableSixelProtocol(): void {
|
|
2571
|
+
if (TERMINAL.imageProtocol) return;
|
|
2318
2572
|
|
|
2319
2573
|
setTerminalImageProtocol(ImageProtocol.Sixel);
|
|
2320
2574
|
this.#queryCellSize();
|
|
@@ -2408,7 +2662,7 @@ export class TUI extends Container {
|
|
|
2408
2662
|
this.#cancelResizeProbe();
|
|
2409
2663
|
if (this.#resizeAltActive) {
|
|
2410
2664
|
this.#resizeAltActive = false;
|
|
2411
|
-
this.terminal.write(`${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
2665
|
+
this.terminal.write(`${this.#takePendingAltEnter()}${this.#keyboardEnhancementExit()}\x1b[?1049l`);
|
|
2412
2666
|
setAltScreenActive(false);
|
|
2413
2667
|
}
|
|
2414
2668
|
if (this.#altActive || this.#pendingAltExit) {
|
|
@@ -2419,7 +2673,7 @@ export class TUI extends Container {
|
|
|
2419
2673
|
// so the final OFF goes last or the shell keeps reporting.
|
|
2420
2674
|
const mouseExit = this.#mouseTracking !== "off" ? MOUSE_TRACKING_OFF : "";
|
|
2421
2675
|
const exitSequence = this.#pendingAltExit
|
|
2422
|
-
? `${this.#pendingAltExit}${mouseExit}`
|
|
2676
|
+
? `${this.#takePendingAltEnter()}${this.#pendingAltExit}${mouseExit}`
|
|
2423
2677
|
: `${mouseExit}${this.#keyboardEnhancementExit()}\x1b[?1049l`;
|
|
2424
2678
|
this.terminal.write(exitSequence);
|
|
2425
2679
|
setAltScreenActive(false);
|
|
@@ -2428,6 +2682,7 @@ export class TUI extends Container {
|
|
|
2428
2682
|
this.#altPreviousLines = [];
|
|
2429
2683
|
this.#altPreparedRows = [];
|
|
2430
2684
|
this.#pendingAltExit = "";
|
|
2685
|
+
this.#resizeAltExitFused = false;
|
|
2431
2686
|
} else if (this.#mouseTracking !== "off") {
|
|
2432
2687
|
// Inline capture with no overlay: still owned by us at quit, so
|
|
2433
2688
|
// release it — otherwise the parent shell keeps mouse reporting
|
|
@@ -2582,6 +2837,13 @@ export class TUI extends Container {
|
|
|
2582
2837
|
if (this.#renderTimer) {
|
|
2583
2838
|
this.#renderTimer.cancel();
|
|
2584
2839
|
this.#renderTimer = undefined;
|
|
2840
|
+
// The cancelled timer was the only callback owed to a pending ordinary
|
|
2841
|
+
// request. Every caller paints right after (a forced or ordinary request
|
|
2842
|
+
// it issues, or the render already in progress), and that paint serves
|
|
2843
|
+
// the request. Leaving the flag set would turn every later ordinary
|
|
2844
|
+
// request into a no-op: the resize settle's rebuild and all spinner
|
|
2845
|
+
// frames would never paint until some forced render arrived.
|
|
2846
|
+
this.#renderRequested = false;
|
|
2585
2847
|
}
|
|
2586
2848
|
}
|
|
2587
2849
|
|
|
@@ -2683,7 +2945,10 @@ export class TUI extends Container {
|
|
|
2683
2945
|
const tagEpoch = this.#cprColumnTags.get(column);
|
|
2684
2946
|
this.#cprColumnTags.delete(column);
|
|
2685
2947
|
const probe = this.#resizeProbe;
|
|
2686
|
-
|
|
2948
|
+
// tmux can resize its grid before the application receives SIGWINCH.
|
|
2949
|
+
// A reply below our known screen is from that newer geometry: do not
|
|
2950
|
+
// repaint it using the stale height. Wait for resize or the probe retry.
|
|
2951
|
+
if (probe !== undefined && tagEpoch === probe.epoch && row >= 1 && row <= this.terminal.rows) {
|
|
2687
2952
|
this.#resolveResizeAnchor(Number(match[1]) - 1);
|
|
2688
2953
|
}
|
|
2689
2954
|
}
|
|
@@ -2693,12 +2958,47 @@ export class TUI extends Container {
|
|
|
2693
2958
|
data = data.slice(0, searchFrom + match.index) + data.slice(searchFrom + match.index + match[0].length);
|
|
2694
2959
|
}
|
|
2695
2960
|
if (data.length === 0) return;
|
|
2961
|
+
|
|
2962
|
+
if (this.#heldInput !== undefined && this.#focusedComponent === this.#heldFocus) {
|
|
2963
|
+
this.#holdInput(data);
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// If focused component is an overlay, verify it's still visible (visibility can change due to
|
|
2968
|
+
// terminal resize or visible() callback). Runs before the capture preflight below, which must
|
|
2969
|
+
// target the effective focus owner, not a hidden overlay.
|
|
2970
|
+
const focusedOverlay = this.overlayStack.find(o => o.component === this.#focusedComponent);
|
|
2971
|
+
if (focusedOverlay && !this.#isOverlayVisible(focusedOverlay)) {
|
|
2972
|
+
// Focused overlay is no longer visible, redirect to topmost visible overlay
|
|
2973
|
+
const topVisible = this.#getTopmostVisibleOverlay();
|
|
2974
|
+
if (topVisible) {
|
|
2975
|
+
this.setFocus(topVisible.component);
|
|
2976
|
+
} else {
|
|
2977
|
+
// No visible overlays, restore to preFocus
|
|
2978
|
+
this.setFocus(focusedOverlay.preFocus);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2696
2982
|
// Ctrl+C/Esc use app-level double-press windows. Give those gestures one
|
|
2697
2983
|
// frame to drain queued input before an ordinary repaint; delaying every
|
|
2698
2984
|
// key would make idle navigation pay a full frame of latency.
|
|
2699
2985
|
if (matchesKey(data, "ctrl+c") || matchesKey(data, "escape")) {
|
|
2700
2986
|
this.#inputRenderGraceUntilMs = this.#renderScheduler.now() + TUI.#INPUT_RENDER_GRACE_MS;
|
|
2701
2987
|
}
|
|
2988
|
+
|
|
2989
|
+
// A focused input owner can reserve its gesture before TUI-wide listeners
|
|
2990
|
+
// and debug shortcuts see the key, then handle the raw event exactly once.
|
|
2991
|
+
const focusedForCapture = this.#focusedComponent;
|
|
2992
|
+
if (
|
|
2993
|
+
focusedForCapture?.handleInput &&
|
|
2994
|
+
(!isKeyRelease(data) || focusedForCapture.wantsKeyRelease) &&
|
|
2995
|
+
focusedForCapture.capturesInput?.(data)
|
|
2996
|
+
) {
|
|
2997
|
+
focusedForCapture.handleInput(data);
|
|
2998
|
+
this.requestRender();
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
2702
3002
|
if (this.#inputListeners.size > 0) {
|
|
2703
3003
|
let current = data;
|
|
2704
3004
|
for (const listener of this.#inputListeners) {
|
|
@@ -2727,20 +3027,6 @@ export class TUI extends Container {
|
|
|
2727
3027
|
return;
|
|
2728
3028
|
}
|
|
2729
3029
|
|
|
2730
|
-
// If focused component is an overlay, verify it's still visible
|
|
2731
|
-
// (visibility can change due to terminal resize or visible() callback)
|
|
2732
|
-
const focusedOverlay = this.overlayStack.find(o => o.component === this.#focusedComponent);
|
|
2733
|
-
if (focusedOverlay && !this.#isOverlayVisible(focusedOverlay)) {
|
|
2734
|
-
// Focused overlay is no longer visible, redirect to topmost visible overlay
|
|
2735
|
-
const topVisible = this.#getTopmostVisibleOverlay();
|
|
2736
|
-
if (topVisible) {
|
|
2737
|
-
this.setFocus(topVisible.component);
|
|
2738
|
-
} else {
|
|
2739
|
-
// No visible overlays, restore to preFocus
|
|
2740
|
-
this.setFocus(focusedOverlay.preFocus);
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
3030
|
// Pass input to focused component (including Ctrl+C).
|
|
2745
3031
|
// The focused component can decide how to handle Ctrl+C.
|
|
2746
3032
|
// Opted-in components only dirty their focused subtree. Unregistered
|
|
@@ -2757,6 +3043,18 @@ export class TUI extends Container {
|
|
|
2757
3043
|
}
|
|
2758
3044
|
}
|
|
2759
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
|
+
|
|
2760
3058
|
#consumeCellSizeResponse(data: string): boolean {
|
|
2761
3059
|
// Response format: ESC [ 6 ; height ; width t
|
|
2762
3060
|
const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/);
|
|
@@ -2955,103 +3253,87 @@ export class TUI extends Container {
|
|
|
2955
3253
|
if (idx < 0 || idx >= result.length) continue;
|
|
2956
3254
|
const truncatedOverlayLine =
|
|
2957
3255
|
visibleWidth(overlayLines[i]) > width ? sliceByColumn(overlayLines[i], 0, width, true) : overlayLines[i];
|
|
2958
|
-
result[idx] =
|
|
3256
|
+
result[idx] = compositeLineAt(result[idx], truncatedOverlayLine, col, width, termWidth);
|
|
2959
3257
|
}
|
|
2960
3258
|
}
|
|
2961
3259
|
return result;
|
|
2962
3260
|
}
|
|
2963
3261
|
|
|
2964
|
-
/**
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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,
|
|
2971
3271
|
): string {
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
// Single pass through baseLine extracts both before and after segments
|
|
2982
|
-
const afterStart = startCol + overlayWidth;
|
|
2983
|
-
const base = extractSegments(baseLine, startCol, afterStart, totalWidth - afterStart, true);
|
|
2984
|
-
|
|
2985
|
-
// Extract overlay with width tracking (strict=true to exclude wide chars at boundary)
|
|
2986
|
-
const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true);
|
|
2987
|
-
|
|
2988
|
-
// Pad segments to target widths
|
|
2989
|
-
const beforePad = Math.max(0, startCol - base.beforeWidth);
|
|
2990
|
-
const overlayPad = Math.max(0, overlayWidth - overlay.width);
|
|
2991
|
-
const actualBeforeWidth = Math.max(startCol, base.beforeWidth);
|
|
2992
|
-
const actualOverlayWidth = Math.max(overlayWidth, overlay.width);
|
|
2993
|
-
const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
|
|
2994
|
-
const afterPad = Math.max(0, afterTarget - base.afterWidth);
|
|
2995
|
-
|
|
2996
|
-
// Compose result
|
|
2997
|
-
const r = SEGMENT_RESET;
|
|
2998
|
-
const result =
|
|
2999
|
-
base.before +
|
|
3000
|
-
" ".repeat(beforePad) +
|
|
3001
|
-
r +
|
|
3002
|
-
overlay.text +
|
|
3003
|
-
" ".repeat(overlayPad) +
|
|
3004
|
-
r +
|
|
3005
|
-
base.after +
|
|
3006
|
-
" ".repeat(afterPad);
|
|
3007
|
-
|
|
3008
|
-
// CRITICAL: Always verify and truncate to terminal width.
|
|
3009
|
-
// This is the final safeguard against width overflow which would crash the TUI.
|
|
3010
|
-
// Width tracking can drift from actual visible width due to:
|
|
3011
|
-
// - Complex ANSI/OSC sequences (hyperlinks, colors)
|
|
3012
|
-
// - Wide characters at segment boundaries
|
|
3013
|
-
// - Edge cases in segment extraction
|
|
3014
|
-
const resultWidth = visibleWidth(result);
|
|
3015
|
-
if (resultWidth <= totalWidth) {
|
|
3016
|
-
return result;
|
|
3017
|
-
}
|
|
3018
|
-
// Truncate with strict=true to ensure we don't exceed totalWidth
|
|
3019
|
-
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;
|
|
3020
3279
|
}
|
|
3021
3280
|
|
|
3022
|
-
/**
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
*/
|
|
3030
|
-
#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 {
|
|
3031
3288
|
if (screenRow < 0) return line;
|
|
3032
3289
|
const parsed = parseKittyDirectPlacementLine(line);
|
|
3033
3290
|
if (!parsed) return line;
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
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
|
+
})
|
|
3041
3305
|
);
|
|
3042
|
-
if (!placement) return line;
|
|
3043
|
-
return encodeKittyPlacementLine({
|
|
3044
|
-
imageId: parsed.imageId,
|
|
3045
|
-
placementId: placement.placementId,
|
|
3046
|
-
columns: parsed.columns,
|
|
3047
|
-
rows: parsed.rows,
|
|
3048
|
-
screenRow,
|
|
3049
|
-
imageHeightPx: placement.heightPx,
|
|
3050
|
-
});
|
|
3051
3306
|
}
|
|
3052
3307
|
|
|
3053
3308
|
#terminalLine(line: PreparedLine): string {
|
|
3054
|
-
|
|
3309
|
+
if (line.hasOsc8) return line.terminalContent + LINE_TERMINATOR;
|
|
3310
|
+
return line.terminalContent.endsWith(SEGMENT_RESET) ? line.terminalContent : line.terminalContent + SEGMENT_RESET;
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
/** Encode padding with REP, which tmux expands into identical styled cells. */
|
|
3314
|
+
#compactReplaySpaces(line: string): string {
|
|
3315
|
+
let copied = 0;
|
|
3316
|
+
let output = "";
|
|
3317
|
+
for (let offset = 0; offset < line.length;) {
|
|
3318
|
+
const spaces = line.indexOf(" ", offset);
|
|
3319
|
+
if (spaces === -1) break;
|
|
3320
|
+
const escape = line.indexOf("\x1b", offset);
|
|
3321
|
+
if (escape !== -1 && escape < spaces) {
|
|
3322
|
+
// Only inspect text outside CSI/OSC. Unknown control strings (for
|
|
3323
|
+
// example DCS payloads) must remain byte-for-byte unchanged.
|
|
3324
|
+
const introducer = line.charCodeAt(escape + 1);
|
|
3325
|
+
if (introducer !== 0x5b && introducer !== 0x5d) return line;
|
|
3326
|
+
const end = this.#ansiSequenceEnd(line, escape);
|
|
3327
|
+
if (end < 0) break;
|
|
3328
|
+
offset = end;
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
let end = spaces + 8;
|
|
3332
|
+
while (line.charCodeAt(end) === 0x20) end++;
|
|
3333
|
+
output += `${line.slice(copied, spaces)} \x1b[${end - spaces - 1}b`;
|
|
3334
|
+
copied = offset = end;
|
|
3335
|
+
}
|
|
3336
|
+
return copied === 0 ? line : output + line.slice(copied);
|
|
3055
3337
|
}
|
|
3056
3338
|
|
|
3057
3339
|
#notifyPaint(paint: TuiPaint): void {
|
|
@@ -3128,12 +3410,21 @@ export class TUI extends Container {
|
|
|
3128
3410
|
* out of scrollback), so replaying would write an identical duplicate — the
|
|
3129
3411
|
* editor/status chrome included — below the retained copy. Skip it, matching
|
|
3130
3412
|
* the `widthChanged`-gated commit-ledger logic in {@link #doRender}.
|
|
3413
|
+
*
|
|
3414
|
+
* Both gates key on the whole coalesced burst, not the net change: the
|
|
3415
|
+
* terminal reflowed the normal buffer at every intermediate geometry, so a
|
|
3416
|
+
* drag that ends where it started (80 → 60 → 80) has still rewrapped
|
|
3417
|
+
* retained history and pushed unerased live rows into it. Comparing only
|
|
3418
|
+
* the settled size against the committed one would skip the refresh and
|
|
3419
|
+
* leave those stale copies stacked above the repainted viewport.
|
|
3131
3420
|
*/
|
|
3132
3421
|
#prepareResizeReplay(width: number, height: number): void {
|
|
3133
3422
|
const size = `${width}x${height}`;
|
|
3423
|
+
const widthChanged = this.#resizeBurstWidthChanged || width !== this.#previousPhysicalWidth;
|
|
3424
|
+
const resized = this.#resizeBurstResized || widthChanged || height !== this.#previousHeight;
|
|
3134
3425
|
if (
|
|
3135
3426
|
!this.#hasEverRendered ||
|
|
3136
|
-
|
|
3427
|
+
!resized ||
|
|
3137
3428
|
this.#resizeReplaySize === size ||
|
|
3138
3429
|
this.#resizeScrollbackMode === "preserve" ||
|
|
3139
3430
|
// In-place resizes (Warp) repaint the settled viewport once the drag
|
|
@@ -3150,15 +3441,38 @@ export class TUI extends Container {
|
|
|
3150
3441
|
this.#forceViewportRepaintOnNextRender = true;
|
|
3151
3442
|
return;
|
|
3152
3443
|
}
|
|
3444
|
+
// A height-only settled resize rewraps nothing — rewrap is a width change
|
|
3445
|
+
// everywhere. A pure height grow pulls committed scrollback down without
|
|
3446
|
+
// polluting a copy, so the rebuild (an ED3 plus a full ledger replay)
|
|
3447
|
+
// would be a destructive repaint that buys nothing. `rebuild` still
|
|
3448
|
+
// refreshes on a burst shrink (the multiplexer pushes live pane rows into its
|
|
3449
|
+
// scrollback; the destructive refresh is the only purge) and on a host
|
|
3450
|
+
// that repaints its own grid (ConPTY's stale re-emission is untrusted).
|
|
3153
3451
|
if (this.#resizeScrollbackMode === "rebuild") {
|
|
3452
|
+
// Every destructive cause warrants a rebuild: a burst shrink can push
|
|
3453
|
+
// live rows into history (tmux discards rows below the cursor, then
|
|
3454
|
+
// pushes rows above it; a later grow cannot undo that clipping or the
|
|
3455
|
+
// provider's retirement), and ConPTY can repaint its own stale grid.
|
|
3456
|
+
if (!widthChanged && this.terminal.hostOwnsGridOnResize !== true && !this.#resizeBurstShrank) return;
|
|
3154
3457
|
this.#prepareForcedRender(true);
|
|
3155
3458
|
return;
|
|
3156
3459
|
}
|
|
3157
|
-
if (
|
|
3460
|
+
if (!widthChanged) return;
|
|
3158
3461
|
provider.beginHistoryReplay();
|
|
3159
3462
|
this.#forceViewportRepaintOnNextRender = true;
|
|
3160
3463
|
}
|
|
3161
3464
|
|
|
3465
|
+
/**
|
|
3466
|
+
* Bottom-row `Rebuilding…` notice, closed as its own synchronized update so
|
|
3467
|
+
* tmux forwards it before the replay that follows starts a new hold. Cursor
|
|
3468
|
+
* save/restore and no newline keep the row out of native scrollback; the
|
|
3469
|
+
* replay's ED2 erases it together with the rest of the screen.
|
|
3470
|
+
*/
|
|
3471
|
+
#rebuildNoticeSequence(width: number, height: number): string {
|
|
3472
|
+
const label = truncateToWidth("↻ Rebuilding…", width, Ellipsis.Omit);
|
|
3473
|
+
return `${this.#paintBeginSequence}\x1b7\x1b[${height};1H${SEGMENT_RESET}${ERASE_LINE}${label}${LINE_TERMINATOR}\x1b8${this.#paintEndSequence}`;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3162
3476
|
/**
|
|
3163
3477
|
* Erase `row` and everything below it, absolute-addressed, without ever
|
|
3164
3478
|
* issuing a full-screen clear. tmux and Windows conhost (#9597) archive a
|
|
@@ -3250,6 +3564,7 @@ export class TUI extends Container {
|
|
|
3250
3564
|
// resize in rebuild mode): erase native history and the viewport,
|
|
3251
3565
|
// then repaint from row zero.
|
|
3252
3566
|
const destructiveReset = this.#clearScrollbackOnNextRender;
|
|
3567
|
+
const compactReplay = destructiveReset && classifyTerminalMultiplexerModule()?.expandsRepPadding === true;
|
|
3253
3568
|
if (destructiveReset) {
|
|
3254
3569
|
this.#providerViewportTop = 0;
|
|
3255
3570
|
this.#providerWindow = [];
|
|
@@ -3259,11 +3574,40 @@ export class TUI extends Container {
|
|
|
3259
3574
|
// screen. Appending K history rows moves the anchor down by K; the write
|
|
3260
3575
|
// scrolls only when history + viewport overflow the physical screen, and
|
|
3261
3576
|
// the rows that scroll off the top are exactly the oldest history rows.
|
|
3577
|
+
//
|
|
3578
|
+
// A frame that transiently grew (an editor suggestion row) scrolls the same
|
|
3579
|
+
// way — the extra rows must become visible — but its push was never
|
|
3580
|
+
// committed by a history transaction, so the shrink when the transient
|
|
3581
|
+
// chrome row leaves pulls those rows back and restores the pre-growth
|
|
3582
|
+
// anchor instead of parking the frame one row high above a trailing blank.
|
|
3583
|
+
// Frames carrying a history transaction, a reset, an overlay, or a new
|
|
3584
|
+
// geometry commit or supersede the scroll and track nothing.
|
|
3262
3585
|
const geometryStable = this.#hasEverRendered && this.#previousWidth === width && this.#previousHeight === height;
|
|
3263
3586
|
const startTop = destructiveReset ? 0 : Math.min(this.#providerViewportTop, Math.max(0, height - 1));
|
|
3264
|
-
const
|
|
3587
|
+
const transientFrame =
|
|
3588
|
+
history === undefined && !destructiveReset && geometryStable && this.#getTopmostVisibleOverlay() === undefined;
|
|
3589
|
+
let anchorPull = 0;
|
|
3590
|
+
if (transientFrame && rows < this.#previousFrameLength) {
|
|
3591
|
+
anchorPull = Math.min(Math.max(0, height - rows - startTop), this.#transientScrollPushed);
|
|
3592
|
+
}
|
|
3593
|
+
const newTop = Math.max(0, Math.min(startTop + historyRows.length, height - rows)) + anchorPull;
|
|
3265
3594
|
const pendingAltExit = this.#pendingAltExit;
|
|
3266
|
-
|
|
3595
|
+
// A fused resize exit whose borrow never painted still owes its entry.
|
|
3596
|
+
let buffer = this.#paintBeginSequence + (pendingAltExit ? this.#takePendingAltEnter() : "") + pendingAltExit;
|
|
3597
|
+
const renewSync =
|
|
3598
|
+
destructiveReset &&
|
|
3599
|
+
this.#resizeScrollbackMode === "rebuild" &&
|
|
3600
|
+
this.#synchronizedOutputEnabled &&
|
|
3601
|
+
classifyTerminalMultiplexerModule()?.expiresSynchronizedOutput === true;
|
|
3602
|
+
let syncBytes = Buffer.byteLength(buffer);
|
|
3603
|
+
const append = (sequence: string): void => {
|
|
3604
|
+
buffer += sequence;
|
|
3605
|
+
if (!renewSync) return;
|
|
3606
|
+
syncBytes += Buffer.byteLength(sequence);
|
|
3607
|
+
if (syncBytes < SYNC_OUTPUT_RENEW_BYTES) return;
|
|
3608
|
+
buffer += SYNC_OUTPUT_BEGIN;
|
|
3609
|
+
syncBytes = 0;
|
|
3610
|
+
};
|
|
3267
3611
|
if (destructiveReset && TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
3268
3612
|
// A reset is explicitly destructive, so remove every placement—not only
|
|
3269
3613
|
// the ones this TUI tracked—then resend images composed for the clean
|
|
@@ -3275,7 +3619,7 @@ export class TUI extends Container {
|
|
|
3275
3619
|
// reset forgot are named explicitly here — their tracking is gone, so
|
|
3276
3620
|
// nothing downstream could find them again.
|
|
3277
3621
|
for (const id of this.#imageBudget.takeResetPurgeIds()) buffer += encodeKittyDeleteImage(id);
|
|
3278
|
-
this.#
|
|
3622
|
+
this.#screenImagePlacements.clear();
|
|
3279
3623
|
}
|
|
3280
3624
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
3281
3625
|
for (const id of this.#imageBudget.takePurgeIds()) buffer += encodeKittyDeleteImage(id);
|
|
@@ -3297,15 +3641,17 @@ export class TUI extends Container {
|
|
|
3297
3641
|
// erase reclaim the data, so the replay's placements then referenced an
|
|
3298
3642
|
// image the terminal no longer had and every inline image vanished
|
|
3299
3643
|
// after a settled width resize.
|
|
3300
|
-
if (destructiveReset) buffer +=
|
|
3301
|
-
for (const sequence of this.#imageBudget.takeTransmits()) buffer += sequence;
|
|
3644
|
+
if (destructiveReset) buffer += `${LINE_TERMINATOR}\x1b[H\x1b[2J\x1b[3J`;
|
|
3302
3645
|
const diffable =
|
|
3646
|
+
anchorPull === 0 &&
|
|
3303
3647
|
geometryStable &&
|
|
3304
3648
|
historyRows.length === 0 &&
|
|
3305
3649
|
startTop === newTop &&
|
|
3306
3650
|
!this.#forceViewportRepaintOnNextRender &&
|
|
3307
3651
|
!destructiveReset &&
|
|
3308
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);
|
|
3309
3655
|
if (diffable) {
|
|
3310
3656
|
for (let index = 0; index < rows; index++) {
|
|
3311
3657
|
if (
|
|
@@ -3324,9 +3670,8 @@ export class TUI extends Container {
|
|
|
3324
3670
|
current,
|
|
3325
3671
|
width,
|
|
3326
3672
|
newTop + index,
|
|
3327
|
-
-1,
|
|
3328
|
-
-1,
|
|
3329
3673
|
this.#osc66SpacerGlyphWidth(prepared.lines, index),
|
|
3674
|
+
{ imagePlacements: this.#screenImagePlacements },
|
|
3330
3675
|
)}`;
|
|
3331
3676
|
}
|
|
3332
3677
|
if (this.#providerWindow.length > rows && newTop + rows < height) {
|
|
@@ -3342,29 +3687,45 @@ export class TUI extends Container {
|
|
|
3342
3687
|
if (pushed > this.#providerViewportTop && this.#providerWindow.length > 0) {
|
|
3343
3688
|
buffer += this.#eraseBelowRow(this.#providerViewportTop, height);
|
|
3344
3689
|
}
|
|
3345
|
-
|
|
3346
|
-
|
|
3690
|
+
// The un-committed push of a transiently grown frame, returned from
|
|
3691
|
+
// scrollback: `CSI Ps + T` scrolls the grid down Ps rows filling the
|
|
3692
|
+
// top from scrollback (kitty's SD extension; terminals without it
|
|
3693
|
+
// ignore the sequence and keep today's shifted layout). The write
|
|
3694
|
+
// below then lands on the restored anchor; its rows exactly fill the
|
|
3695
|
+
// screen, so nothing scrolls further.
|
|
3696
|
+
if (anchorPull > 0) buffer += `\x1b[${anchorPull}+T`;
|
|
3697
|
+
// A history-carrying write starts at the anchor and lets any overflow
|
|
3698
|
+
// scroll; a pulled write owns the whole restored region below it.
|
|
3699
|
+
const writeTop = historyRows.length > 0 || anchorPull === 0 ? startTop : newTop;
|
|
3700
|
+
buffer += `\x1b[${writeTop + 1};1H`;
|
|
3701
|
+
let screenRow = writeTop;
|
|
3347
3702
|
for (let index = 0; index < preparedHistory.lines.length; index++) {
|
|
3348
3703
|
if (screenRow > startTop) buffer += "\n";
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3704
|
+
append(
|
|
3705
|
+
this.#lineRewriteSequence(
|
|
3706
|
+
preparedHistory.rows[index]!,
|
|
3707
|
+
width,
|
|
3708
|
+
Math.min(screenRow, height - 1),
|
|
3709
|
+
this.#osc66SpacerGlyphWidth(preparedHistory.lines, index),
|
|
3710
|
+
{ blankRow: compactReplay },
|
|
3711
|
+
),
|
|
3356
3712
|
);
|
|
3357
3713
|
screenRow++;
|
|
3358
3714
|
}
|
|
3359
3715
|
for (let index = 0; index < rows; index++) {
|
|
3360
3716
|
if (screenRow > startTop) buffer += "\n";
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3717
|
+
append(
|
|
3718
|
+
this.#lineRewriteSequence(
|
|
3719
|
+
prepared.rows[index]!,
|
|
3720
|
+
width,
|
|
3721
|
+
Math.min(screenRow, height - 1),
|
|
3722
|
+
this.#osc66SpacerGlyphWidth(prepared.lines, index),
|
|
3723
|
+
{
|
|
3724
|
+
blankRow: compactReplay,
|
|
3725
|
+
imagePlacements: index < replayViewportRows ? undefined : this.#screenImagePlacements,
|
|
3726
|
+
imageRow: newTop + index,
|
|
3727
|
+
},
|
|
3728
|
+
),
|
|
3368
3729
|
);
|
|
3369
3730
|
screenRow++;
|
|
3370
3731
|
}
|
|
@@ -3389,6 +3750,14 @@ export class TUI extends Container {
|
|
|
3389
3750
|
this.#parkedViewportOffset = 0;
|
|
3390
3751
|
}
|
|
3391
3752
|
buffer += this.#paintEndSequence;
|
|
3753
|
+
if (
|
|
3754
|
+
renewSync &&
|
|
3755
|
+
pendingAltExit === "" &&
|
|
3756
|
+
this.#resizeReplaySize !== undefined &&
|
|
3757
|
+
Buffer.byteLength(buffer) >= TUI.#RESIZE_REBUILD_NOTICE_MIN_BYTES
|
|
3758
|
+
) {
|
|
3759
|
+
this.terminal.write(this.#rebuildNoticeSequence(width, height));
|
|
3760
|
+
}
|
|
3392
3761
|
this.terminal.write(buffer);
|
|
3393
3762
|
this.#debugPaint = {
|
|
3394
3763
|
lines: prepared.lines,
|
|
@@ -3399,6 +3768,7 @@ export class TUI extends Container {
|
|
|
3399
3768
|
if (pendingAltExit) {
|
|
3400
3769
|
this.#noteAltBufferToggle();
|
|
3401
3770
|
this.#pendingAltExit = "";
|
|
3771
|
+
this.#resizeAltExitFused = false;
|
|
3402
3772
|
setAltScreenActive(false);
|
|
3403
3773
|
}
|
|
3404
3774
|
if (target) this.#recordHardwareCursorState(target);
|
|
@@ -3406,12 +3776,28 @@ export class TUI extends Container {
|
|
|
3406
3776
|
this.#providerWindow = mutablePreparedLines;
|
|
3407
3777
|
this.#providerPreparedRows = mutablePreparedRows;
|
|
3408
3778
|
this.#providerViewportTop = mutableTop;
|
|
3779
|
+
// Track the un-committed scroll of a transiently grown frame so the
|
|
3780
|
+
// matching shrink can pull exactly those rows back. Every committing or
|
|
3781
|
+
// superseding frame zeroes the debt: history appends and replays own
|
|
3782
|
+
// their scrolls, a reset wipes the ledger, a new geometry reflows it,
|
|
3783
|
+
// and an overlay frame paints against full-screen padding.
|
|
3784
|
+
if (transientFrame) {
|
|
3785
|
+
const scrolledOff = Math.max(0, startTop + rows - height);
|
|
3786
|
+
this.#transientScrollPushed =
|
|
3787
|
+
Math.min(this.#transientScrollPushed + scrolledOff, Math.max(0, height - 1)) - anchorPull;
|
|
3788
|
+
if (this.#transientScrollPushed < 0) this.#transientScrollPushed = 0;
|
|
3789
|
+
} else {
|
|
3790
|
+
this.#transientScrollPushed = 0;
|
|
3791
|
+
}
|
|
3409
3792
|
this.#providerViewportPadTop = replayViewportRows - replayPrependedBlanks;
|
|
3410
3793
|
this.#previousWidth = width;
|
|
3411
3794
|
this.#previousHeight = height;
|
|
3412
3795
|
this.#resizeBurstGrew = false;
|
|
3796
|
+
this.#resizeBurstShrank = false;
|
|
3413
3797
|
this.#resizeBurstLastHeight = undefined;
|
|
3414
3798
|
this.#resizeBurstPull = 0;
|
|
3799
|
+
this.#resizeBurstWidthChanged = false;
|
|
3800
|
+
this.#resizeBurstResized = false;
|
|
3415
3801
|
this.#previousFrameLength = mutablePreparedLines.length;
|
|
3416
3802
|
this.#clearScrollbackOnNextRender = false;
|
|
3417
3803
|
this.#forceViewportRepaintOnNextRender = false;
|
|
@@ -3457,11 +3843,9 @@ export class TUI extends Container {
|
|
|
3457
3843
|
return;
|
|
3458
3844
|
}
|
|
3459
3845
|
if (this.#resizeInPlaceActive && !this.#altActive) {
|
|
3460
|
-
//
|
|
3461
|
-
//
|
|
3462
|
-
//
|
|
3463
|
-
// the frame — the resolve repaints. Fullscreen overlay paints are
|
|
3464
|
-
// buffer-isolated and still allowed.
|
|
3846
|
+
// The normal-buffer anchor is stale until the settled rebuild or CPR
|
|
3847
|
+
// recovery. Painting now would overwrite history and record the new
|
|
3848
|
+
// geometry over the pending recovery, so defer until the settle.
|
|
3465
3849
|
return;
|
|
3466
3850
|
}
|
|
3467
3851
|
|
|
@@ -3483,9 +3867,19 @@ export class TUI extends Container {
|
|
|
3483
3867
|
// modified-key reporting sequence on the freshly entered alternate
|
|
3484
3868
|
// screen, or Esc/modified keys revert to legacy encoding inside
|
|
3485
3869
|
// fullscreen overlays (Ghostty/kitty/iTerm2).
|
|
3486
|
-
this.#
|
|
3487
|
-
|
|
3488
|
-
|
|
3870
|
+
if (this.#resizeAltExitFused) {
|
|
3871
|
+
// An overlay opened while the settled resize rebuild was still
|
|
3872
|
+
// pending: the terminal never left the borrowed buffer, which already
|
|
3873
|
+
// carries the pushed keyboard mode and its image store, so the overlay
|
|
3874
|
+
// takes it over. The latched rebuild then lands with the overlay's
|
|
3875
|
+
// own fused exit when it closes.
|
|
3876
|
+
this.#pendingAltExit = "";
|
|
3877
|
+
this.#resizeAltExitFused = false;
|
|
3878
|
+
} else {
|
|
3879
|
+
this.#noteAltBufferToggle();
|
|
3880
|
+
this.#imageBudget.beginAltScreenLifecycle();
|
|
3881
|
+
this.terminal.write(`\x1b[?1049h${this.#keyboardEnhancementEnter()}`);
|
|
3882
|
+
}
|
|
3489
3883
|
this.#setMouseTracking(wantMouse);
|
|
3490
3884
|
setAltScreenActive(true);
|
|
3491
3885
|
this.terminal.hideCursor();
|
|
@@ -3589,6 +3983,7 @@ export class TUI extends Container {
|
|
|
3589
3983
|
if (this.#maybeDeferGhosttyInitialImagePaint()) return;
|
|
3590
3984
|
this.#emitPlanFrame(width, height, viewport, undefined, undefined);
|
|
3591
3985
|
this.#paintGutter(width, rawWidth, height, viewport.length);
|
|
3986
|
+
this.#previousPhysicalWidth = rawWidth;
|
|
3592
3987
|
}
|
|
3593
3988
|
|
|
3594
3989
|
/**
|
|
@@ -3965,9 +4360,8 @@ export class TUI extends Container {
|
|
|
3965
4360
|
line: PreparedLine,
|
|
3966
4361
|
width: number,
|
|
3967
4362
|
screenRow = -1,
|
|
3968
|
-
frameRow = -1,
|
|
3969
|
-
committedTo = -1,
|
|
3970
4363
|
spacerGlyphWidth = -1,
|
|
4364
|
+
options?: { blankRow?: boolean; imagePlacements?: Map<number, LiveImagePlacement>; imageRow?: number },
|
|
3971
4365
|
): string {
|
|
3972
4366
|
// End every rewrite at column zero. ConPTY can materialize a pending
|
|
3973
4367
|
// wrap before a following cursor-addressing sequence even while DECAWM is
|
|
@@ -3984,10 +4378,19 @@ export class TUI extends Container {
|
|
|
3984
4378
|
if (spacerGlyphWidth >= 0) {
|
|
3985
4379
|
rewrite = spacerGlyphWidth >= width ? "" : `${SEGMENT_RESET}\x1b[${spacerGlyphWidth}C${ERASE_TO_END_OF_LINE}`;
|
|
3986
4380
|
} else if (line.isImage) {
|
|
3987
|
-
rewrite =
|
|
4381
|
+
rewrite =
|
|
4382
|
+
ERASE_LINE +
|
|
4383
|
+
this.#imageLineSequence(line.line, screenRow, options?.imagePlacements, options?.imageRow ?? screenRow);
|
|
3988
4384
|
} else {
|
|
3989
4385
|
const terminalLine = this.#terminalLine(line);
|
|
3990
|
-
if (
|
|
4386
|
+
if (options?.blankRow) {
|
|
4387
|
+
// A destructive replay starts on a cleared screen and advances only
|
|
4388
|
+
// into fresh rows. Each line resets its rendition before scrolling,
|
|
4389
|
+
// so those rows already have the default background. Re-erasing every
|
|
4390
|
+
// row only adds terminal parser work; images and OSC 66 spacers keep
|
|
4391
|
+
// their dedicated cleanup above.
|
|
4392
|
+
rewrite = this.#compactReplaySpaces(terminalLine);
|
|
4393
|
+
} else if (line.asciiWidth !== undefined) {
|
|
3991
4394
|
// Exact width model: skip the erase only when the row truly fills
|
|
3992
4395
|
// the line (an EL there would eat the last cell via pending-wrap).
|
|
3993
4396
|
rewrite = line.asciiWidth >= width ? terminalLine : terminalLine + ERASE_TO_END_OF_LINE;
|
|
@@ -4052,9 +4455,9 @@ export class TUI extends Container {
|
|
|
4052
4455
|
|
|
4053
4456
|
/**
|
|
4054
4457
|
* Compose and paint a single fullscreen overlay frame on the alt buffer.
|
|
4055
|
-
* Cursor markers are stripped
|
|
4056
|
-
*
|
|
4057
|
-
*
|
|
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.
|
|
4058
4461
|
*/
|
|
4059
4462
|
#renderAltFrame(width: number, height: number): void {
|
|
4060
4463
|
// oxlint-disable-next-line unicorn/no-new-array -- alt-frame length preallocation
|
|
@@ -4064,8 +4467,18 @@ export class TUI extends Container {
|
|
|
4064
4467
|
this.#imageBudget.beginPass(false, true);
|
|
4065
4468
|
lines = this.#compositeOverlaysIntoWindow(base, width, height);
|
|
4066
4469
|
} while (this.#imageBudget.endPass());
|
|
4067
|
-
const
|
|
4068
|
-
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);
|
|
4069
4482
|
}
|
|
4070
4483
|
|
|
4071
4484
|
/**
|
|
@@ -4073,9 +4486,15 @@ export class TUI extends Container {
|
|
|
4073
4486
|
* previous frame, or every row when the height changed, a repaint is forced,
|
|
4074
4487
|
* or a changed frame holds OSC 66 text before or after. Emits only
|
|
4075
4488
|
* sync-output brackets, cursor moves, and per-row rewrites — never ED3 or
|
|
4076
|
-
* any native-scrollback byte.
|
|
4489
|
+
* any native-scrollback byte.
|
|
4077
4490
|
*/
|
|
4078
|
-
#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 {
|
|
4079
4498
|
// The pass that composed this frame ran with `altScreen`, so the normal
|
|
4080
4499
|
// screen's own placements behind it are not treated as retired.
|
|
4081
4500
|
this.#imageBudget.limitResidentImages();
|
|
@@ -4084,17 +4503,14 @@ export class TUI extends Container {
|
|
|
4084
4503
|
// frame resolve against loaded data. The normal-screen path flushes these
|
|
4085
4504
|
// ahead of its paint; without this, an image first shown inside a
|
|
4086
4505
|
// fullscreen overlay (e.g. the settings shape preview) would render as
|
|
4087
|
-
// 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();
|
|
4088
4509
|
const purgeIds = this.#imageBudget.takePurgeIds();
|
|
4089
4510
|
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
|
|
4090
|
-
for (const id of purgeIds)
|
|
4091
|
-
}
|
|
4092
|
-
const imageTransmits = this.#imageBudget.takeTransmits();
|
|
4093
|
-
if (imageTransmits.length > 0) {
|
|
4094
|
-
let transmitBuffer = "";
|
|
4095
|
-
for (const seq of imageTransmits) transmitBuffer += seq;
|
|
4096
|
-
this.terminal.write(transmitBuffer);
|
|
4511
|
+
for (const id of purgeIds) prelude += encodeKittyDeleteImage(id);
|
|
4097
4512
|
}
|
|
4513
|
+
for (const seq of this.#imageBudget.takeTransmits()) prelude += seq;
|
|
4098
4514
|
// A forced repaint (resetDisplay, requestRender(true)) rewrites every row
|
|
4099
4515
|
// even when the cached frame is byte-identical: the redraw gesture must
|
|
4100
4516
|
// repair a corrupted modal. So does a changed frame with OSC 66 text in
|
|
@@ -4111,6 +4527,7 @@ export class TUI extends Container {
|
|
|
4111
4527
|
prepared.rows.some((_row, r) =>
|
|
4112
4528
|
this.#rowNeedsRewrite(this.#altPreviousLines, this.#altPreparedRows, prepared.lines, prepared.rows, r),
|
|
4113
4529
|
));
|
|
4530
|
+
prelude += this.#retireImagePlacements(this.#altImagePlacements, prepared.lines, 0, full);
|
|
4114
4531
|
let rowsBuffer = "";
|
|
4115
4532
|
for (let r = 0; r < height; r++) {
|
|
4116
4533
|
if (full) {
|
|
@@ -4126,17 +4543,44 @@ export class TUI extends Container {
|
|
|
4126
4543
|
prepared.rows[r]!,
|
|
4127
4544
|
width,
|
|
4128
4545
|
r,
|
|
4129
|
-
-1,
|
|
4130
|
-
-1,
|
|
4131
4546
|
this.#osc66SpacerGlyphWidth(prepared.lines, r),
|
|
4547
|
+
{ imagePlacements: this.#altImagePlacements },
|
|
4132
4548
|
);
|
|
4133
4549
|
}
|
|
4134
4550
|
this.#altPreviousLines = prepared.lines;
|
|
4135
4551
|
this.#altPreparedRows = prepared.rows;
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
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 !== "") {
|
|
4140
4584
|
this.#notifyPaint({
|
|
4141
4585
|
history: [],
|
|
4142
4586
|
viewport: prepared.lines,
|
|
@@ -4146,6 +4590,11 @@ export class TUI extends Container {
|
|
|
4146
4590
|
rows: height,
|
|
4147
4591
|
});
|
|
4148
4592
|
}
|
|
4149
|
-
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();
|
|
4150
4599
|
}
|
|
4151
4600
|
}
|