omk-tui 1.3.2 → 1.4.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 +23 -0
- package/dist/components/box.d.ts +5 -0
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +50 -15
- package/dist/components/box.js.map +1 -1
- package/dist/components/image.d.ts +2 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +4 -0
- package/dist/components/image.js.map +1 -1
- package/dist/components/markdown.d.ts +4 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +14 -0
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/spacer.d.ts +2 -0
- package/dist/components/spacer.d.ts.map +1 -1
- package/dist/components/spacer.js +8 -0
- package/dist/components/spacer.js.map +1 -1
- package/dist/components/text.d.ts +4 -0
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +14 -0
- package/dist/components/text.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/line-reset-memo.d.ts +27 -0
- package/dist/line-reset-memo.d.ts.map +1 -0
- package/dist/line-reset-memo.js +101 -0
- package/dist/line-reset-memo.js.map +1 -0
- package/dist/render-generation.d.ts +24 -0
- package/dist/render-generation.d.ts.map +1 -0
- package/dist/render-generation.js +48 -0
- package/dist/render-generation.js.map +1 -0
- package/dist/terminal-output.d.ts +20 -1
- package/dist/terminal-output.d.ts.map +1 -1
- package/dist/terminal-output.js +62 -1
- package/dist/terminal-output.js.map +1 -1
- package/dist/terminal-resize-log.d.ts +39 -0
- package/dist/terminal-resize-log.d.ts.map +1 -0
- package/dist/terminal-resize-log.js +57 -0
- package/dist/terminal-resize-log.js.map +1 -0
- package/dist/terminal-resync.d.ts +74 -0
- package/dist/terminal-resync.d.ts.map +1 -0
- package/dist/terminal-resync.js +112 -0
- package/dist/terminal-resync.js.map +1 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +1 -1
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +29 -9
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +63 -85
- package/dist/tui.js.map +1 -1
- package/dist/undo-stack.d.ts +8 -2
- package/dist/undo-stack.d.ts.map +1 -1
- package/dist/undo-stack.js +12 -2
- package/dist/undo-stack.js.map +1 -1
- package/dist/utils.d.ts +2 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -128
- package/dist/utils.js.map +1 -1
- package/dist/visible-width.d.ts +14 -0
- package/dist/visible-width.d.ts.map +1 -0
- package/dist/visible-width.js +163 -0
- package/dist/visible-width.js.map +1 -0
- package/dist/windowed-container.d.ts +63 -0
- package/dist/windowed-container.d.ts.map +1 -0
- package/dist/windowed-container.js +259 -0
- package/dist/windowed-container.js.map +1 -0
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -6,30 +6,12 @@ import * as os from "node:os";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
8
8
|
import { isKeyRelease, matchesKey } from "./keys.js";
|
|
9
|
+
import { extractKittyImageIds, LineResetMemo, SEGMENT_RESET } from "./line-reset-memo.js";
|
|
10
|
+
import { maxChildGeneration, nextRenderGeneration, releaseRenderCache } from "./render-generation.js";
|
|
9
11
|
import { finishTerminalFrame } from "./terminal-final-frame.js";
|
|
10
12
|
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
function extractKittyImageIds(line) {
|
|
14
|
-
const sequenceStart = line.indexOf(KITTY_SEQUENCE_PREFIX);
|
|
15
|
-
if (sequenceStart === -1)
|
|
16
|
-
return [];
|
|
17
|
-
const paramsStart = sequenceStart + KITTY_SEQUENCE_PREFIX.length;
|
|
18
|
-
const paramsEnd = line.indexOf(";", paramsStart);
|
|
19
|
-
if (paramsEnd === -1)
|
|
20
|
-
return [];
|
|
21
|
-
const params = line.slice(paramsStart, paramsEnd);
|
|
22
|
-
for (const param of params.split(",")) {
|
|
23
|
-
const [key, value] = param.split("=", 2);
|
|
24
|
-
if (key !== "i" || value === undefined)
|
|
25
|
-
continue;
|
|
26
|
-
const id = Number(value);
|
|
27
|
-
if (Number.isInteger(id) && id > 0 && id <= 0xffffffff) {
|
|
28
|
-
return [id];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return [];
|
|
32
|
-
}
|
|
13
|
+
import { isTermuxSession, REPAINT_BUDGET_SCREENS, ResizeResync } from "./terminal-resync.js";
|
|
14
|
+
import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
|
33
15
|
/** Type guard to check if a component implements Focusable */
|
|
34
16
|
export function isFocusable(component) {
|
|
35
17
|
return component !== null && "focused" in component;
|
|
@@ -44,51 +26,55 @@ export const CURSOR_MARKER = "\x1b_pi:c\x07";
|
|
|
44
26
|
export { visibleWidth };
|
|
45
27
|
/** Parse a SizeValue into absolute value given a reference size */
|
|
46
28
|
function parseSizeValue(value, referenceSize) {
|
|
47
|
-
if (value === undefined)
|
|
48
|
-
return undefined;
|
|
49
|
-
if (typeof value === "number")
|
|
29
|
+
if (value === undefined || typeof value === "number")
|
|
50
30
|
return value;
|
|
51
31
|
// Parse percentage string like "50%"
|
|
52
32
|
const match = value.match(/^(\d+(?:\.\d+)?)%$/);
|
|
53
|
-
|
|
54
|
-
return Math.floor((referenceSize * parseFloat(match[1])) / 100);
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
function isTermuxSession() {
|
|
59
|
-
return Boolean(process.env.TERMUX_VERSION);
|
|
33
|
+
return match ? Math.floor((referenceSize * parseFloat(match[1])) / 100) : undefined;
|
|
60
34
|
}
|
|
61
35
|
/**
|
|
62
36
|
* Container - a component that contains other components
|
|
63
37
|
*/
|
|
64
38
|
export class Container {
|
|
65
39
|
children = [];
|
|
40
|
+
/** Stamp of the last structural change (see render-generation.ts). */
|
|
41
|
+
structureGeneration = 0;
|
|
66
42
|
addChild(component) {
|
|
67
43
|
this.children.push(component);
|
|
44
|
+
this.structureGeneration = nextRenderGeneration();
|
|
68
45
|
}
|
|
69
46
|
removeChild(component) {
|
|
70
47
|
const index = this.children.indexOf(component);
|
|
71
48
|
if (index !== -1) {
|
|
72
49
|
this.children.splice(index, 1);
|
|
50
|
+
this.structureGeneration = nextRenderGeneration();
|
|
73
51
|
}
|
|
74
52
|
}
|
|
75
53
|
clear() {
|
|
76
54
|
this.children = [];
|
|
55
|
+
this.structureGeneration = nextRenderGeneration();
|
|
56
|
+
}
|
|
57
|
+
/** Raises whenever this container's child list or any descendant changes. */
|
|
58
|
+
getRenderGeneration() {
|
|
59
|
+
return maxChildGeneration(this.structureGeneration, this.children);
|
|
77
60
|
}
|
|
78
61
|
invalidate() {
|
|
79
62
|
for (const child of this.children) {
|
|
80
63
|
child.invalidate?.();
|
|
81
64
|
}
|
|
82
65
|
}
|
|
66
|
+
/** Forwards to children; a container keeps no render cache of its own. */
|
|
67
|
+
releaseRenderCache() {
|
|
68
|
+
for (const child of this.children)
|
|
69
|
+
releaseRenderCache(child);
|
|
70
|
+
}
|
|
83
71
|
render(width) {
|
|
84
|
-
const
|
|
85
|
-
for (const child of this.children)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
return lines;
|
|
72
|
+
const parts = [];
|
|
73
|
+
for (const child of this.children)
|
|
74
|
+
parts.push(child.render(width));
|
|
75
|
+
// Always a fresh array (callers mutate it). Native concat bulk-copies large
|
|
76
|
+
// children such as a windowed transcript instead of pushing row by row.
|
|
77
|
+
return parts.length < 8192 ? [].concat(...parts) : parts.flat();
|
|
92
78
|
}
|
|
93
79
|
}
|
|
94
80
|
/**
|
|
@@ -98,6 +84,7 @@ export class TUI extends Container {
|
|
|
98
84
|
terminal;
|
|
99
85
|
previousLines = [];
|
|
100
86
|
previousKittyImageIds = new Set();
|
|
87
|
+
lineResets = new LineResetMemo();
|
|
101
88
|
previousWidth = 0;
|
|
102
89
|
previousHeight = 0;
|
|
103
90
|
focusedComponent = null;
|
|
@@ -108,12 +95,6 @@ export class TUI extends Container {
|
|
|
108
95
|
renderTimer;
|
|
109
96
|
lastRenderAt = 0;
|
|
110
97
|
static MIN_RENDER_INTERVAL_MS = 16;
|
|
111
|
-
/**
|
|
112
|
-
* How far back a clearing repair repaint may reach, in viewport screens.
|
|
113
|
-
* Bounds the scrollback churn of fixing rows that already scrolled off
|
|
114
|
-
* (see the repaint budget note in doRender's fullRender helper).
|
|
115
|
-
*/
|
|
116
|
-
static REPAINT_BUDGET_SCREENS = 4;
|
|
117
98
|
cursorRow = 0; // Logical cursor row (end of rendered content)
|
|
118
99
|
hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
|
|
119
100
|
showHardwareCursor = process.env.OMK_HARDWARE_CURSOR === "1";
|
|
@@ -123,6 +104,7 @@ export class TUI extends Container {
|
|
|
123
104
|
static MIN_CONTENT_COLUMNS = 20;
|
|
124
105
|
maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
|
|
125
106
|
previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
107
|
+
resizeResync = new ResizeResync(); // pre-overlay rows of recent frames, for height resizes
|
|
126
108
|
fullRedrawCount = 0;
|
|
127
109
|
stopped = false;
|
|
128
110
|
// Overlay stack for modal components rendered on top of base content
|
|
@@ -139,6 +121,10 @@ export class TUI extends Container {
|
|
|
139
121
|
get fullRedraws() {
|
|
140
122
|
return this.fullRedrawCount;
|
|
141
123
|
}
|
|
124
|
+
/** First buffer row inside the viewport as of the last render (0 while the whole frame fits). */
|
|
125
|
+
get viewportTop() {
|
|
126
|
+
return this.previousViewportTop;
|
|
127
|
+
}
|
|
142
128
|
getShowHardwareCursor() {
|
|
143
129
|
return this.showHardwareCursor;
|
|
144
130
|
}
|
|
@@ -807,26 +793,6 @@ export class TUI extends Container {
|
|
|
807
793
|
}
|
|
808
794
|
return result;
|
|
809
795
|
}
|
|
810
|
-
static SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07";
|
|
811
|
-
applyLineResets(lines) {
|
|
812
|
-
const reset = TUI.SEGMENT_RESET;
|
|
813
|
-
for (let i = 0; i < lines.length; i++) {
|
|
814
|
-
const line = lines[i];
|
|
815
|
-
if (!isImageLine(line)) {
|
|
816
|
-
lines[i] = normalizeTerminalOutput(line) + reset;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
return lines;
|
|
820
|
-
}
|
|
821
|
-
collectKittyImageIds(lines) {
|
|
822
|
-
const ids = new Set();
|
|
823
|
-
for (const line of lines) {
|
|
824
|
-
for (const id of extractKittyImageIds(line)) {
|
|
825
|
-
ids.add(id);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
return ids;
|
|
829
|
-
}
|
|
830
796
|
deleteKittyImages(ids) {
|
|
831
797
|
let buffer = "";
|
|
832
798
|
for (const id of ids) {
|
|
@@ -835,6 +801,10 @@ export class TUI extends Container {
|
|
|
835
801
|
return buffer;
|
|
836
802
|
}
|
|
837
803
|
expandLastChangedForKittyImages(firstChanged, lastChanged) {
|
|
804
|
+
// previousKittyImageIds is the id set of exactly these previousLines: when it
|
|
805
|
+
// is empty no row can match, so skip the O(history) scan.
|
|
806
|
+
if (this.previousKittyImageIds.size === 0)
|
|
807
|
+
return lastChanged;
|
|
838
808
|
let expandedLastChanged = lastChanged;
|
|
839
809
|
for (let i = firstChanged; i < this.previousLines.length; i++) {
|
|
840
810
|
if (extractKittyImageIds(this.previousLines[i]).length > 0) {
|
|
@@ -872,7 +842,7 @@ export class TUI extends Container {
|
|
|
872
842
|
const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
|
|
873
843
|
const afterPad = Math.max(0, afterTarget - base.afterWidth);
|
|
874
844
|
// Compose result
|
|
875
|
-
const r =
|
|
845
|
+
const r = SEGMENT_RESET;
|
|
876
846
|
const result = base.before +
|
|
877
847
|
" ".repeat(beforePad) +
|
|
878
848
|
r +
|
|
@@ -938,20 +908,22 @@ export class TUI extends Container {
|
|
|
938
908
|
// Render all components to get new lines. Children render at the content
|
|
939
909
|
// width (terminal minus the reserved right gutter); overlays composite at
|
|
940
910
|
// full-terminal coordinates below, so pinned rails own the gutter columns.
|
|
941
|
-
let newLines = finalLines ?? this.render(width - this.resolveRightGutter(width, height));
|
|
911
|
+
let newLines = this.resizeResync.track(finalLines ?? this.render(width - this.resolveRightGutter(width, height)));
|
|
942
912
|
// Composite overlays into the rendered lines (before differential compare)
|
|
943
913
|
if (finalLines === undefined && this.overlayStack.length > 0) {
|
|
944
914
|
newLines = this.compositeOverlays(newLines, width, height);
|
|
945
915
|
}
|
|
946
916
|
// Extract cursor position before applying line resets (marker must be found first)
|
|
947
917
|
const cursorPos = this.extractCursorPosition(newLines, height);
|
|
948
|
-
newLines = this.
|
|
918
|
+
newLines = this.lineResets.apply(newLines);
|
|
949
919
|
// Helper to optionally clear the visible viewport and render all new lines
|
|
950
|
-
const fullRender = (clear, fromRow) => {
|
|
920
|
+
const fullRender = (clear, fromRow, resync = "") => {
|
|
951
921
|
this.fullRedrawCount += 1;
|
|
952
922
|
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
|
953
923
|
if (clear) {
|
|
954
|
-
|
|
924
|
+
// A height resize first rewrites, relative to the cursor, the rows
|
|
925
|
+
// the resize may have cost scrollback (see resyncAfterResize).
|
|
926
|
+
buffer += this.deleteKittyImages(this.previousKittyImageIds) + resync;
|
|
955
927
|
// Repaint the visible screen IN PLACE: home the cursor, erase every row
|
|
956
928
|
// as it is rewritten below, then erase whatever is left over.
|
|
957
929
|
// \x1b[2J (erase all) is deliberately not used: conpty/Windows Terminal
|
|
@@ -987,7 +959,7 @@ export class TUI extends Container {
|
|
|
987
959
|
// destroys more history than it repairs (a long report loses its
|
|
988
960
|
// beginning). Rows older than the budget keep their existing copy.
|
|
989
961
|
// Without fromRow the clamp collapses to tailStart, i.e. tail-only.
|
|
990
|
-
const repairStart = newLines.length - height *
|
|
962
|
+
const repairStart = newLines.length - height * REPAINT_BUDGET_SCREENS;
|
|
991
963
|
const firstPrinted = clear ? Math.min(Math.max(fromRow ?? tailStart, repairStart), tailStart) : 0;
|
|
992
964
|
for (let i = firstPrinted; i < newLines.length; i++) {
|
|
993
965
|
if (i > firstPrinted)
|
|
@@ -1010,13 +982,12 @@ export class TUI extends Container {
|
|
|
1010
982
|
this.previousViewportTop = Math.max(0, bufferLength - height);
|
|
1011
983
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1012
984
|
this.previousLines = newLines;
|
|
1013
|
-
this.previousKittyImageIds = this.
|
|
985
|
+
this.previousKittyImageIds = this.lineResets.kittyImageIds;
|
|
1014
986
|
this.previousWidth = width;
|
|
1015
987
|
this.previousHeight = height;
|
|
1016
988
|
};
|
|
1017
|
-
const debugRedraw = process.env.OMK_DEBUG_REDRAW === "1";
|
|
1018
989
|
const logRedraw = (reason) => {
|
|
1019
|
-
if (
|
|
990
|
+
if (process.env.OMK_DEBUG_REDRAW !== "1")
|
|
1020
991
|
return;
|
|
1021
992
|
const agentDir = process.env.OMK_CODING_AGENT_DIR ?? path.join(os.homedir(), ".omk", "agent");
|
|
1022
993
|
const logPath = path.join(agentDir, "omk-debug.log");
|
|
@@ -1039,22 +1010,29 @@ export class TUI extends Container {
|
|
|
1039
1010
|
// but Termux changes height when the software keyboard shows or hides.
|
|
1040
1011
|
// In that environment, a full redraw causes the entire history to replay on every toggle.
|
|
1041
1012
|
if (heightChanged && !isTermuxSession()) {
|
|
1042
|
-
|
|
1043
|
-
|
|
1013
|
+
const resync = this.resizeResync.after(newLines, this.hardwareCursorRow, height);
|
|
1014
|
+
logRedraw(`terminal height changed (${this.previousHeight} -> ${height}; ${this.resizeResync.detail})`);
|
|
1015
|
+
fullRender(true, undefined, resync);
|
|
1044
1016
|
return;
|
|
1045
1017
|
}
|
|
1046
1018
|
// Find first and last changed lines
|
|
1019
|
+
// Scan inward from both ends: unchanged rows compare by reference, and a
|
|
1020
|
+
// change far above the viewport (which shifts every row below it) no longer
|
|
1021
|
+
// costs a content comparison per history row.
|
|
1047
1022
|
let firstChanged = -1;
|
|
1048
1023
|
let lastChanged = -1;
|
|
1049
1024
|
const maxLines = Math.max(newLines.length, this.previousLines.length);
|
|
1025
|
+
const rowDiffers = (i) => (i < this.previousLines.length ? this.previousLines[i] : "") !== (i < newLines.length ? newLines[i] : "");
|
|
1050
1026
|
for (let i = 0; i < maxLines; i++) {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1027
|
+
if (rowDiffers(i)) {
|
|
1028
|
+
firstChanged = i;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
for (let i = maxLines - 1; firstChanged !== -1 && i >= firstChanged; i--) {
|
|
1033
|
+
if (rowDiffers(i)) {
|
|
1057
1034
|
lastChanged = i;
|
|
1035
|
+
break;
|
|
1058
1036
|
}
|
|
1059
1037
|
}
|
|
1060
1038
|
if (firstChanged !== -1) {
|
|
@@ -1134,7 +1112,7 @@ export class TUI extends Container {
|
|
|
1134
1112
|
}
|
|
1135
1113
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1136
1114
|
this.previousLines = newLines;
|
|
1137
|
-
this.previousKittyImageIds = this.
|
|
1115
|
+
this.previousKittyImageIds = this.lineResets.kittyImageIds;
|
|
1138
1116
|
this.previousWidth = width;
|
|
1139
1117
|
this.previousHeight = height;
|
|
1140
1118
|
this.previousViewportTop = prevViewportTop;
|
|
@@ -1270,7 +1248,7 @@ export class TUI extends Container {
|
|
|
1270
1248
|
// Position hardware cursor for IME
|
|
1271
1249
|
this.positionHardwareCursor(cursorPos, newLines.length);
|
|
1272
1250
|
this.previousLines = newLines;
|
|
1273
|
-
this.previousKittyImageIds = this.
|
|
1251
|
+
this.previousKittyImageIds = this.lineResets.kittyImageIds;
|
|
1274
1252
|
this.previousWidth = width;
|
|
1275
1253
|
this.previousHeight = height;
|
|
1276
1254
|
}
|