@termdraw/opentui 0.3.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/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/app.d.ts +71 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +826 -0
- package/dist/draw-state.d.ts +207 -0
- package/dist/draw-state.d.ts.map +1 -0
- package/dist/draw-state.js +2058 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/react.d.ts +22 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +30 -0
- package/package.json +77 -0
package/dist/app.js
ADDED
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import { FrameBufferRenderable, MouseButton, RGBA, TextAttributes, } from "@opentui/core";
|
|
2
|
+
import { DrawState, INK_COLORS, padToWidth, truncateToCells, visibleCellCount, } from "./draw-state.js";
|
|
3
|
+
const MIN_WIDTH = 45;
|
|
4
|
+
const MIN_HEIGHT = 27;
|
|
5
|
+
const TOOL_PALETTE_WIDTH = 17;
|
|
6
|
+
const TOOL_BUTTON_WIDTH = 13;
|
|
7
|
+
const BOX_STYLE_BUTTON_WIDTH = 10;
|
|
8
|
+
const TOOL_BUTTON_HEIGHT = 3;
|
|
9
|
+
const BOX_STYLE_ROW_COUNT = 4;
|
|
10
|
+
const COLOR_SWATCH_WIDTH = 3;
|
|
11
|
+
const COLOR_SWATCH_COLUMNS = 4;
|
|
12
|
+
const COLORS = {
|
|
13
|
+
background: RGBA.fromHex("#0f172a"),
|
|
14
|
+
panel: RGBA.fromHex("#0f172a"),
|
|
15
|
+
border: RGBA.fromHex("#475569"),
|
|
16
|
+
text: RGBA.fromHex("#e2e8f0"),
|
|
17
|
+
dim: RGBA.fromHex("#94a3b8"),
|
|
18
|
+
select: RGBA.fromHex("#38bdf8"),
|
|
19
|
+
accent: RGBA.fromHex("#22d3ee"),
|
|
20
|
+
warning: RGBA.fromHex("#f59e0b"),
|
|
21
|
+
success: RGBA.fromHex("#22c55e"),
|
|
22
|
+
paint: RGBA.fromHex("#a855f7"),
|
|
23
|
+
preview: RGBA.fromHex("#64748b"),
|
|
24
|
+
selectionFg: RGBA.fromHex("#f8fafc"),
|
|
25
|
+
selectionBg: RGBA.fromHex("#0ea5e9"),
|
|
26
|
+
handleFg: RGBA.fromHex("#f59e0b"),
|
|
27
|
+
handleBg: RGBA.fromHex("#0f172a"),
|
|
28
|
+
cursorFg: RGBA.fromHex("#0f172a"),
|
|
29
|
+
cursorBg: RGBA.fromHex("#f8fafc"),
|
|
30
|
+
};
|
|
31
|
+
const STARTUP_LOGO_LINES = [
|
|
32
|
+
" `:: :::::::-. :::::::.. :::. .:: . .:::.:",
|
|
33
|
+
" ;; ;;, `';,;;;;``;;;; ;;`;; ';;, ;; ;;;';;;",
|
|
34
|
+
"=[[[[[[.,cc[[[cc.=,,[[==[ccc, ,cccc,`[[ [[ [[[,/[[[' ,[[ '[[,'[[, [[, [[' '[[",
|
|
35
|
+
' $$ $$$___--\'`$$$"``$$$$$$$$"$$$ $$, $$ $$$$$$c c$$$cc$$$c Y$c$$$c$P $$',
|
|
36
|
+
' 88, 88b ,o,888 888 Y88" 888o888_,o8P\' 888b "88bo,888 888 "88"888 ""',
|
|
37
|
+
' MMM "YUMMMMP""MM, MMM M\' "MMMMMMP"` MMMM "W" YMM ""` "M "M" MM',
|
|
38
|
+
];
|
|
39
|
+
const STARTUP_LOGO_CAPTION = "(c) 2026 Ben Vinegar · Licensed under MIT";
|
|
40
|
+
const BOX_STYLE_OPTIONS = [
|
|
41
|
+
{ style: "auto", sample: "▣", label: "Auto" },
|
|
42
|
+
{ style: "light", sample: "┌─┐", label: "Single" },
|
|
43
|
+
{ style: "heavy", sample: "┏━┓", label: "Heavy" },
|
|
44
|
+
{ style: "double", sample: "╔═╗", label: "Double" },
|
|
45
|
+
];
|
|
46
|
+
const INK_COLOR_VALUES = {
|
|
47
|
+
white: RGBA.fromHex("#e2e8f0"),
|
|
48
|
+
red: RGBA.fromHex("#ef4444"),
|
|
49
|
+
orange: RGBA.fromHex("#f97316"),
|
|
50
|
+
yellow: RGBA.fromHex("#eab308"),
|
|
51
|
+
green: RGBA.fromHex("#22c55e"),
|
|
52
|
+
cyan: RGBA.fromHex("#06b6d4"),
|
|
53
|
+
blue: RGBA.fromHex("#3b82f6"),
|
|
54
|
+
magenta: RGBA.fromHex("#d946ef"),
|
|
55
|
+
};
|
|
56
|
+
function isPrintableKey(key) {
|
|
57
|
+
if (key.ctrl || key.meta || key.option)
|
|
58
|
+
return false;
|
|
59
|
+
if (!key.raw || key.raw.startsWith("\u001b"))
|
|
60
|
+
return false;
|
|
61
|
+
if (key.name === "space")
|
|
62
|
+
return false;
|
|
63
|
+
return visibleCellCount(key.raw) === 1;
|
|
64
|
+
}
|
|
65
|
+
function drawSegment(buffer, x, y, text, fg, bg, attributes = TextAttributes.NONE) {
|
|
66
|
+
if (text.length === 0)
|
|
67
|
+
return x;
|
|
68
|
+
buffer.drawText(text, x, y, fg, bg, attributes);
|
|
69
|
+
return x + visibleCellCount(text);
|
|
70
|
+
}
|
|
71
|
+
function mixColor(a, b, t) {
|
|
72
|
+
const [ar, ag, ab, aa] = a.toInts();
|
|
73
|
+
const [br, bg, bb, ba] = b.toInts();
|
|
74
|
+
const mix = (left, right) => Math.round(left + (right - left) * t);
|
|
75
|
+
return RGBA.fromInts(mix(ar, br), mix(ag, bg), mix(ab, bb), mix(aa, ba));
|
|
76
|
+
}
|
|
77
|
+
function getStartupLogoColor(rowIndex, colIndex, lineWidth) {
|
|
78
|
+
const verticalT = STARTUP_LOGO_LINES.length <= 1 ? 0 : rowIndex / (STARTUP_LOGO_LINES.length - 1);
|
|
79
|
+
const verticalColor = verticalT <= 0.55
|
|
80
|
+
? mixColor(COLORS.dim, COLORS.accent, verticalT / 0.55)
|
|
81
|
+
: mixColor(COLORS.accent, COLORS.warning, (verticalT - 0.55) / 0.45);
|
|
82
|
+
const horizontalT = lineWidth <= 1 ? 0 : colIndex / (lineWidth - 1);
|
|
83
|
+
const highlightStrength = 0.1 + 0.16 * Math.sin(horizontalT * Math.PI);
|
|
84
|
+
return mixColor(verticalColor, COLORS.text, highlightStrength);
|
|
85
|
+
}
|
|
86
|
+
function getStartupLogoCaptionColor() {
|
|
87
|
+
return mixColor(COLORS.border, COLORS.text, 0.3);
|
|
88
|
+
}
|
|
89
|
+
function getInkColorValue(color) {
|
|
90
|
+
return INK_COLOR_VALUES[color];
|
|
91
|
+
}
|
|
92
|
+
function getInkColorContrast(color) {
|
|
93
|
+
return color === "white" || color === "yellow" ? COLORS.panel : COLORS.text;
|
|
94
|
+
}
|
|
95
|
+
function isInsideRect(x, y, left, top, width, height) {
|
|
96
|
+
return x >= left && x < left + width && y >= top && y < top + height;
|
|
97
|
+
}
|
|
98
|
+
const FULL_CHROME_CANVAS_INSETS = {
|
|
99
|
+
left: 1,
|
|
100
|
+
top: 3,
|
|
101
|
+
right: 1,
|
|
102
|
+
bottom: 2,
|
|
103
|
+
};
|
|
104
|
+
const EDITOR_CANVAS_INSETS = {
|
|
105
|
+
left: 0,
|
|
106
|
+
top: 0,
|
|
107
|
+
right: 0,
|
|
108
|
+
bottom: 0,
|
|
109
|
+
};
|
|
110
|
+
function getCanvasInsets(chromeMode) {
|
|
111
|
+
return chromeMode === "full" ? FULL_CHROME_CANVAS_INSETS : EDITOR_CANVAS_INSETS;
|
|
112
|
+
}
|
|
113
|
+
export class TermDrawRenderable extends FrameBufferRenderable {
|
|
114
|
+
state;
|
|
115
|
+
chromeMode;
|
|
116
|
+
onSaveCallback = null;
|
|
117
|
+
onCancelCallback = null;
|
|
118
|
+
autoFocusEnabled = false;
|
|
119
|
+
startupLogoEnabled = true;
|
|
120
|
+
startupLogoDismissed = false;
|
|
121
|
+
cancelOnCtrlCEnabled = false;
|
|
122
|
+
footerTextOverride = null;
|
|
123
|
+
constructor(ctx, options = {}) {
|
|
124
|
+
const { width, height, onSave, onCancel, autoFocus = false, showStartupLogo = true, cancelOnCtrlC = false, footerText, chromeMode = "full", respectAlpha, ...renderableOptions } = options;
|
|
125
|
+
super(ctx, {
|
|
126
|
+
id: options.id ?? "term-draw",
|
|
127
|
+
width: typeof width === "number" ? width : 1,
|
|
128
|
+
height: typeof height === "number" ? height : 1,
|
|
129
|
+
respectAlpha,
|
|
130
|
+
...renderableOptions,
|
|
131
|
+
});
|
|
132
|
+
this.chromeMode = chromeMode;
|
|
133
|
+
this.state = new DrawState(this.width, this.height, getCanvasInsets(this.chromeMode));
|
|
134
|
+
this.focusable = true;
|
|
135
|
+
this.onSave = onSave;
|
|
136
|
+
this.onCancel = onCancel;
|
|
137
|
+
this.showStartupLogo = showStartupLogo;
|
|
138
|
+
this.autoFocus = autoFocus;
|
|
139
|
+
this.cancelOnCtrlC = cancelOnCtrlC;
|
|
140
|
+
this.footerText = footerText;
|
|
141
|
+
if (width !== undefined) {
|
|
142
|
+
this.width = width;
|
|
143
|
+
}
|
|
144
|
+
if (height !== undefined) {
|
|
145
|
+
this.height = height;
|
|
146
|
+
}
|
|
147
|
+
this.syncCanvasLayout();
|
|
148
|
+
}
|
|
149
|
+
set onSave(handler) {
|
|
150
|
+
this.onSaveCallback = handler ?? null;
|
|
151
|
+
}
|
|
152
|
+
set onCancel(handler) {
|
|
153
|
+
this.onCancelCallback = handler ?? null;
|
|
154
|
+
}
|
|
155
|
+
set autoFocus(value) {
|
|
156
|
+
this.autoFocusEnabled = value ?? false;
|
|
157
|
+
if (!this.autoFocusEnabled)
|
|
158
|
+
return;
|
|
159
|
+
queueMicrotask(() => {
|
|
160
|
+
if (this.isDestroyed || !this.autoFocusEnabled)
|
|
161
|
+
return;
|
|
162
|
+
this.focus();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
set showStartupLogo(value) {
|
|
166
|
+
this.startupLogoEnabled = value ?? true;
|
|
167
|
+
if (!this.startupLogoEnabled) {
|
|
168
|
+
this.startupLogoDismissed = true;
|
|
169
|
+
}
|
|
170
|
+
this.requestRender();
|
|
171
|
+
}
|
|
172
|
+
set cancelOnCtrlC(value) {
|
|
173
|
+
this.cancelOnCtrlCEnabled = value ?? false;
|
|
174
|
+
}
|
|
175
|
+
set footerText(value) {
|
|
176
|
+
this.footerTextOverride = value?.trim() ? value : null;
|
|
177
|
+
this.requestRender();
|
|
178
|
+
}
|
|
179
|
+
exportArt() {
|
|
180
|
+
return this.state.exportArt();
|
|
181
|
+
}
|
|
182
|
+
onResize(width, height) {
|
|
183
|
+
super.onResize(width, height);
|
|
184
|
+
this.syncCanvasLayout();
|
|
185
|
+
}
|
|
186
|
+
onMouseEvent(event) {
|
|
187
|
+
const layout = this.syncCanvasLayout();
|
|
188
|
+
const x = event.x - this.x;
|
|
189
|
+
const y = event.y - this.y;
|
|
190
|
+
if (event.type !== "move" && event.type !== "over" && event.type !== "out") {
|
|
191
|
+
this.dismissStartupLogo();
|
|
192
|
+
}
|
|
193
|
+
if (this.chromeMode === "full" && layout && !this.state.hasActivePointerInteraction) {
|
|
194
|
+
const toolButton = this.getToolButtons(layout).find((button) => isInsideRect(x, y, button.left, button.top, button.width, button.height));
|
|
195
|
+
if (toolButton) {
|
|
196
|
+
if (event.type === "down" && event.button === MouseButton.LEFT) {
|
|
197
|
+
this.state.setMode(toolButton.mode);
|
|
198
|
+
this.requestRender();
|
|
199
|
+
}
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
event.stopPropagation();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const boxStyleButton = this.getBoxStyleButtons(layout).find((button) => isInsideRect(x, y, button.left, button.top, button.width, 1));
|
|
205
|
+
if (boxStyleButton) {
|
|
206
|
+
if (event.type === "down" && event.button === MouseButton.LEFT) {
|
|
207
|
+
this.state.setMode("box");
|
|
208
|
+
this.state.setBoxStyle(boxStyleButton.style);
|
|
209
|
+
this.requestRender();
|
|
210
|
+
}
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
event.stopPropagation();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const colorSwatch = this.getColorSwatches(layout).find((swatch) => isInsideRect(x, y, swatch.left, swatch.top, swatch.width, 1));
|
|
216
|
+
if (colorSwatch) {
|
|
217
|
+
if (event.type === "down" && event.button === MouseButton.LEFT) {
|
|
218
|
+
this.state.setInkColor(colorSwatch.color);
|
|
219
|
+
this.requestRender();
|
|
220
|
+
}
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
event.stopPropagation();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!this.isCanvasChromeEvent(x, y, layout)) {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
event.stopPropagation();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const translated = {
|
|
232
|
+
type: event.type,
|
|
233
|
+
button: event.button,
|
|
234
|
+
x,
|
|
235
|
+
y,
|
|
236
|
+
scrollDirection: event.scroll?.direction,
|
|
237
|
+
};
|
|
238
|
+
this.state.handlePointerEvent(translated);
|
|
239
|
+
this.requestRender();
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
event.stopPropagation();
|
|
242
|
+
}
|
|
243
|
+
renderSelf(buffer) {
|
|
244
|
+
const layout = this.syncCanvasLayout();
|
|
245
|
+
this.frameBuffer.clear(COLORS.panel);
|
|
246
|
+
if (this.chromeMode === "full") {
|
|
247
|
+
if (this.width < MIN_WIDTH || this.height < MIN_HEIGHT) {
|
|
248
|
+
this.drawTooSmallMessage();
|
|
249
|
+
super.renderSelf(buffer);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.drawChrome(layout);
|
|
253
|
+
this.drawToolPalette(layout);
|
|
254
|
+
}
|
|
255
|
+
this.drawCanvas();
|
|
256
|
+
this.drawStartupLogo(layout);
|
|
257
|
+
super.renderSelf(buffer);
|
|
258
|
+
}
|
|
259
|
+
handleKeyPress(key) {
|
|
260
|
+
const name = key.name.toLowerCase();
|
|
261
|
+
this.dismissStartupLogo();
|
|
262
|
+
if ((this.cancelOnCtrlCEnabled && key.ctrl && name === "c") || (key.ctrl && name === "q")) {
|
|
263
|
+
key.preventDefault();
|
|
264
|
+
this.onCancelCallback?.();
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (name === "escape") {
|
|
268
|
+
key.preventDefault();
|
|
269
|
+
this.state.clearSelection();
|
|
270
|
+
this.requestRender();
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (name === "enter" || name === "return" || (key.ctrl && name === "s")) {
|
|
274
|
+
key.preventDefault();
|
|
275
|
+
this.onSaveCallback?.(this.state.exportArt());
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
if (name === "tab" || (key.ctrl && name === "t")) {
|
|
279
|
+
key.preventDefault();
|
|
280
|
+
this.state.cycleMode();
|
|
281
|
+
this.requestRender();
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (key.ctrl && !key.shift && name === "z") {
|
|
285
|
+
key.preventDefault();
|
|
286
|
+
this.state.undo();
|
|
287
|
+
this.requestRender();
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
if ((key.ctrl && name === "y") || (key.ctrl && key.shift && name === "z")) {
|
|
291
|
+
key.preventDefault();
|
|
292
|
+
this.state.redo();
|
|
293
|
+
this.requestRender();
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
if (key.ctrl && name === "x") {
|
|
297
|
+
key.preventDefault();
|
|
298
|
+
this.state.clearCanvas();
|
|
299
|
+
this.requestRender();
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (!this.state.isEditingText &&
|
|
303
|
+
this.state.hasSelectedObject &&
|
|
304
|
+
(name === "backspace" || name === "delete")) {
|
|
305
|
+
key.preventDefault();
|
|
306
|
+
this.state.deleteSelectedObject();
|
|
307
|
+
this.requestRender();
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (name === "up") {
|
|
311
|
+
key.preventDefault();
|
|
312
|
+
if (this.state.hasSelectedObject && !this.state.isEditingText) {
|
|
313
|
+
this.state.moveSelectedObjectBy(0, -1);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
this.state.moveCursor(0, -1);
|
|
317
|
+
}
|
|
318
|
+
this.requestRender();
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
if (name === "down") {
|
|
322
|
+
key.preventDefault();
|
|
323
|
+
if (this.state.hasSelectedObject && !this.state.isEditingText) {
|
|
324
|
+
this.state.moveSelectedObjectBy(0, 1);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
this.state.moveCursor(0, 1);
|
|
328
|
+
}
|
|
329
|
+
this.requestRender();
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
if (name === "left") {
|
|
333
|
+
key.preventDefault();
|
|
334
|
+
if (this.state.hasSelectedObject && !this.state.isEditingText) {
|
|
335
|
+
this.state.moveSelectedObjectBy(-1, 0);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.state.moveCursor(-1, 0);
|
|
339
|
+
}
|
|
340
|
+
this.requestRender();
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
if (name === "right") {
|
|
344
|
+
key.preventDefault();
|
|
345
|
+
if (this.state.hasSelectedObject && !this.state.isEditingText) {
|
|
346
|
+
this.state.moveSelectedObjectBy(1, 0);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
this.state.moveCursor(1, 0);
|
|
350
|
+
}
|
|
351
|
+
this.requestRender();
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if (this.state.currentMode === "box") {
|
|
355
|
+
if (key.raw === "[") {
|
|
356
|
+
key.preventDefault();
|
|
357
|
+
this.state.cycleBoxStyle(-1);
|
|
358
|
+
this.requestRender();
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
if (key.raw === "]") {
|
|
362
|
+
key.preventDefault();
|
|
363
|
+
this.state.cycleBoxStyle(1);
|
|
364
|
+
this.requestRender();
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (this.state.currentMode === "line" || this.state.currentMode === "paint") {
|
|
369
|
+
if (key.raw === "[") {
|
|
370
|
+
key.preventDefault();
|
|
371
|
+
this.state.cycleBrush(-1);
|
|
372
|
+
this.requestRender();
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
if (key.raw === "]") {
|
|
376
|
+
key.preventDefault();
|
|
377
|
+
this.state.cycleBrush(1);
|
|
378
|
+
this.requestRender();
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
if (name === "space") {
|
|
382
|
+
key.preventDefault();
|
|
383
|
+
this.state.stampBrushAtCursor();
|
|
384
|
+
this.requestRender();
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (name === "backspace" || name === "delete") {
|
|
388
|
+
key.preventDefault();
|
|
389
|
+
this.state.eraseAtCursor();
|
|
390
|
+
this.requestRender();
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
if (isPrintableKey(key)) {
|
|
394
|
+
key.preventDefault();
|
|
395
|
+
this.state.setBrush(key.raw);
|
|
396
|
+
this.requestRender();
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
if (this.state.currentMode === "text") {
|
|
402
|
+
if (name === "backspace") {
|
|
403
|
+
key.preventDefault();
|
|
404
|
+
this.state.backspace();
|
|
405
|
+
this.requestRender();
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
if (name === "delete") {
|
|
409
|
+
key.preventDefault();
|
|
410
|
+
this.state.deleteAtCursor();
|
|
411
|
+
this.requestRender();
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
if (name === "space") {
|
|
415
|
+
key.preventDefault();
|
|
416
|
+
this.state.insertCharacter(" ");
|
|
417
|
+
this.requestRender();
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
if (isPrintableKey(key)) {
|
|
421
|
+
key.preventDefault();
|
|
422
|
+
this.state.insertCharacter(key.raw);
|
|
423
|
+
this.requestRender();
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
dismissStartupLogo() {
|
|
430
|
+
if (!this.startupLogoEnabled || this.startupLogoDismissed)
|
|
431
|
+
return;
|
|
432
|
+
this.startupLogoDismissed = true;
|
|
433
|
+
this.requestRender();
|
|
434
|
+
}
|
|
435
|
+
syncCanvasLayout() {
|
|
436
|
+
if (this.chromeMode === "editor") {
|
|
437
|
+
this.state.ensureCanvasSize(this.width, this.height, EDITOR_CANVAS_INSETS);
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const layout = this.getLayout();
|
|
441
|
+
this.state.ensureCanvasSize(layout.canvasViewWidth, this.height, FULL_CHROME_CANVAS_INSETS);
|
|
442
|
+
return layout;
|
|
443
|
+
}
|
|
444
|
+
getLayout() {
|
|
445
|
+
const footerY = this.height - 2;
|
|
446
|
+
const bodyTop = 3;
|
|
447
|
+
const bodyBottom = this.height - 3;
|
|
448
|
+
const dividerX = this.width - TOOL_PALETTE_WIDTH - 2;
|
|
449
|
+
return {
|
|
450
|
+
dividerX,
|
|
451
|
+
paletteLeft: dividerX + 1,
|
|
452
|
+
paletteWidth: this.width - dividerX - 2,
|
|
453
|
+
bodyTop,
|
|
454
|
+
bodyBottom,
|
|
455
|
+
footerY,
|
|
456
|
+
canvasViewWidth: this.width - TOOL_PALETTE_WIDTH - 1,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
getPaletteButtonLeft(layout) {
|
|
460
|
+
return layout.paletteLeft + 1;
|
|
461
|
+
}
|
|
462
|
+
getToolButtons(layout) {
|
|
463
|
+
const buttonLeft = this.getPaletteButtonLeft(layout);
|
|
464
|
+
const firstTop = layout.bodyTop;
|
|
465
|
+
const boxTop = firstTop + TOOL_BUTTON_HEIGHT;
|
|
466
|
+
const lineTop = boxTop + TOOL_BUTTON_HEIGHT + BOX_STYLE_ROW_COUNT;
|
|
467
|
+
const paintTop = lineTop + TOOL_BUTTON_HEIGHT;
|
|
468
|
+
const textTop = paintTop + TOOL_BUTTON_HEIGHT;
|
|
469
|
+
return [
|
|
470
|
+
{
|
|
471
|
+
mode: "select",
|
|
472
|
+
left: buttonLeft,
|
|
473
|
+
top: firstTop,
|
|
474
|
+
width: TOOL_BUTTON_WIDTH,
|
|
475
|
+
height: TOOL_BUTTON_HEIGHT,
|
|
476
|
+
icon: "◎",
|
|
477
|
+
label: "Select",
|
|
478
|
+
color: COLORS.select,
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
mode: "box",
|
|
482
|
+
left: buttonLeft,
|
|
483
|
+
top: boxTop,
|
|
484
|
+
width: TOOL_BUTTON_WIDTH,
|
|
485
|
+
height: TOOL_BUTTON_HEIGHT,
|
|
486
|
+
icon: "▣",
|
|
487
|
+
label: "Box",
|
|
488
|
+
color: COLORS.warning,
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
mode: "line",
|
|
492
|
+
left: buttonLeft,
|
|
493
|
+
top: lineTop,
|
|
494
|
+
width: TOOL_BUTTON_WIDTH,
|
|
495
|
+
height: TOOL_BUTTON_HEIGHT,
|
|
496
|
+
icon: "╱",
|
|
497
|
+
label: "Line",
|
|
498
|
+
color: COLORS.accent,
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
mode: "paint",
|
|
502
|
+
left: buttonLeft,
|
|
503
|
+
top: paintTop,
|
|
504
|
+
width: TOOL_BUTTON_WIDTH,
|
|
505
|
+
height: TOOL_BUTTON_HEIGHT,
|
|
506
|
+
icon: "▒",
|
|
507
|
+
label: "Paint",
|
|
508
|
+
color: COLORS.paint,
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
mode: "text",
|
|
512
|
+
left: buttonLeft,
|
|
513
|
+
top: textTop,
|
|
514
|
+
width: TOOL_BUTTON_WIDTH,
|
|
515
|
+
height: TOOL_BUTTON_HEIGHT,
|
|
516
|
+
icon: "T",
|
|
517
|
+
label: "Text",
|
|
518
|
+
color: COLORS.success,
|
|
519
|
+
},
|
|
520
|
+
];
|
|
521
|
+
}
|
|
522
|
+
getBoxStyleButtons(layout) {
|
|
523
|
+
const buttonLeft = this.getPaletteButtonLeft(layout);
|
|
524
|
+
const firstTop = layout.bodyTop;
|
|
525
|
+
const boxTop = firstTop + TOOL_BUTTON_HEIGHT;
|
|
526
|
+
const stylesTop = boxTop + TOOL_BUTTON_HEIGHT;
|
|
527
|
+
return BOX_STYLE_OPTIONS.map((option, index) => ({
|
|
528
|
+
style: option.style,
|
|
529
|
+
left: buttonLeft,
|
|
530
|
+
top: stylesTop + index,
|
|
531
|
+
width: BOX_STYLE_BUTTON_WIDTH,
|
|
532
|
+
sample: option.sample,
|
|
533
|
+
label: option.label,
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
536
|
+
getColorSwatches(layout) {
|
|
537
|
+
const buttonLeft = this.getPaletteButtonLeft(layout);
|
|
538
|
+
const firstTop = layout.bodyTop;
|
|
539
|
+
const boxTop = firstTop + TOOL_BUTTON_HEIGHT;
|
|
540
|
+
const lineTop = boxTop + TOOL_BUTTON_HEIGHT + BOX_STYLE_ROW_COUNT;
|
|
541
|
+
const paintTop = lineTop + TOOL_BUTTON_HEIGHT;
|
|
542
|
+
const textTop = paintTop + TOOL_BUTTON_HEIGHT;
|
|
543
|
+
const colorTop = textTop + TOOL_BUTTON_HEIGHT + 1;
|
|
544
|
+
return INK_COLORS.map((color, index) => ({
|
|
545
|
+
color,
|
|
546
|
+
left: buttonLeft + (index % COLOR_SWATCH_COLUMNS) * COLOR_SWATCH_WIDTH,
|
|
547
|
+
top: colorTop + Math.floor(index / COLOR_SWATCH_COLUMNS),
|
|
548
|
+
width: COLOR_SWATCH_WIDTH,
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
isCanvasChromeEvent(x, y, layout) {
|
|
552
|
+
return (x >= this.state.canvasLeftCol &&
|
|
553
|
+
x <= layout.dividerX - 1 &&
|
|
554
|
+
y >= this.state.canvasTopRow &&
|
|
555
|
+
y <= layout.bodyBottom);
|
|
556
|
+
}
|
|
557
|
+
drawTooSmallMessage() {
|
|
558
|
+
const width = this.width;
|
|
559
|
+
const height = this.height;
|
|
560
|
+
const lines = [
|
|
561
|
+
"Terminal too small for termDRAW!.",
|
|
562
|
+
`Need at least ${MIN_WIDTH}x${MIN_HEIGHT}.`,
|
|
563
|
+
"Resize and try again.",
|
|
564
|
+
];
|
|
565
|
+
const startY = Math.max(0, Math.floor(height / 2) - 1);
|
|
566
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
567
|
+
const text = lines[index];
|
|
568
|
+
const x = Math.max(0, Math.floor((width - visibleCellCount(text)) / 2));
|
|
569
|
+
this.frameBuffer.drawText(text, x, startY + index, COLORS.warning, COLORS.panel, TextAttributes.BOLD);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
drawChrome(layout) {
|
|
573
|
+
this.drawHorizontalBorder(0, "╭", "╮");
|
|
574
|
+
this.drawHorizontalBorder(this.height - 1, "╰", "╯");
|
|
575
|
+
for (let y = 1; y < this.height - 1; y += 1) {
|
|
576
|
+
this.drawOuterSideBorders(y);
|
|
577
|
+
}
|
|
578
|
+
for (let y = 1; y <= layout.bodyBottom; y += 1) {
|
|
579
|
+
this.frameBuffer.setCell(layout.dividerX, y, "│", COLORS.border, COLORS.panel);
|
|
580
|
+
}
|
|
581
|
+
this.drawHeaderRow(layout);
|
|
582
|
+
this.drawHeaderDivider(layout);
|
|
583
|
+
this.drawFooterRow(layout);
|
|
584
|
+
}
|
|
585
|
+
drawHeaderRow(layout) {
|
|
586
|
+
const y = 1;
|
|
587
|
+
const canvasHeaderWidth = Math.max(1, layout.dividerX - 1);
|
|
588
|
+
const paletteWidth = Math.max(1, this.width - layout.dividerX - 2);
|
|
589
|
+
this.frameBuffer.drawText(" ".repeat(canvasHeaderWidth), 1, y, COLORS.text, COLORS.panel);
|
|
590
|
+
this.frameBuffer.drawText(" ".repeat(paletteWidth), layout.dividerX + 1, y, COLORS.text, COLORS.panel);
|
|
591
|
+
let x = 1;
|
|
592
|
+
x = drawSegment(this.frameBuffer, x, y, "termDRAW!", COLORS.accent, COLORS.panel, TextAttributes.BOLD);
|
|
593
|
+
x = drawSegment(this.frameBuffer, x, y, " tool:", COLORS.dim, COLORS.panel);
|
|
594
|
+
const modeLabel = this.state.getModeLabel();
|
|
595
|
+
const modeColor = this.state.currentMode === "select"
|
|
596
|
+
? COLORS.select
|
|
597
|
+
: this.state.currentMode === "line"
|
|
598
|
+
? COLORS.accent
|
|
599
|
+
: this.state.currentMode === "box"
|
|
600
|
+
? COLORS.warning
|
|
601
|
+
: this.state.currentMode === "paint"
|
|
602
|
+
? COLORS.paint
|
|
603
|
+
: COLORS.success;
|
|
604
|
+
x = drawSegment(this.frameBuffer, x, y, modeLabel, modeColor, COLORS.panel, TextAttributes.BOLD);
|
|
605
|
+
if (this.state.currentMode === "line" || this.state.currentMode === "paint") {
|
|
606
|
+
x = drawSegment(this.frameBuffer, x, y, " brush:", COLORS.dim, COLORS.panel);
|
|
607
|
+
x = drawSegment(this.frameBuffer, x, y, `"${this.state.currentBrush}"`, this.state.currentMode === "paint" ? COLORS.paint : COLORS.accent, COLORS.panel);
|
|
608
|
+
}
|
|
609
|
+
else if (this.state.currentMode === "box") {
|
|
610
|
+
const boxStyle = BOX_STYLE_OPTIONS.find((option) => option.style === this.state.currentBoxStyle) ??
|
|
611
|
+
BOX_STYLE_OPTIONS[0];
|
|
612
|
+
x = drawSegment(this.frameBuffer, x, y, " style:", COLORS.dim, COLORS.panel);
|
|
613
|
+
x = drawSegment(this.frameBuffer, x, y, `${boxStyle.sample} ${boxStyle.label}`, COLORS.warning, COLORS.panel);
|
|
614
|
+
}
|
|
615
|
+
x = drawSegment(this.frameBuffer, x, y, " color:", COLORS.dim, COLORS.panel);
|
|
616
|
+
drawSegment(this.frameBuffer, x, y, "●", getInkColorValue(this.state.currentInkColor), COLORS.panel, TextAttributes.BOLD);
|
|
617
|
+
const paletteTitle = padToWidth("Tools", paletteWidth);
|
|
618
|
+
this.frameBuffer.drawText(paletteTitle, layout.dividerX + 1, y, COLORS.dim, COLORS.panel, TextAttributes.BOLD);
|
|
619
|
+
}
|
|
620
|
+
drawHeaderDivider(layout) {
|
|
621
|
+
const y = 2;
|
|
622
|
+
this.frameBuffer.setCell(0, y, "├", COLORS.border, COLORS.panel);
|
|
623
|
+
for (let x = 1; x < this.width - 1; x += 1) {
|
|
624
|
+
this.frameBuffer.setCell(x, y, "─", COLORS.border, COLORS.panel);
|
|
625
|
+
}
|
|
626
|
+
this.frameBuffer.setCell(layout.dividerX, y, "┼", COLORS.border, COLORS.panel);
|
|
627
|
+
this.frameBuffer.setCell(this.width - 1, y, "┤", COLORS.border, COLORS.panel);
|
|
628
|
+
}
|
|
629
|
+
drawFooterRow(layout) {
|
|
630
|
+
const text = this.footerTextOverride ??
|
|
631
|
+
"Right palette tools/styles/colors • select tool can marquee multiple objects • drag box corners / line endpoints to edit • Esc deselect • Ctrl+Q quit";
|
|
632
|
+
const combined = `${text} ${this.state.currentStatus}`;
|
|
633
|
+
const padded = padToWidth(combined, Math.max(1, this.width - 2));
|
|
634
|
+
this.frameBuffer.drawText(padded, 1, layout.footerY, COLORS.dim, COLORS.panel);
|
|
635
|
+
}
|
|
636
|
+
drawToolPalette(layout) {
|
|
637
|
+
const paletteWidth = Math.max(1, this.width - layout.dividerX - 2);
|
|
638
|
+
const paletteX = layout.dividerX + 1;
|
|
639
|
+
for (let y = layout.bodyTop; y <= layout.bodyBottom; y += 1) {
|
|
640
|
+
this.frameBuffer.drawText(" ".repeat(paletteWidth), paletteX, y, COLORS.text, COLORS.panel);
|
|
641
|
+
}
|
|
642
|
+
for (const button of this.getToolButtons(layout)) {
|
|
643
|
+
this.drawToolButton(button);
|
|
644
|
+
}
|
|
645
|
+
for (const button of this.getBoxStyleButtons(layout)) {
|
|
646
|
+
this.drawBoxStyleButton(button);
|
|
647
|
+
}
|
|
648
|
+
this.drawColorPicker(layout);
|
|
649
|
+
}
|
|
650
|
+
drawToolButton(button) {
|
|
651
|
+
const isActive = this.state.currentMode === button.mode;
|
|
652
|
+
const fg = isActive ? COLORS.panel : button.color;
|
|
653
|
+
const bg = isActive ? button.color : COLORS.panel;
|
|
654
|
+
const borderColor = isActive ? button.color : COLORS.border;
|
|
655
|
+
this.frameBuffer.drawText(`┌${"─".repeat(button.width - 2)}┐`, button.left, button.top, borderColor, COLORS.panel, TextAttributes.BOLD);
|
|
656
|
+
const label = padToWidth(` ${button.icon} ${button.label} `, button.width - 2);
|
|
657
|
+
this.frameBuffer.drawText("│", button.left, button.top + 1, borderColor, COLORS.panel, TextAttributes.BOLD);
|
|
658
|
+
this.frameBuffer.drawText(label, button.left + 1, button.top + 1, fg, bg, TextAttributes.BOLD);
|
|
659
|
+
this.frameBuffer.drawText("│", button.left + button.width - 1, button.top + 1, borderColor, COLORS.panel, TextAttributes.BOLD);
|
|
660
|
+
this.frameBuffer.drawText(`└${"─".repeat(button.width - 2)}┘`, button.left, button.top + 2, borderColor, COLORS.panel, TextAttributes.BOLD);
|
|
661
|
+
}
|
|
662
|
+
drawBoxStyleButton(button) {
|
|
663
|
+
const isActive = this.state.currentBoxStyle === button.style;
|
|
664
|
+
const fg = isActive ? COLORS.panel : COLORS.text;
|
|
665
|
+
const bg = isActive ? COLORS.warning : COLORS.panel;
|
|
666
|
+
const text = padToWidth(`${button.sample} ${button.label}`, button.width);
|
|
667
|
+
this.frameBuffer.drawText(text, button.left, button.top, fg, bg, isActive ? TextAttributes.BOLD : TextAttributes.NONE);
|
|
668
|
+
}
|
|
669
|
+
drawColorPicker(layout) {
|
|
670
|
+
const buttonLeft = this.getPaletteButtonLeft(layout);
|
|
671
|
+
const firstTop = layout.bodyTop;
|
|
672
|
+
const boxTop = firstTop + TOOL_BUTTON_HEIGHT;
|
|
673
|
+
const lineTop = boxTop + TOOL_BUTTON_HEIGHT + BOX_STYLE_ROW_COUNT;
|
|
674
|
+
const paintTop = lineTop + TOOL_BUTTON_HEIGHT;
|
|
675
|
+
const textTop = paintTop + TOOL_BUTTON_HEIGHT;
|
|
676
|
+
const colorLabelTop = textTop + TOOL_BUTTON_HEIGHT;
|
|
677
|
+
this.frameBuffer.drawText("Color", buttonLeft, colorLabelTop, COLORS.dim, COLORS.panel);
|
|
678
|
+
for (const swatch of this.getColorSwatches(layout)) {
|
|
679
|
+
this.drawColorSwatch(swatch);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
drawColorSwatch(swatch) {
|
|
683
|
+
const isActive = this.state.currentInkColor === swatch.color;
|
|
684
|
+
const bg = getInkColorValue(swatch.color);
|
|
685
|
+
const fg = getInkColorContrast(swatch.color);
|
|
686
|
+
const text = isActive ? " • " : " ";
|
|
687
|
+
this.frameBuffer.drawText(text, swatch.left, swatch.top, fg, bg, isActive ? TextAttributes.BOLD : TextAttributes.NONE);
|
|
688
|
+
}
|
|
689
|
+
drawCanvas() {
|
|
690
|
+
const preview = this.state.getActivePreviewCharacters();
|
|
691
|
+
const marqueeChars = this.state.getSelectionMarqueeCharacters();
|
|
692
|
+
const selectedCells = this.state.getSelectedCellKeys();
|
|
693
|
+
const handleChars = this.state.getSelectionHandleCharacters();
|
|
694
|
+
for (let y = 0; y < this.state.height; y += 1) {
|
|
695
|
+
const rowY = this.state.canvasTopRow + y;
|
|
696
|
+
for (let x = 0; x < this.state.width; x += 1) {
|
|
697
|
+
const key = `${x},${y}`;
|
|
698
|
+
const handleChar = handleChars.get(key);
|
|
699
|
+
const marqueeChar = marqueeChars.get(key);
|
|
700
|
+
const previewChar = preview.get(key);
|
|
701
|
+
const cell = handleChar ?? marqueeChar ?? previewChar ?? this.state.getCompositeCell(x, y);
|
|
702
|
+
const cellColor = this.state.getCompositeColor(x, y);
|
|
703
|
+
const isCursor = x === this.state.currentCursorX && y === this.state.currentCursorY;
|
|
704
|
+
const isSelected = selectedCells.has(key);
|
|
705
|
+
const isHandle = handleChar !== undefined;
|
|
706
|
+
const isMarquee = marqueeChar !== undefined;
|
|
707
|
+
const fg = isCursor
|
|
708
|
+
? COLORS.cursorFg
|
|
709
|
+
: isHandle
|
|
710
|
+
? COLORS.handleFg
|
|
711
|
+
: isMarquee
|
|
712
|
+
? COLORS.select
|
|
713
|
+
: isSelected
|
|
714
|
+
? COLORS.selectionFg
|
|
715
|
+
: previewChar
|
|
716
|
+
? getInkColorValue(this.state.currentInkColor)
|
|
717
|
+
: cellColor
|
|
718
|
+
? getInkColorValue(cellColor)
|
|
719
|
+
: COLORS.text;
|
|
720
|
+
const bg = isCursor
|
|
721
|
+
? COLORS.cursorBg
|
|
722
|
+
: isHandle
|
|
723
|
+
? COLORS.handleBg
|
|
724
|
+
: isSelected
|
|
725
|
+
? COLORS.selectionBg
|
|
726
|
+
: COLORS.panel;
|
|
727
|
+
const attributes = isCursor || isSelected || isHandle || isMarquee
|
|
728
|
+
? TextAttributes.BOLD
|
|
729
|
+
: TextAttributes.NONE;
|
|
730
|
+
this.frameBuffer.setCell(x + this.state.canvasLeftCol, rowY, cell, fg, bg, attributes);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
drawStartupLogo(layout) {
|
|
735
|
+
if (!this.startupLogoEnabled || this.startupLogoDismissed)
|
|
736
|
+
return;
|
|
737
|
+
const logoWidth = Math.max(...STARTUP_LOGO_LINES.map((line) => visibleCellCount(line)));
|
|
738
|
+
const logoHeight = STARTUP_LOGO_LINES.length;
|
|
739
|
+
const captionWidth = visibleCellCount(STARTUP_LOGO_CAPTION);
|
|
740
|
+
const availableWidth = this.chromeMode === "full" && layout
|
|
741
|
+
? layout.dividerX - this.state.canvasLeftCol
|
|
742
|
+
: this.state.width;
|
|
743
|
+
const availableHeight = this.chromeMode === "full" && layout
|
|
744
|
+
? layout.bodyBottom - this.state.canvasTopRow + 1
|
|
745
|
+
: this.state.height;
|
|
746
|
+
const showCaption = availableWidth >= captionWidth && availableHeight >= logoHeight + 2;
|
|
747
|
+
const overlayHeight = showCaption ? logoHeight + 2 : logoHeight;
|
|
748
|
+
if (availableWidth < logoWidth || availableHeight < overlayHeight) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const startY = this.state.canvasTopRow + Math.floor((availableHeight - overlayHeight) / 2);
|
|
752
|
+
for (const [rowIndex, line] of STARTUP_LOGO_LINES.entries()) {
|
|
753
|
+
const y = startY + rowIndex;
|
|
754
|
+
const lineWidth = visibleCellCount(line);
|
|
755
|
+
const startX = this.state.canvasLeftCol + Math.floor((availableWidth - lineWidth) / 2);
|
|
756
|
+
for (const [colIndex, char] of Array.from(line).entries()) {
|
|
757
|
+
if (char === " ")
|
|
758
|
+
continue;
|
|
759
|
+
const x = startX + colIndex;
|
|
760
|
+
const fg = getStartupLogoColor(rowIndex, colIndex, line.length);
|
|
761
|
+
const attributes = rowIndex >= 2 ? TextAttributes.BOLD : TextAttributes.NONE;
|
|
762
|
+
this.frameBuffer.setCell(x, y, char, fg, COLORS.panel, attributes);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (showCaption) {
|
|
766
|
+
const captionY = startY + logoHeight + 1;
|
|
767
|
+
const captionX = this.state.canvasLeftCol + Math.floor((availableWidth - captionWidth) / 2);
|
|
768
|
+
this.frameBuffer.drawText(STARTUP_LOGO_CAPTION, captionX, captionY, getStartupLogoCaptionColor(), COLORS.panel, TextAttributes.DIM);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
drawOuterSideBorders(y) {
|
|
772
|
+
this.frameBuffer.setCell(0, y, "│", COLORS.border, COLORS.panel);
|
|
773
|
+
this.frameBuffer.setCell(this.width - 1, y, "│", COLORS.border, COLORS.panel);
|
|
774
|
+
}
|
|
775
|
+
drawHorizontalBorder(y, left, right) {
|
|
776
|
+
this.frameBuffer.setCell(0, y, left, COLORS.border, COLORS.panel);
|
|
777
|
+
for (let x = 1; x < this.width - 1; x += 1) {
|
|
778
|
+
this.frameBuffer.setCell(x, y, "─", COLORS.border, COLORS.panel);
|
|
779
|
+
}
|
|
780
|
+
this.frameBuffer.setCell(this.width - 1, y, right, COLORS.border, COLORS.panel);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
export class TermDrawAppRenderable extends TermDrawRenderable {
|
|
784
|
+
constructor(ctx, options = {}) {
|
|
785
|
+
super(ctx, { ...options, chromeMode: "full" });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
export class TermDrawEditorRenderable extends TermDrawRenderable {
|
|
789
|
+
constructor(ctx, options = {}) {
|
|
790
|
+
super(ctx, {
|
|
791
|
+
...options,
|
|
792
|
+
chromeMode: "editor",
|
|
793
|
+
showStartupLogo: options.showStartupLogo ?? false,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
export function formatSavedOutput(art, fenced) {
|
|
798
|
+
if (!fenced)
|
|
799
|
+
return art;
|
|
800
|
+
const content = art.length > 0 ? art : " ";
|
|
801
|
+
return `\`\`\`text\n${content}\n\`\`\``;
|
|
802
|
+
}
|
|
803
|
+
export function buildHelpText(binaryName = "termdraw") {
|
|
804
|
+
return truncateToCells(`${binaryName} [--output file] [--fenced|--plain]\n\n` +
|
|
805
|
+
`Controls:\n` +
|
|
806
|
+
` right palette click Select / Box / Line / Paint / Text, box styles, and colors\n` +
|
|
807
|
+
` Ctrl+T / Tab cycle select / box / line / paint / text\n` +
|
|
808
|
+
` select tool click to select, drag empty space to marquee-select multiple objects\n` +
|
|
809
|
+
` click objects select and move them\n` +
|
|
810
|
+
` drag handles resize boxes / adjust line endpoints\n` +
|
|
811
|
+
` selected text shows a virtual selection box\n` +
|
|
812
|
+
` Delete remove selected object\n` +
|
|
813
|
+
` Esc deselect\n` +
|
|
814
|
+
` Ctrl+Q quit\n` +
|
|
815
|
+
` Ctrl+Z / Ctrl+Y undo / redo\n` +
|
|
816
|
+
` Ctrl+X clear canvas\n` +
|
|
817
|
+
` [ / ] cycle box style or paint/line brush\n` +
|
|
818
|
+
` mouse wheel cycle box style or paint/line brush\n` +
|
|
819
|
+
` Space stamp brush in paint/line mode / insert space in text mode\n` +
|
|
820
|
+
` Enter / Ctrl+S save\n\n` +
|
|
821
|
+
`Options:\n` +
|
|
822
|
+
` -o, --output <file> write the result to a file\n` +
|
|
823
|
+
` --fenced output as a fenced markdown code block\n` +
|
|
824
|
+
` --plain output plain text (default)\n` +
|
|
825
|
+
` -h, --help show this help\n`, 4000);
|
|
826
|
+
}
|