blit-react 0.1.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/dist/BlitTerminal.d.ts +21 -0
- package/dist/BlitTerminal.d.ts.map +1 -0
- package/dist/BlitTerminal.js +910 -0
- package/dist/BlitTerminal.js.map +1 -0
- package/dist/hooks/useBlitConnection.d.ts +35 -0
- package/dist/hooks/useBlitConnection.d.ts.map +1 -0
- package/dist/hooks/useBlitConnection.js +183 -0
- package/dist/hooks/useBlitConnection.js.map +1 -0
- package/dist/hooks/useBlitTerminal.d.ts +40 -0
- package/dist/hooks/useBlitTerminal.d.ts.map +1 -0
- package/dist/hooks/useBlitTerminal.js +72 -0
- package/dist/hooks/useBlitTerminal.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/transports/websocket.d.ts +41 -0
- package/dist/transports/websocket.d.ts.map +1 -0
- package/dist/transports/websocket.js +131 -0
- package/dist/transports/websocket.js.map +1 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from 'react';
|
|
3
|
+
import { C2S_ACK } from './types';
|
|
4
|
+
import { useBlitConnection } from './hooks/useBlitConnection';
|
|
5
|
+
import { measureCell } from './hooks/useBlitTerminal';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Constants
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const DEFAULT_FONT = '"PragmataPro Liga", "PragmataPro", ui-monospace, monospace';
|
|
10
|
+
const DEFAULT_FONT_SIZE = 13;
|
|
11
|
+
const BG_OP_STRIDE = 4;
|
|
12
|
+
const GLYPH_OP_STRIDE = 8;
|
|
13
|
+
const MAX_BATCH_VERTS = 65532;
|
|
14
|
+
const MAX_ACK_AHEAD = 2;
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Shader sources (ported verbatim from web/index.html)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const RECT_VS = `
|
|
19
|
+
attribute vec2 a_pos;
|
|
20
|
+
attribute vec4 a_color;
|
|
21
|
+
uniform vec2 u_resolution;
|
|
22
|
+
varying vec4 v_color;
|
|
23
|
+
|
|
24
|
+
void main() {
|
|
25
|
+
vec2 zeroToOne = a_pos / u_resolution;
|
|
26
|
+
vec2 zeroToTwo = zeroToOne * 2.0;
|
|
27
|
+
vec2 clip = zeroToTwo - 1.0;
|
|
28
|
+
gl_Position = vec4(clip * vec2(1.0, -1.0), 0.0, 1.0);
|
|
29
|
+
v_color = a_color;
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
const RECT_FS = `
|
|
33
|
+
precision mediump float;
|
|
34
|
+
varying vec4 v_color;
|
|
35
|
+
|
|
36
|
+
void main() {
|
|
37
|
+
gl_FragColor = vec4(v_color.rgb * v_color.a, v_color.a);
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
const GLYPH_VS = `
|
|
41
|
+
attribute vec2 a_pos;
|
|
42
|
+
attribute vec2 a_uv;
|
|
43
|
+
attribute vec4 a_color;
|
|
44
|
+
uniform vec2 u_resolution;
|
|
45
|
+
varying vec2 v_uv;
|
|
46
|
+
varying vec4 v_color;
|
|
47
|
+
|
|
48
|
+
void main() {
|
|
49
|
+
vec2 zeroToOne = a_pos / u_resolution;
|
|
50
|
+
vec2 zeroToTwo = zeroToOne * 2.0;
|
|
51
|
+
vec2 clip = zeroToTwo - 1.0;
|
|
52
|
+
gl_Position = vec4(clip * vec2(1.0, -1.0), 0.0, 1.0);
|
|
53
|
+
v_uv = a_uv;
|
|
54
|
+
v_color = a_color;
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
const GLYPH_FS = `
|
|
58
|
+
precision mediump float;
|
|
59
|
+
varying vec2 v_uv;
|
|
60
|
+
varying vec4 v_color;
|
|
61
|
+
uniform sampler2D u_texture;
|
|
62
|
+
|
|
63
|
+
void main() {
|
|
64
|
+
vec4 tex = texture2D(u_texture, v_uv);
|
|
65
|
+
float minC = min(tex.r, min(tex.g, tex.b));
|
|
66
|
+
float maxC = max(tex.r, max(tex.g, tex.b));
|
|
67
|
+
float isGray = step(maxC - minC, 0.02);
|
|
68
|
+
vec3 tinted = v_color.rgb * tex.a;
|
|
69
|
+
gl_FragColor = vec4(mix(tex.rgb, tinted, isGray), tex.a);
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// GL helpers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
function compileShader(gl, type, source) {
|
|
76
|
+
const shader = gl.createShader(type);
|
|
77
|
+
if (!shader)
|
|
78
|
+
return null;
|
|
79
|
+
gl.shaderSource(shader, source);
|
|
80
|
+
gl.compileShader(shader);
|
|
81
|
+
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS))
|
|
82
|
+
return shader;
|
|
83
|
+
gl.deleteShader(shader);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function createProgram(gl, vs, fs) {
|
|
87
|
+
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vs);
|
|
88
|
+
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fs);
|
|
89
|
+
if (!vertexShader || !fragmentShader)
|
|
90
|
+
return null;
|
|
91
|
+
const program = gl.createProgram();
|
|
92
|
+
if (!program)
|
|
93
|
+
return null;
|
|
94
|
+
gl.attachShader(program, vertexShader);
|
|
95
|
+
gl.attachShader(program, fragmentShader);
|
|
96
|
+
gl.linkProgram(program);
|
|
97
|
+
gl.deleteShader(vertexShader);
|
|
98
|
+
gl.deleteShader(fragmentShader);
|
|
99
|
+
if (gl.getProgramParameter(program, gl.LINK_STATUS))
|
|
100
|
+
return program;
|
|
101
|
+
gl.deleteProgram(program);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function createGlRenderer(canvas) {
|
|
105
|
+
const gl = canvas.getContext('webgl', {
|
|
106
|
+
alpha: true,
|
|
107
|
+
antialias: false,
|
|
108
|
+
depth: false,
|
|
109
|
+
stencil: false,
|
|
110
|
+
premultipliedAlpha: true,
|
|
111
|
+
preserveDrawingBuffer: false,
|
|
112
|
+
});
|
|
113
|
+
if (!gl) {
|
|
114
|
+
return {
|
|
115
|
+
supported: false,
|
|
116
|
+
resize() { },
|
|
117
|
+
render() { },
|
|
118
|
+
dispose() { },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const rectProgram = createProgram(gl, RECT_VS, RECT_FS);
|
|
122
|
+
const glyphProgram = createProgram(gl, GLYPH_VS, GLYPH_FS);
|
|
123
|
+
if (!rectProgram || !glyphProgram) {
|
|
124
|
+
return {
|
|
125
|
+
supported: false,
|
|
126
|
+
resize() { },
|
|
127
|
+
render() { },
|
|
128
|
+
dispose() { },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const rectBuffer = gl.createBuffer();
|
|
132
|
+
const glyphBuffer = gl.createBuffer();
|
|
133
|
+
const atlasTexture = gl.createTexture();
|
|
134
|
+
const rectPosLoc = gl.getAttribLocation(rectProgram, 'a_pos');
|
|
135
|
+
const rectColorLoc = gl.getAttribLocation(rectProgram, 'a_color');
|
|
136
|
+
const rectResLoc = gl.getUniformLocation(rectProgram, 'u_resolution');
|
|
137
|
+
const glyphPosLoc = gl.getAttribLocation(glyphProgram, 'a_pos');
|
|
138
|
+
const glyphUvLoc = gl.getAttribLocation(glyphProgram, 'a_uv');
|
|
139
|
+
const glyphColorLoc = gl.getAttribLocation(glyphProgram, 'a_color');
|
|
140
|
+
const glyphResLoc = gl.getUniformLocation(glyphProgram, 'u_resolution');
|
|
141
|
+
const glyphTexLoc = gl.getUniformLocation(glyphProgram, 'u_texture');
|
|
142
|
+
let uploadedAtlasVersion = -1;
|
|
143
|
+
let uploadedAtlasCanvas = null;
|
|
144
|
+
gl.disable(gl.DEPTH_TEST);
|
|
145
|
+
gl.disable(gl.CULL_FACE);
|
|
146
|
+
gl.enable(gl.BLEND);
|
|
147
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
148
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
149
|
+
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
|
|
150
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
151
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
152
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
153
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
154
|
+
function ensureAtlas(atlasCanvas, version) {
|
|
155
|
+
if (atlasCanvas === uploadedAtlasCanvas &&
|
|
156
|
+
(version >>> 0) === (uploadedAtlasVersion >>> 0)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
|
|
160
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlasCanvas);
|
|
161
|
+
uploadedAtlasCanvas = atlasCanvas;
|
|
162
|
+
uploadedAtlasVersion = version >>> 0;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
function drawColoredTriangles(data) {
|
|
166
|
+
if (!data.length)
|
|
167
|
+
return;
|
|
168
|
+
const floatsPerVert = 6;
|
|
169
|
+
const totalVerts = data.length / floatsPerVert;
|
|
170
|
+
gl.useProgram(rectProgram);
|
|
171
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, rectBuffer);
|
|
172
|
+
gl.enableVertexAttribArray(rectPosLoc);
|
|
173
|
+
gl.enableVertexAttribArray(rectColorLoc);
|
|
174
|
+
gl.uniform2f(rectResLoc, canvas.width, canvas.height);
|
|
175
|
+
for (let off = 0; off < totalVerts; off += MAX_BATCH_VERTS) {
|
|
176
|
+
const count = Math.min(MAX_BATCH_VERTS, totalVerts - off);
|
|
177
|
+
const slice = data.subarray(off * floatsPerVert, (off + count) * floatsPerVert);
|
|
178
|
+
gl.bufferData(gl.ARRAY_BUFFER, slice, gl.DYNAMIC_DRAW);
|
|
179
|
+
gl.vertexAttribPointer(rectPosLoc, 2, gl.FLOAT, false, 24, 0);
|
|
180
|
+
gl.vertexAttribPointer(rectColorLoc, 4, gl.FLOAT, false, 24, 8);
|
|
181
|
+
gl.drawArrays(gl.TRIANGLES, 0, count);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function drawSolidRect(x1, y1, x2, y2, r, g, b, a) {
|
|
185
|
+
drawColoredTriangles(new Float32Array([
|
|
186
|
+
x1, y1, r, g, b, a,
|
|
187
|
+
x2, y1, r, g, b, a,
|
|
188
|
+
x1, y2, r, g, b, a,
|
|
189
|
+
x1, y2, r, g, b, a,
|
|
190
|
+
x2, y1, r, g, b, a,
|
|
191
|
+
x2, y2, r, g, b, a,
|
|
192
|
+
]));
|
|
193
|
+
}
|
|
194
|
+
function renderRectangles(bgOps, cell) {
|
|
195
|
+
if (!bgOps.length)
|
|
196
|
+
return;
|
|
197
|
+
const data = new Float32Array((bgOps.length / BG_OP_STRIDE) * 36);
|
|
198
|
+
let offset = 0;
|
|
199
|
+
for (let i = 0; i < bgOps.length; i += BG_OP_STRIDE) {
|
|
200
|
+
const row = bgOps[i];
|
|
201
|
+
const col = bgOps[i + 1];
|
|
202
|
+
const colSpan = bgOps[i + 2];
|
|
203
|
+
const packed = bgOps[i + 3];
|
|
204
|
+
const x1 = col * cell.pw;
|
|
205
|
+
const y1 = row * cell.ph;
|
|
206
|
+
const x2 = x1 + colSpan * cell.pw;
|
|
207
|
+
const y2 = y1 + cell.ph;
|
|
208
|
+
const r = ((packed >>> 16) & 0xff) / 255;
|
|
209
|
+
const g = ((packed >>> 8) & 0xff) / 255;
|
|
210
|
+
const b = (packed & 0xff) / 255;
|
|
211
|
+
data.set([
|
|
212
|
+
x1, y1, r, g, b, 1,
|
|
213
|
+
x2, y1, r, g, b, 1,
|
|
214
|
+
x1, y2, r, g, b, 1,
|
|
215
|
+
x1, y2, r, g, b, 1,
|
|
216
|
+
x2, y1, r, g, b, 1,
|
|
217
|
+
x2, y2, r, g, b, 1,
|
|
218
|
+
], offset);
|
|
219
|
+
offset += 36;
|
|
220
|
+
}
|
|
221
|
+
drawColoredTriangles(data);
|
|
222
|
+
}
|
|
223
|
+
function renderCursor(cursorVisible, cursorCol, cursorRow, cell) {
|
|
224
|
+
if (!cursorVisible)
|
|
225
|
+
return;
|
|
226
|
+
const x1 = cursorCol * cell.pw;
|
|
227
|
+
const y1 = cursorRow * cell.ph;
|
|
228
|
+
const x2 = x1 + cell.pw;
|
|
229
|
+
const y2 = y1 + cell.ph;
|
|
230
|
+
drawSolidRect(x1, y1, x2, y2, 0.8, 0.8, 0.8, 0.5);
|
|
231
|
+
}
|
|
232
|
+
function renderGlyphs(glyphOps, atlasCanvas, atlasVersion, cell) {
|
|
233
|
+
if (!glyphOps.length || !ensureAtlas(atlasCanvas, atlasVersion))
|
|
234
|
+
return;
|
|
235
|
+
const atlasWidth = atlasCanvas.width || 1;
|
|
236
|
+
const atlasHeight = atlasCanvas.height || 1;
|
|
237
|
+
const floatsPerVert = 8;
|
|
238
|
+
const vertsPerGlyph = 6;
|
|
239
|
+
const data = new Float32Array((glyphOps.length / GLYPH_OP_STRIDE) * vertsPerGlyph * floatsPerVert);
|
|
240
|
+
let offset = 0;
|
|
241
|
+
for (let i = 0; i < glyphOps.length; i += GLYPH_OP_STRIDE) {
|
|
242
|
+
const srcX = glyphOps[i];
|
|
243
|
+
const srcY = glyphOps[i + 1];
|
|
244
|
+
const srcW = glyphOps[i + 2];
|
|
245
|
+
const srcH = glyphOps[i + 3];
|
|
246
|
+
const row = glyphOps[i + 4];
|
|
247
|
+
const col = glyphOps[i + 5];
|
|
248
|
+
const colSpan = glyphOps[i + 6];
|
|
249
|
+
const packed = glyphOps[i + 7];
|
|
250
|
+
const dx1 = col * cell.pw;
|
|
251
|
+
const dy1 = row * cell.ph;
|
|
252
|
+
const dx2 = dx1 + colSpan * cell.pw;
|
|
253
|
+
const dy2 = dy1 + cell.ph;
|
|
254
|
+
const u1 = srcX / atlasWidth;
|
|
255
|
+
const v1 = srcY / atlasHeight;
|
|
256
|
+
const u2 = (srcX + srcW) / atlasWidth;
|
|
257
|
+
const v2 = (srcY + srcH) / atlasHeight;
|
|
258
|
+
const r = ((packed >>> 16) & 0xff) / 255;
|
|
259
|
+
const g = ((packed >>> 8) & 0xff) / 255;
|
|
260
|
+
const b = (packed & 0xff) / 255;
|
|
261
|
+
data.set([
|
|
262
|
+
dx1, dy1, u1, v1, r, g, b, 1,
|
|
263
|
+
dx2, dy1, u2, v1, r, g, b, 1,
|
|
264
|
+
dx1, dy2, u1, v2, r, g, b, 1,
|
|
265
|
+
dx1, dy2, u1, v2, r, g, b, 1,
|
|
266
|
+
dx2, dy1, u2, v1, r, g, b, 1,
|
|
267
|
+
dx2, dy2, u2, v2, r, g, b, 1,
|
|
268
|
+
], offset);
|
|
269
|
+
offset += vertsPerGlyph * floatsPerVert;
|
|
270
|
+
}
|
|
271
|
+
const totalVerts = data.length / floatsPerVert;
|
|
272
|
+
const stride = floatsPerVert * 4;
|
|
273
|
+
gl.useProgram(glyphProgram);
|
|
274
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glyphBuffer);
|
|
275
|
+
gl.enableVertexAttribArray(glyphPosLoc);
|
|
276
|
+
gl.enableVertexAttribArray(glyphUvLoc);
|
|
277
|
+
gl.enableVertexAttribArray(glyphColorLoc);
|
|
278
|
+
gl.uniform2f(glyphResLoc, canvas.width, canvas.height);
|
|
279
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
280
|
+
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
|
|
281
|
+
gl.uniform1i(glyphTexLoc, 0);
|
|
282
|
+
for (let off = 0; off < totalVerts; off += MAX_BATCH_VERTS) {
|
|
283
|
+
const count = Math.min(MAX_BATCH_VERTS, totalVerts - off);
|
|
284
|
+
const slice = data.subarray(off * floatsPerVert, (off + count) * floatsPerVert);
|
|
285
|
+
gl.bufferData(gl.ARRAY_BUFFER, slice, gl.DYNAMIC_DRAW);
|
|
286
|
+
gl.vertexAttribPointer(glyphPosLoc, 2, gl.FLOAT, false, stride, 0);
|
|
287
|
+
gl.vertexAttribPointer(glyphUvLoc, 2, gl.FLOAT, false, stride, 8);
|
|
288
|
+
gl.vertexAttribPointer(glyphColorLoc, 4, gl.FLOAT, false, stride, 16);
|
|
289
|
+
gl.drawArrays(gl.TRIANGLES, 0, count);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
supported: true,
|
|
294
|
+
resize(width, height) {
|
|
295
|
+
if (canvas.width !== width)
|
|
296
|
+
canvas.width = width;
|
|
297
|
+
if (canvas.height !== height)
|
|
298
|
+
canvas.height = height;
|
|
299
|
+
},
|
|
300
|
+
render(bgOps, glyphOps, atlasCanvas, atlasVersion, cursorVisible, cursorCol, cursorRow, cell) {
|
|
301
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
302
|
+
gl.clearColor(0, 0, 0, 1);
|
|
303
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
304
|
+
renderRectangles(bgOps, cell);
|
|
305
|
+
if (atlasCanvas) {
|
|
306
|
+
renderGlyphs(glyphOps, atlasCanvas, atlasVersion, cell);
|
|
307
|
+
}
|
|
308
|
+
renderCursor(cursorVisible, cursorCol, cursorRow, cell);
|
|
309
|
+
},
|
|
310
|
+
dispose() {
|
|
311
|
+
gl.deleteBuffer(rectBuffer);
|
|
312
|
+
gl.deleteBuffer(glyphBuffer);
|
|
313
|
+
gl.deleteTexture(atlasTexture);
|
|
314
|
+
gl.deleteProgram(rectProgram);
|
|
315
|
+
gl.deleteProgram(glyphProgram);
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// Keyboard encoding (ported from web/index.html keyToBytes)
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
const encoder = new TextEncoder();
|
|
323
|
+
function keyToBytes(e, appCursor) {
|
|
324
|
+
if (e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
325
|
+
const kc = e.key.charCodeAt(0);
|
|
326
|
+
if (e.key.length === 1 && kc >= 1 && kc <= 26)
|
|
327
|
+
return new Uint8Array([kc]);
|
|
328
|
+
if (e.key.length === 1) {
|
|
329
|
+
const code = e.key.toLowerCase().charCodeAt(0);
|
|
330
|
+
if (code >= 97 && code <= 122)
|
|
331
|
+
return new Uint8Array([code - 96]);
|
|
332
|
+
if (e.key === '[')
|
|
333
|
+
return new Uint8Array([0x1b]);
|
|
334
|
+
if (e.key === '\\')
|
|
335
|
+
return new Uint8Array([0x1c]);
|
|
336
|
+
if (e.key === ']')
|
|
337
|
+
return new Uint8Array([0x1d]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) {
|
|
341
|
+
if (e.key === '?')
|
|
342
|
+
return new Uint8Array([0x7f]);
|
|
343
|
+
if (e.key === ' ' || e.key === '@')
|
|
344
|
+
return new Uint8Array([0x00]);
|
|
345
|
+
}
|
|
346
|
+
const arrows = {
|
|
347
|
+
ArrowUp: 'A',
|
|
348
|
+
ArrowDown: 'B',
|
|
349
|
+
ArrowRight: 'C',
|
|
350
|
+
ArrowLeft: 'D',
|
|
351
|
+
};
|
|
352
|
+
if (arrows[e.key]) {
|
|
353
|
+
const mod = (e.shiftKey ? 1 : 0) +
|
|
354
|
+
(e.altKey ? 2 : 0) +
|
|
355
|
+
(e.ctrlKey ? 4 : 0) +
|
|
356
|
+
(e.metaKey ? 8 : 0);
|
|
357
|
+
if (mod)
|
|
358
|
+
return encoder.encode(`\x1b[1;${mod + 1}${arrows[e.key]}`);
|
|
359
|
+
const prefix = appCursor ? '\x1bO' : '\x1b[';
|
|
360
|
+
return encoder.encode(prefix + arrows[e.key]);
|
|
361
|
+
}
|
|
362
|
+
const mod = (e.shiftKey ? 1 : 0) +
|
|
363
|
+
(e.altKey ? 2 : 0) +
|
|
364
|
+
(e.ctrlKey ? 4 : 0) +
|
|
365
|
+
(e.metaKey ? 8 : 0);
|
|
366
|
+
const tilde = {
|
|
367
|
+
PageUp: '5',
|
|
368
|
+
PageDown: '6',
|
|
369
|
+
Delete: '3',
|
|
370
|
+
Insert: '2',
|
|
371
|
+
};
|
|
372
|
+
if (tilde[e.key]) {
|
|
373
|
+
if (mod)
|
|
374
|
+
return encoder.encode(`\x1b[${tilde[e.key]};${mod + 1}~`);
|
|
375
|
+
return encoder.encode(`\x1b[${tilde[e.key]}~`);
|
|
376
|
+
}
|
|
377
|
+
const he = { Home: 'H', End: 'F' };
|
|
378
|
+
if (he[e.key]) {
|
|
379
|
+
if (mod)
|
|
380
|
+
return encoder.encode(`\x1b[1;${mod + 1}${he[e.key]}`);
|
|
381
|
+
return encoder.encode(`\x1b[${he[e.key]}`);
|
|
382
|
+
}
|
|
383
|
+
const f14 = { F1: 'P', F2: 'Q', F3: 'R', F4: 'S' };
|
|
384
|
+
if (f14[e.key]) {
|
|
385
|
+
if (mod)
|
|
386
|
+
return encoder.encode(`\x1b[1;${mod + 1}${f14[e.key]}`);
|
|
387
|
+
return encoder.encode(`\x1bO${f14[e.key]}`);
|
|
388
|
+
}
|
|
389
|
+
const fkeys = {
|
|
390
|
+
F5: '15', F6: '17', F7: '18', F8: '19',
|
|
391
|
+
F9: '20', F10: '21', F11: '23', F12: '24',
|
|
392
|
+
};
|
|
393
|
+
if (fkeys[e.key]) {
|
|
394
|
+
if (mod)
|
|
395
|
+
return encoder.encode(`\x1b[${fkeys[e.key]};${mod + 1}~`);
|
|
396
|
+
return encoder.encode(`\x1b[${fkeys[e.key]}~`);
|
|
397
|
+
}
|
|
398
|
+
const simple = {
|
|
399
|
+
Enter: '\r',
|
|
400
|
+
Backspace: '\x7f',
|
|
401
|
+
Tab: '\t',
|
|
402
|
+
Escape: '\x1b',
|
|
403
|
+
};
|
|
404
|
+
if (simple[e.key])
|
|
405
|
+
return encoder.encode(simple[e.key]);
|
|
406
|
+
if (e.altKey && !e.ctrlKey && !e.metaKey && e.key.length === 1) {
|
|
407
|
+
const code = e.key.charCodeAt(0);
|
|
408
|
+
if (code >= 0x20 && code <= 0x7e)
|
|
409
|
+
return encoder.encode('\x1b' + e.key);
|
|
410
|
+
return encoder.encode(e.key);
|
|
411
|
+
}
|
|
412
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
413
|
+
return encoder.encode(e.key);
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// WASM singleton initialisation
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
let wasmInitPromise = null;
|
|
421
|
+
function initWasm() {
|
|
422
|
+
if (!wasmInitPromise) {
|
|
423
|
+
wasmInitPromise = import('blit-browser').then(async (mod) => {
|
|
424
|
+
await mod.default();
|
|
425
|
+
return mod;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return wasmInitPromise;
|
|
429
|
+
}
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// Component
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
/**
|
|
434
|
+
* BlitTerminal renders a blit terminal inside a WebGL canvas.
|
|
435
|
+
*
|
|
436
|
+
* It handles WASM initialisation, server message processing, GL rendering,
|
|
437
|
+
* keyboard/mouse input, and dynamic resizing through a ResizeObserver.
|
|
438
|
+
*/
|
|
439
|
+
export const BlitTerminal = forwardRef(function BlitTerminal(props, ref) {
|
|
440
|
+
const { transport, ptyId, fontFamily = DEFAULT_FONT, fontSize = DEFAULT_FONT_SIZE, onTitleChange, onPtyCreated, onPtyClosed, onPtyList, className, style, } = props;
|
|
441
|
+
// Refs for DOM elements.
|
|
442
|
+
const containerRef = useRef(null);
|
|
443
|
+
const glCanvasRef = useRef(null);
|
|
444
|
+
const overlayCanvasRef = useRef(null);
|
|
445
|
+
const inputRef = useRef(null);
|
|
446
|
+
// Refs for mutable state that must not trigger re-renders.
|
|
447
|
+
const terminalRef = useRef(null);
|
|
448
|
+
const rendererRef = useRef(null);
|
|
449
|
+
const rafRef = useRef(0);
|
|
450
|
+
const cellRef = useRef(measureCell(fontFamily, fontSize));
|
|
451
|
+
const rowsRef = useRef(24);
|
|
452
|
+
const colsRef = useRef(80);
|
|
453
|
+
const needsRenderRef = useRef(false);
|
|
454
|
+
const ackAheadRef = useRef(0);
|
|
455
|
+
const subscribedRef = useRef(false);
|
|
456
|
+
const scrollOffsetRef = useRef(0);
|
|
457
|
+
const wasmModRef = useRef(null);
|
|
458
|
+
// React state for things the consumer might read.
|
|
459
|
+
const [wasmReady, setWasmReady] = useState(false);
|
|
460
|
+
// -----------------------------------------------------------------------
|
|
461
|
+
// Connection callbacks
|
|
462
|
+
// -----------------------------------------------------------------------
|
|
463
|
+
const onUpdate = useCallback((updatePtyId, payload) => {
|
|
464
|
+
if (updatePtyId !== ptyId || !terminalRef.current)
|
|
465
|
+
return;
|
|
466
|
+
terminalRef.current.feed_compressed(payload);
|
|
467
|
+
needsRenderRef.current = true;
|
|
468
|
+
// ACK
|
|
469
|
+
if (ackAheadRef.current < MAX_ACK_AHEAD) {
|
|
470
|
+
transport.send(new Uint8Array([C2S_ACK]));
|
|
471
|
+
ackAheadRef.current += 1;
|
|
472
|
+
}
|
|
473
|
+
}, [ptyId, transport]);
|
|
474
|
+
const onCreated = useCallback((createdId) => {
|
|
475
|
+
onPtyCreated?.(createdId);
|
|
476
|
+
}, [onPtyCreated]);
|
|
477
|
+
const onClosed = useCallback((closedId) => {
|
|
478
|
+
onPtyClosed?.(closedId);
|
|
479
|
+
}, [onPtyClosed]);
|
|
480
|
+
const onList = useCallback((ids) => {
|
|
481
|
+
onPtyList?.(ids);
|
|
482
|
+
}, [onPtyList]);
|
|
483
|
+
const onTitle = useCallback((titlePtyId, title) => {
|
|
484
|
+
if (titlePtyId === ptyId) {
|
|
485
|
+
onTitleChange?.(title);
|
|
486
|
+
}
|
|
487
|
+
}, [ptyId, onTitleChange]);
|
|
488
|
+
const { status, sendInput, sendResize, sendSubscribe, sendUnsubscribe, sendScroll } = useBlitConnection(transport, {
|
|
489
|
+
onUpdate,
|
|
490
|
+
onCreated,
|
|
491
|
+
onClosed,
|
|
492
|
+
onList,
|
|
493
|
+
onTitle,
|
|
494
|
+
});
|
|
495
|
+
// -----------------------------------------------------------------------
|
|
496
|
+
// Imperative handle
|
|
497
|
+
// -----------------------------------------------------------------------
|
|
498
|
+
useImperativeHandle(ref, () => ({
|
|
499
|
+
get terminal() {
|
|
500
|
+
return terminalRef.current;
|
|
501
|
+
},
|
|
502
|
+
get rows() {
|
|
503
|
+
return rowsRef.current;
|
|
504
|
+
},
|
|
505
|
+
get cols() {
|
|
506
|
+
return colsRef.current;
|
|
507
|
+
},
|
|
508
|
+
status,
|
|
509
|
+
focus() {
|
|
510
|
+
inputRef.current?.focus();
|
|
511
|
+
},
|
|
512
|
+
}), [status]);
|
|
513
|
+
// -----------------------------------------------------------------------
|
|
514
|
+
// WASM init
|
|
515
|
+
// -----------------------------------------------------------------------
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
let cancelled = false;
|
|
518
|
+
initWasm().then((mod) => {
|
|
519
|
+
if (cancelled)
|
|
520
|
+
return;
|
|
521
|
+
wasmModRef.current = mod;
|
|
522
|
+
setWasmReady(true);
|
|
523
|
+
});
|
|
524
|
+
return () => {
|
|
525
|
+
cancelled = true;
|
|
526
|
+
};
|
|
527
|
+
}, []);
|
|
528
|
+
// -----------------------------------------------------------------------
|
|
529
|
+
// Cell measurement (re-measure when font changes)
|
|
530
|
+
// -----------------------------------------------------------------------
|
|
531
|
+
useEffect(() => {
|
|
532
|
+
cellRef.current = measureCell(fontFamily, fontSize);
|
|
533
|
+
if (terminalRef.current) {
|
|
534
|
+
terminalRef.current.set_cell_size(cellRef.current.pw, cellRef.current.ph);
|
|
535
|
+
terminalRef.current.set_font_family(fontFamily);
|
|
536
|
+
needsRenderRef.current = true;
|
|
537
|
+
}
|
|
538
|
+
}, [fontFamily, fontSize]);
|
|
539
|
+
// -----------------------------------------------------------------------
|
|
540
|
+
// GL renderer lifecycle
|
|
541
|
+
// -----------------------------------------------------------------------
|
|
542
|
+
useEffect(() => {
|
|
543
|
+
const canvas = glCanvasRef.current;
|
|
544
|
+
if (!canvas)
|
|
545
|
+
return;
|
|
546
|
+
const renderer = createGlRenderer(canvas);
|
|
547
|
+
rendererRef.current = renderer;
|
|
548
|
+
return () => {
|
|
549
|
+
renderer.dispose();
|
|
550
|
+
rendererRef.current = null;
|
|
551
|
+
};
|
|
552
|
+
}, []);
|
|
553
|
+
// -----------------------------------------------------------------------
|
|
554
|
+
// Terminal instance lifecycle
|
|
555
|
+
// -----------------------------------------------------------------------
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
if (!wasmReady || ptyId === null)
|
|
558
|
+
return;
|
|
559
|
+
const mod = wasmModRef.current;
|
|
560
|
+
const cell = cellRef.current;
|
|
561
|
+
const t = new mod.Terminal(rowsRef.current, colsRef.current, cell.pw, cell.ph);
|
|
562
|
+
t.set_font_family(fontFamily);
|
|
563
|
+
terminalRef.current = t;
|
|
564
|
+
needsRenderRef.current = true;
|
|
565
|
+
return () => {
|
|
566
|
+
t.free();
|
|
567
|
+
if (terminalRef.current === t) {
|
|
568
|
+
terminalRef.current = null;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}, [wasmReady, ptyId, fontFamily]);
|
|
572
|
+
// -----------------------------------------------------------------------
|
|
573
|
+
// Subscribe/unsubscribe to PTY
|
|
574
|
+
// -----------------------------------------------------------------------
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
if (ptyId === null || status !== 'connected') {
|
|
577
|
+
if (subscribedRef.current && ptyId !== null) {
|
|
578
|
+
sendUnsubscribe(ptyId);
|
|
579
|
+
subscribedRef.current = false;
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
sendSubscribe(ptyId);
|
|
584
|
+
subscribedRef.current = true;
|
|
585
|
+
// Send initial resize.
|
|
586
|
+
sendResize(ptyId, rowsRef.current, colsRef.current);
|
|
587
|
+
return () => {
|
|
588
|
+
if (subscribedRef.current) {
|
|
589
|
+
sendUnsubscribe(ptyId);
|
|
590
|
+
subscribedRef.current = false;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}, [ptyId, status, sendSubscribe, sendUnsubscribe, sendResize]);
|
|
594
|
+
// -----------------------------------------------------------------------
|
|
595
|
+
// Resize observer
|
|
596
|
+
// -----------------------------------------------------------------------
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
const container = containerRef.current;
|
|
599
|
+
if (!container)
|
|
600
|
+
return;
|
|
601
|
+
const handleResize = () => {
|
|
602
|
+
const cell = cellRef.current;
|
|
603
|
+
const w = container.clientWidth;
|
|
604
|
+
const h = container.clientHeight;
|
|
605
|
+
const cols = Math.max(1, Math.floor(w / cell.w));
|
|
606
|
+
const rows = Math.max(1, Math.floor(h / cell.h));
|
|
607
|
+
if (cols === colsRef.current && rows === rowsRef.current)
|
|
608
|
+
return;
|
|
609
|
+
rowsRef.current = rows;
|
|
610
|
+
colsRef.current = cols;
|
|
611
|
+
// Resize GL canvas backing.
|
|
612
|
+
const pw = cols * cell.pw;
|
|
613
|
+
const ph = rows * cell.ph;
|
|
614
|
+
rendererRef.current?.resize(pw, ph);
|
|
615
|
+
// Resize overlay canvas.
|
|
616
|
+
const overlay = overlayCanvasRef.current;
|
|
617
|
+
if (overlay) {
|
|
618
|
+
overlay.width = pw;
|
|
619
|
+
overlay.height = ph;
|
|
620
|
+
overlay.style.width = `${cols * cell.w}px`;
|
|
621
|
+
overlay.style.height = `${rows * cell.h}px`;
|
|
622
|
+
}
|
|
623
|
+
// Resize GL canvas CSS size.
|
|
624
|
+
const glCanvas = glCanvasRef.current;
|
|
625
|
+
if (glCanvas) {
|
|
626
|
+
glCanvas.style.width = `${cols * cell.w}px`;
|
|
627
|
+
glCanvas.style.height = `${rows * cell.h}px`;
|
|
628
|
+
}
|
|
629
|
+
needsRenderRef.current = true;
|
|
630
|
+
if (ptyId !== null && status === 'connected') {
|
|
631
|
+
sendResize(ptyId, rows, cols);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const observer = new ResizeObserver(handleResize);
|
|
635
|
+
observer.observe(container);
|
|
636
|
+
// Run once immediately.
|
|
637
|
+
handleResize();
|
|
638
|
+
return () => observer.disconnect();
|
|
639
|
+
}, [ptyId, status, sendResize]);
|
|
640
|
+
// -----------------------------------------------------------------------
|
|
641
|
+
// Render loop
|
|
642
|
+
// -----------------------------------------------------------------------
|
|
643
|
+
useEffect(() => {
|
|
644
|
+
let running = true;
|
|
645
|
+
const renderLoop = () => {
|
|
646
|
+
if (!running)
|
|
647
|
+
return;
|
|
648
|
+
if (needsRenderRef.current && terminalRef.current && rendererRef.current?.supported) {
|
|
649
|
+
needsRenderRef.current = false;
|
|
650
|
+
const t = terminalRef.current;
|
|
651
|
+
const cell = cellRef.current;
|
|
652
|
+
const renderer = rendererRef.current;
|
|
653
|
+
t.prepare_render_ops();
|
|
654
|
+
if (t.last_render_scroll_rows()) {
|
|
655
|
+
t.prepare_full_render_ops();
|
|
656
|
+
}
|
|
657
|
+
const bgOps = t.background_ops();
|
|
658
|
+
const glyphOps = t.glyph_ops();
|
|
659
|
+
const atlasCanvas = t.glyph_atlas_canvas();
|
|
660
|
+
const atlasVersion = t.glyph_atlas_version();
|
|
661
|
+
renderer.render(bgOps, glyphOps, atlasCanvas, atlasVersion, t.cursor_visible(), t.cursor_col, t.cursor_row, cell);
|
|
662
|
+
// Render overflow text (emoji / wide Unicode) via 2D overlay canvas.
|
|
663
|
+
const overflowCount = t.overflow_text_count();
|
|
664
|
+
const overlay = overlayCanvasRef.current;
|
|
665
|
+
if (overlay) {
|
|
666
|
+
const ctx = overlay.getContext('2d');
|
|
667
|
+
if (ctx) {
|
|
668
|
+
ctx.clearRect(0, 0, overlay.width, overlay.height);
|
|
669
|
+
if (overflowCount > 0) {
|
|
670
|
+
const cw = cell.pw;
|
|
671
|
+
const ch = cell.ph;
|
|
672
|
+
const scale = 0.85;
|
|
673
|
+
const scaledH = ch * scale;
|
|
674
|
+
const fSize = Math.max(1, Math.round(scaledH));
|
|
675
|
+
ctx.font = `${fSize}px ${fontFamily}`;
|
|
676
|
+
ctx.textBaseline = 'bottom';
|
|
677
|
+
ctx.fillStyle = '#ccc';
|
|
678
|
+
for (let i = 0; i < overflowCount; i++) {
|
|
679
|
+
const op = t.overflow_text_op(i);
|
|
680
|
+
if (!op)
|
|
681
|
+
continue;
|
|
682
|
+
const [row, col, colSpan, text] = op;
|
|
683
|
+
const x = col * cw;
|
|
684
|
+
const y = row * ch;
|
|
685
|
+
const w = colSpan * cw;
|
|
686
|
+
const padX = (w - w * scale) / 2;
|
|
687
|
+
const padY = (ch - scaledH) / 2;
|
|
688
|
+
ctx.save();
|
|
689
|
+
ctx.beginPath();
|
|
690
|
+
ctx.rect(x, y, w, ch);
|
|
691
|
+
ctx.clip();
|
|
692
|
+
ctx.fillText(text, x + padX, y + padY + scaledH);
|
|
693
|
+
ctx.restore();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Reset ack-ahead after rendering a frame.
|
|
699
|
+
ackAheadRef.current = 0;
|
|
700
|
+
}
|
|
701
|
+
rafRef.current = requestAnimationFrame(renderLoop);
|
|
702
|
+
};
|
|
703
|
+
rafRef.current = requestAnimationFrame(renderLoop);
|
|
704
|
+
return () => {
|
|
705
|
+
running = false;
|
|
706
|
+
cancelAnimationFrame(rafRef.current);
|
|
707
|
+
};
|
|
708
|
+
}, [fontFamily]);
|
|
709
|
+
// -----------------------------------------------------------------------
|
|
710
|
+
// Keyboard input
|
|
711
|
+
// -----------------------------------------------------------------------
|
|
712
|
+
useEffect(() => {
|
|
713
|
+
const input = inputRef.current;
|
|
714
|
+
if (!input)
|
|
715
|
+
return;
|
|
716
|
+
const handleKeyDown = (e) => {
|
|
717
|
+
if (ptyId === null || status !== 'connected')
|
|
718
|
+
return;
|
|
719
|
+
if (e.isComposing)
|
|
720
|
+
return;
|
|
721
|
+
if (e.key === 'Dead')
|
|
722
|
+
return;
|
|
723
|
+
const t = terminalRef.current;
|
|
724
|
+
const appCursor = t ? t.app_cursor() : false;
|
|
725
|
+
const bytes = keyToBytes(e, appCursor);
|
|
726
|
+
if (bytes) {
|
|
727
|
+
e.preventDefault();
|
|
728
|
+
// Reset scroll to live on any real input.
|
|
729
|
+
if (scrollOffsetRef.current > 0) {
|
|
730
|
+
scrollOffsetRef.current = 0;
|
|
731
|
+
sendScroll(ptyId, 0);
|
|
732
|
+
}
|
|
733
|
+
sendInput(ptyId, bytes);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
const handleCompositionEnd = (e) => {
|
|
737
|
+
if (e.data && ptyId !== null && status === 'connected') {
|
|
738
|
+
sendInput(ptyId, encoder.encode(e.data));
|
|
739
|
+
}
|
|
740
|
+
input.value = '';
|
|
741
|
+
};
|
|
742
|
+
const handleInput = (e) => {
|
|
743
|
+
const inputEvent = e;
|
|
744
|
+
if (inputEvent.isComposing) {
|
|
745
|
+
if (inputEvent.inputType === 'deleteContentBackward' &&
|
|
746
|
+
!input.value &&
|
|
747
|
+
ptyId !== null &&
|
|
748
|
+
status === 'connected') {
|
|
749
|
+
sendInput(ptyId, new Uint8Array([0x7f]));
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (inputEvent.inputType === 'deleteContentBackward' &&
|
|
754
|
+
!input.value) {
|
|
755
|
+
if (ptyId !== null && status === 'connected') {
|
|
756
|
+
sendInput(ptyId, new Uint8Array([0x7f]));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
else if (input.value && ptyId !== null && status === 'connected') {
|
|
760
|
+
sendInput(ptyId, encoder.encode(input.value.replace(/\n/g, '\r')));
|
|
761
|
+
}
|
|
762
|
+
input.value = '';
|
|
763
|
+
};
|
|
764
|
+
input.addEventListener('keydown', handleKeyDown);
|
|
765
|
+
input.addEventListener('compositionend', handleCompositionEnd);
|
|
766
|
+
input.addEventListener('input', handleInput);
|
|
767
|
+
return () => {
|
|
768
|
+
input.removeEventListener('keydown', handleKeyDown);
|
|
769
|
+
input.removeEventListener('compositionend', handleCompositionEnd);
|
|
770
|
+
input.removeEventListener('input', handleInput);
|
|
771
|
+
};
|
|
772
|
+
}, [ptyId, status, sendInput, sendScroll]);
|
|
773
|
+
// -----------------------------------------------------------------------
|
|
774
|
+
// Mouse input
|
|
775
|
+
// -----------------------------------------------------------------------
|
|
776
|
+
useEffect(() => {
|
|
777
|
+
const canvas = glCanvasRef.current;
|
|
778
|
+
if (!canvas)
|
|
779
|
+
return;
|
|
780
|
+
function mouseToCell(e) {
|
|
781
|
+
const rect = canvas.getBoundingClientRect();
|
|
782
|
+
const cell = cellRef.current;
|
|
783
|
+
return {
|
|
784
|
+
row: Math.min(Math.max(Math.floor((e.clientY - rect.top) / cell.h), 0), rowsRef.current - 1),
|
|
785
|
+
col: Math.min(Math.max(Math.floor((e.clientX - rect.left) / cell.w), 0), colsRef.current - 1),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function sendMouseEvent(type, e, button) {
|
|
789
|
+
const t = terminalRef.current;
|
|
790
|
+
if (!t || t.mouse_mode() === 0 || ptyId === null || status !== 'connected')
|
|
791
|
+
return false;
|
|
792
|
+
const pos = mouseToCell(e);
|
|
793
|
+
const enc = t.mouse_encoding();
|
|
794
|
+
if (enc === 2) {
|
|
795
|
+
const suffix = type === 'up' ? 'm' : 'M';
|
|
796
|
+
const seq = `\x1b[<${button};${pos.col + 1};${pos.row + 1}${suffix}`;
|
|
797
|
+
sendInput(ptyId, encoder.encode(seq));
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
if (type === 'up')
|
|
801
|
+
button = 3;
|
|
802
|
+
const seq = new Uint8Array([
|
|
803
|
+
0x1b, 0x5b, 0x4d,
|
|
804
|
+
button + 32,
|
|
805
|
+
pos.col + 33,
|
|
806
|
+
pos.row + 33,
|
|
807
|
+
]);
|
|
808
|
+
sendInput(ptyId, seq);
|
|
809
|
+
}
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
const handleMouseDown = (e) => {
|
|
813
|
+
if (!e.shiftKey && sendMouseEvent('down', e, e.button)) {
|
|
814
|
+
e.preventDefault();
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
const handleMouseMove = (e) => {
|
|
818
|
+
if (e.shiftKey)
|
|
819
|
+
return;
|
|
820
|
+
const t = terminalRef.current;
|
|
821
|
+
if (!t)
|
|
822
|
+
return;
|
|
823
|
+
const mode = t.mouse_mode();
|
|
824
|
+
if (mode === 3 || mode === 4) {
|
|
825
|
+
const button = e.buttons & 1 ? 0 : e.buttons & 2 ? 2 : e.buttons & 4 ? 1 : 0;
|
|
826
|
+
if (e.buttons || mode === 4) {
|
|
827
|
+
sendMouseEvent('move', e, button + 32);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
const handleMouseUp = (e) => {
|
|
832
|
+
if (!e.shiftKey) {
|
|
833
|
+
sendMouseEvent('up', e, e.button);
|
|
834
|
+
}
|
|
835
|
+
inputRef.current?.focus();
|
|
836
|
+
};
|
|
837
|
+
const handleWheel = (e) => {
|
|
838
|
+
const t = terminalRef.current;
|
|
839
|
+
if (t && t.mouse_mode() > 0) {
|
|
840
|
+
e.preventDefault();
|
|
841
|
+
const button = e.deltaY < 0 ? 64 : 65;
|
|
842
|
+
sendMouseEvent('down', e, button);
|
|
843
|
+
}
|
|
844
|
+
else if (ptyId !== null && status === 'connected') {
|
|
845
|
+
e.preventDefault();
|
|
846
|
+
const lines = Math.round(-e.deltaY / 20) || (e.deltaY > 0 ? -3 : 3);
|
|
847
|
+
scrollOffsetRef.current = Math.max(0, scrollOffsetRef.current + lines);
|
|
848
|
+
sendScroll(ptyId, scrollOffsetRef.current);
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const handleContextMenu = (e) => {
|
|
852
|
+
const t = terminalRef.current;
|
|
853
|
+
if (t && t.mouse_mode() > 0)
|
|
854
|
+
e.preventDefault();
|
|
855
|
+
};
|
|
856
|
+
const handleClick = () => {
|
|
857
|
+
inputRef.current?.focus();
|
|
858
|
+
};
|
|
859
|
+
canvas.addEventListener('mousedown', handleMouseDown);
|
|
860
|
+
canvas.addEventListener('mousemove', handleMouseMove);
|
|
861
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
862
|
+
canvas.addEventListener('wheel', handleWheel, { passive: false });
|
|
863
|
+
canvas.addEventListener('contextmenu', handleContextMenu);
|
|
864
|
+
canvas.addEventListener('click', handleClick);
|
|
865
|
+
return () => {
|
|
866
|
+
canvas.removeEventListener('mousedown', handleMouseDown);
|
|
867
|
+
canvas.removeEventListener('mousemove', handleMouseMove);
|
|
868
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
869
|
+
canvas.removeEventListener('wheel', handleWheel);
|
|
870
|
+
canvas.removeEventListener('contextmenu', handleContextMenu);
|
|
871
|
+
canvas.removeEventListener('click', handleClick);
|
|
872
|
+
};
|
|
873
|
+
}, [ptyId, status, sendInput, sendScroll]);
|
|
874
|
+
// -----------------------------------------------------------------------
|
|
875
|
+
// Render
|
|
876
|
+
// -----------------------------------------------------------------------
|
|
877
|
+
return (_jsxs("div", { ref: containerRef, className: className, style: {
|
|
878
|
+
position: 'relative',
|
|
879
|
+
overflow: 'hidden',
|
|
880
|
+
background: '#000',
|
|
881
|
+
...style,
|
|
882
|
+
}, children: [_jsx("canvas", { ref: glCanvasRef, style: {
|
|
883
|
+
display: 'block',
|
|
884
|
+
position: 'absolute',
|
|
885
|
+
top: 0,
|
|
886
|
+
left: 0,
|
|
887
|
+
imageRendering: 'pixelated',
|
|
888
|
+
cursor: 'text',
|
|
889
|
+
} }), _jsx("canvas", { ref: overlayCanvasRef, style: {
|
|
890
|
+
display: 'block',
|
|
891
|
+
position: 'absolute',
|
|
892
|
+
top: 0,
|
|
893
|
+
left: 0,
|
|
894
|
+
pointerEvents: 'none',
|
|
895
|
+
imageRendering: 'pixelated',
|
|
896
|
+
} }), _jsx("textarea", { ref: inputRef, "aria-label": "Terminal input", autoCapitalize: "off", autoComplete: "off", autoCorrect: "off", spellCheck: false, style: {
|
|
897
|
+
position: 'absolute',
|
|
898
|
+
opacity: 0,
|
|
899
|
+
width: 1,
|
|
900
|
+
height: 1,
|
|
901
|
+
top: 0,
|
|
902
|
+
left: 0,
|
|
903
|
+
padding: 0,
|
|
904
|
+
border: 'none',
|
|
905
|
+
outline: 'none',
|
|
906
|
+
resize: 'none',
|
|
907
|
+
overflow: 'hidden',
|
|
908
|
+
}, tabIndex: 0 })] }));
|
|
909
|
+
});
|
|
910
|
+
//# sourceMappingURL=BlitTerminal.js.map
|