@zgeoff/atc 1.0.0 → 1.0.2
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/package.json +1 -1
- package/src/client/plan-vacated-rows.ts +26 -0
- package/src/client/ui.ts +14 -0
- package/src/daemon/daemon.ts +6 -1
- package/src/daemon/screen-model.ts +12 -3
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface BoxExtent {
|
|
2
|
+
readonly top: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The screen rows the previous box covered that the next one does not. A
|
|
8
|
+
* centered box shifts and shrinks as its row count changes, so a redraw
|
|
9
|
+
* erases these rows first — otherwise the old box's edges linger as ghost
|
|
10
|
+
* lines above and below the new one.
|
|
11
|
+
*/
|
|
12
|
+
export function planVacatedRows(prev: BoxExtent | null, next: BoxExtent): number[] {
|
|
13
|
+
if (prev === null) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const vacated: number[] = [];
|
|
18
|
+
|
|
19
|
+
for (let row = prev.top; row < prev.top + prev.height; row++) {
|
|
20
|
+
if (row < next.top || row >= next.top + next.height) {
|
|
21
|
+
vacated.push(row);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return vacated;
|
|
26
|
+
}
|
package/src/client/ui.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { SessionState } from '../daemon/sessions';
|
|
|
4
4
|
import { RESET_INPUT_MODES } from '../shared/reset-input-modes';
|
|
5
5
|
import { formatDir } from './dirs';
|
|
6
6
|
import { formatOverlayAgentMark } from './format-overlay-agent-mark';
|
|
7
|
+
import { planVacatedRows } from './plan-vacated-rows';
|
|
8
|
+
import type { BoxExtent } from './plan-vacated-rows';
|
|
7
9
|
|
|
8
10
|
// The slice of a session the drawing layer needs; satisfied by both the
|
|
9
11
|
// daemon's sessions and the wire descriptors a client mirrors.
|
|
@@ -119,12 +121,24 @@ interface Row {
|
|
|
119
121
|
readonly width: number;
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
// The extent of the last box drawn, so the next draw can erase the rows a
|
|
125
|
+
// shrinking or shifting box leaves behind. Erasing an already-blank row is a
|
|
126
|
+
// no-op, so a stale extent after a full clear is harmless.
|
|
127
|
+
let lastBoxExtent: BoxExtent | null = null;
|
|
128
|
+
|
|
122
129
|
function drawBox(rowsList: readonly Row[]) {
|
|
123
130
|
const boxWidth = Math.max(...rowsList.map((r) => r.width));
|
|
124
131
|
const top = Math.max(1, Math.floor((rows() - 1 - rowsList.length) / 2));
|
|
125
132
|
const left = Math.max(1, Math.floor((cols() - boxWidth) / 2));
|
|
133
|
+
const extent: BoxExtent = { top, height: rowsList.length };
|
|
126
134
|
let buf = ansi.hideCursor;
|
|
127
135
|
|
|
136
|
+
for (const row of planVacatedRows(lastBoxExtent, extent)) {
|
|
137
|
+
buf += `${ansi.moveTo(row, 1)}${ESC}[2K`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
lastBoxExtent = extent;
|
|
141
|
+
|
|
128
142
|
for (const [i, r] of rowsList.entries()) {
|
|
129
143
|
buf += ansi.moveTo(top + i, left) + r.styled;
|
|
130
144
|
}
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -481,7 +481,12 @@ export async function startDaemon(opts: DaemonOptions): Promise<DaemonHandle> {
|
|
|
481
481
|
attachments.attach(sessionID, client, dims);
|
|
482
482
|
mgr.attach(sessionID);
|
|
483
483
|
|
|
484
|
-
|
|
484
|
+
// The PTY and screen model take the new effective size before the
|
|
485
|
+
// replay renders, so the repaint arrives at the attaching terminal's
|
|
486
|
+
// own dims instead of the previous attachment's — a replay at stale
|
|
487
|
+
// dims paints vertically misaligned and the agent's resize repaint
|
|
488
|
+
// only redraws its live region, never the rows above it.
|
|
489
|
+
applyEffectiveDims(sessionID);
|
|
485
490
|
void sendReplay(sessionID, client);
|
|
486
491
|
|
|
487
492
|
return 'ok';
|
|
@@ -84,10 +84,19 @@ export class ScreenModel {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
// The terminal parses asynchronously
|
|
88
|
-
//
|
|
87
|
+
// The terminal parses asynchronously, and bytes recorded while a flush is
|
|
88
|
+
// awaited re-arm it — so the replay drains until no newer write is
|
|
89
|
+
// pending. Serializing earlier would omit bytes already streamed live to
|
|
90
|
+
// clients, and the replay's leading clear would erase them from the
|
|
91
|
+
// client's screen for good.
|
|
89
92
|
async renderReplay(): Promise<string> {
|
|
90
|
-
|
|
93
|
+
let pending: Promise<void>;
|
|
94
|
+
|
|
95
|
+
do {
|
|
96
|
+
pending = this.flushed;
|
|
97
|
+
|
|
98
|
+
await pending;
|
|
99
|
+
} while (pending !== this.flushed);
|
|
91
100
|
|
|
92
101
|
return RESET_INPUT_MODES + this.serializer.serialize() + this.renderInputModes();
|
|
93
102
|
}
|