blots-editor 0.0.9 → 0.0.11
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/core/document/document.d.ts +47 -4
- package/dist/core/document/document.js +215 -30
- package/dist/core/document/operations.js +16 -6
- package/dist/core/document/text-line.d.ts +13 -0
- package/dist/core/document/text-line.js +31 -0
- package/dist/core/editor-controller.js +14 -3
- package/dist/view/click-mapping.d.ts +9 -4
- package/dist/view/click-mapping.js +18 -6
- package/dist/view/editor.js +36 -19
- package/dist/view/logo.d.ts +4 -7
- package/dist/view/logo.js +82 -18
- package/dist/view/main-menu.js +10 -8
- package/package.json +6 -1
|
@@ -17,6 +17,8 @@ export declare class Document {
|
|
|
17
17
|
private _lines;
|
|
18
18
|
private _cursor;
|
|
19
19
|
private _scrollTop;
|
|
20
|
+
private _wrapWidth;
|
|
21
|
+
private _viewScrollTop;
|
|
20
22
|
readonly indentWidth: number;
|
|
21
23
|
constructor(text: string, options?: DocumentOptions);
|
|
22
24
|
get lineCount(): number;
|
|
@@ -25,6 +27,35 @@ export declare class Document {
|
|
|
25
27
|
getLine(i: number): string;
|
|
26
28
|
get cursor(): CursorState;
|
|
27
29
|
get scrollTop(): number;
|
|
30
|
+
/** Soft-wrap width in terminal cells; `Infinity` disables wrapping. */
|
|
31
|
+
get wrapWidth(): number;
|
|
32
|
+
/** Set the soft-wrap width (clamped to at least 1 cell). */
|
|
33
|
+
setWrapWidth(width: number): void;
|
|
34
|
+
/** The soft-wrap segments of one logical line (an empty line is one empty segment). */
|
|
35
|
+
private lineSegments;
|
|
36
|
+
/** Total number of visual (soft-wrapped) lines across the document. */
|
|
37
|
+
get visualLineCount(): number;
|
|
38
|
+
/**
|
|
39
|
+
* Map a global visual line to its logical line and the segment's logical
|
|
40
|
+
* span. `first` marks the segment that starts at the logical line start
|
|
41
|
+
* (the one that carries the line number).
|
|
42
|
+
*/
|
|
43
|
+
visualLineAt(vline: number): {
|
|
44
|
+
line: number;
|
|
45
|
+
start: number;
|
|
46
|
+
end: number;
|
|
47
|
+
first: boolean;
|
|
48
|
+
} | null;
|
|
49
|
+
/** Global visual line the cursor sits on. */
|
|
50
|
+
get cursorVisualLine(): number;
|
|
51
|
+
/** Visual offset of the cursor within its current soft-wrap segment. */
|
|
52
|
+
get cursorSegmentVisual(): number;
|
|
53
|
+
/**
|
|
54
|
+
* The target logical column for a vertical move onto `segment` (in any
|
|
55
|
+
* line): keep the cursor's offset within its current segment, clamping to
|
|
56
|
+
* the segment end only when the target segment is narrower.
|
|
57
|
+
*/
|
|
58
|
+
private moveToSegment;
|
|
28
59
|
/** Gutter width = decimal digit count of the last line number (at least 1). */
|
|
29
60
|
getLineNumberWidth(): number;
|
|
30
61
|
setLine(i: number, text: string): void;
|
|
@@ -40,6 +71,10 @@ export declare class Document {
|
|
|
40
71
|
* (see {@link TextLine.logicalAt}).
|
|
41
72
|
*/
|
|
42
73
|
setCursorAtVisual(line: number, visual: number): void;
|
|
74
|
+
/** Terminal column on a logical line → nearest logical column. */
|
|
75
|
+
logicalAtVisual(line: number, visual: number): number;
|
|
76
|
+
/** Logical column → terminal column on a logical line. */
|
|
77
|
+
visualAtLogical(line: number, logical: number): number;
|
|
43
78
|
moveLeft(): void;
|
|
44
79
|
moveRight(): void;
|
|
45
80
|
/** Vertical movement aligns by visual column, so wide-character lines do not drift. */
|
|
@@ -53,13 +88,21 @@ export declare class Document {
|
|
|
53
88
|
moveWordForward(): void;
|
|
54
89
|
/** A word is a run of non-whitespace; jump back to the current/previous word start. */
|
|
55
90
|
moveWordBackward(): void;
|
|
56
|
-
/** Page up: move the cursor up by `height - 1` lines (one line of overlap), keeping the visual column. */
|
|
91
|
+
/** Page up: move the cursor up by `height - 1` visual lines (one line of overlap), keeping the visual column. */
|
|
57
92
|
movePageUp(height: number): void;
|
|
58
93
|
movePageDown(height: number): void;
|
|
59
94
|
/**
|
|
60
|
-
* Scroll the viewport
|
|
61
|
-
*
|
|
62
|
-
*
|
|
95
|
+
* Scroll the viewport by `delta` visual lines without moving the cursor,
|
|
96
|
+
* clamped to the document range. Locks the view in place (Ctrl+wheel
|
|
97
|
+
* browsing) until the cursor moves.
|
|
98
|
+
*/
|
|
99
|
+
scrollView(delta: number, height: number): number;
|
|
100
|
+
/**
|
|
101
|
+
* Scroll the viewport so the cursor stays visible, in visual-line terms:
|
|
102
|
+
* if the cursor's visual line is above the viewport, scroll to it; if
|
|
103
|
+
* below, scroll so it becomes the last visible line. Returns the new
|
|
104
|
+
* scrollTop (in visual lines). While the view is manually locked
|
|
105
|
+
* (`scrollView`), the position is kept instead and only clamped.
|
|
63
106
|
*/
|
|
64
107
|
updateScroll(height: number): number;
|
|
65
108
|
}
|
|
@@ -14,6 +14,10 @@ export class Document {
|
|
|
14
14
|
_lines;
|
|
15
15
|
_cursor;
|
|
16
16
|
_scrollTop = 0;
|
|
17
|
+
_wrapWidth = Infinity;
|
|
18
|
+
// Non-null while the user is scrolling the view manually (Ctrl+wheel):
|
|
19
|
+
// updateScroll keeps this position instead of following the cursor.
|
|
20
|
+
_viewScrollTop = null;
|
|
17
21
|
indentWidth;
|
|
18
22
|
constructor(text, options = {}) {
|
|
19
23
|
this._lines = text.split(/\r?\n/).map((t) => new TextLine(t));
|
|
@@ -41,6 +45,106 @@ export class Document {
|
|
|
41
45
|
get scrollTop() {
|
|
42
46
|
return this._scrollTop;
|
|
43
47
|
}
|
|
48
|
+
/** Soft-wrap width in terminal cells; `Infinity` disables wrapping. */
|
|
49
|
+
get wrapWidth() {
|
|
50
|
+
return this._wrapWidth;
|
|
51
|
+
}
|
|
52
|
+
/** Set the soft-wrap width (clamped to at least 1 cell). */
|
|
53
|
+
setWrapWidth(width) {
|
|
54
|
+
this._wrapWidth = Math.max(1, width);
|
|
55
|
+
}
|
|
56
|
+
/** The soft-wrap segments of one logical line (an empty line is one empty segment). */
|
|
57
|
+
lineSegments(line) {
|
|
58
|
+
const tl = this._lines[line];
|
|
59
|
+
if (tl.text.length === 0) {
|
|
60
|
+
return [{ start: 0, end: 0 }];
|
|
61
|
+
}
|
|
62
|
+
const segs = [];
|
|
63
|
+
let segVisual = 0;
|
|
64
|
+
let segStart = 0;
|
|
65
|
+
while (segStart < tl.text.length) {
|
|
66
|
+
const seg = tl.segmentFrom(segVisual, this._wrapWidth);
|
|
67
|
+
segs.push({ start: segStart, end: seg.endLogical });
|
|
68
|
+
if (seg.text === "") {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
segStart = seg.endLogical;
|
|
72
|
+
segVisual = seg.endVisual;
|
|
73
|
+
}
|
|
74
|
+
return segs;
|
|
75
|
+
}
|
|
76
|
+
/** Total number of visual (soft-wrapped) lines across the document. */
|
|
77
|
+
get visualLineCount() {
|
|
78
|
+
let total = 0;
|
|
79
|
+
for (let i = 0; i < this._lines.length; i++) {
|
|
80
|
+
total += this.lineSegments(i).length;
|
|
81
|
+
}
|
|
82
|
+
return total;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Map a global visual line to its logical line and the segment's logical
|
|
86
|
+
* span. `first` marks the segment that starts at the logical line start
|
|
87
|
+
* (the one that carries the line number).
|
|
88
|
+
*/
|
|
89
|
+
visualLineAt(vline) {
|
|
90
|
+
if (vline < 0) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
let remaining = vline;
|
|
94
|
+
for (let line = 0; line < this._lines.length; line++) {
|
|
95
|
+
const segs = this.lineSegments(line);
|
|
96
|
+
if (remaining < segs.length) {
|
|
97
|
+
return {
|
|
98
|
+
line,
|
|
99
|
+
start: segs[remaining].start,
|
|
100
|
+
end: segs[remaining].end,
|
|
101
|
+
first: remaining === 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
remaining -= segs.length;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/** Global visual line the cursor sits on. */
|
|
109
|
+
get cursorVisualLine() {
|
|
110
|
+
const { line, logical } = this._cursor;
|
|
111
|
+
let v = 0;
|
|
112
|
+
for (let i = 0; i < line; i++) {
|
|
113
|
+
v += this.lineSegments(i).length;
|
|
114
|
+
}
|
|
115
|
+
const segs = this.lineSegments(line);
|
|
116
|
+
let idx = 0;
|
|
117
|
+
// A cursor exactly on a segment boundary belongs to the segment
|
|
118
|
+
// starting there, so visual lines read like real lines: the boundary
|
|
119
|
+
// column is the first cell of the next visual line.
|
|
120
|
+
while (idx < segs.length - 1 && logical >= segs[idx + 1].start) {
|
|
121
|
+
idx++;
|
|
122
|
+
}
|
|
123
|
+
return v + idx;
|
|
124
|
+
}
|
|
125
|
+
/** Visual offset of the cursor within its current soft-wrap segment. */
|
|
126
|
+
get cursorSegmentVisual() {
|
|
127
|
+
const tl = this._lines[this._cursor.line];
|
|
128
|
+
const segs = this.lineSegments(this._cursor.line);
|
|
129
|
+
const { logical } = this._cursor;
|
|
130
|
+
let idx = segs.length - 1;
|
|
131
|
+
while (idx > 0 && logical < segs[idx].start) {
|
|
132
|
+
idx--;
|
|
133
|
+
}
|
|
134
|
+
return tl.visualAt(logical) - tl.visualAt(segs[idx].start);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The target logical column for a vertical move onto `segment` (in any
|
|
138
|
+
* line): keep the cursor's offset within its current segment, clamping to
|
|
139
|
+
* the segment end only when the target segment is narrower.
|
|
140
|
+
*/
|
|
141
|
+
moveToSegment(segment) {
|
|
142
|
+
const offset = this.cursorSegmentVisual;
|
|
143
|
+
const targetVisual = this.visualAtLogical(segment.line, segment.start) + offset;
|
|
144
|
+
let col = this.logicalAtVisual(segment.line, targetVisual);
|
|
145
|
+
col = Math.min(Math.max(col, segment.start), segment.end);
|
|
146
|
+
this.setCursor(segment.line, col);
|
|
147
|
+
}
|
|
44
148
|
/** Gutter width = decimal digit count of the last line number (at least 1). */
|
|
45
149
|
getLineNumberWidth() {
|
|
46
150
|
return Math.max(String(this._lines.length - 1).length, 1);
|
|
@@ -59,8 +163,22 @@ export class Document {
|
|
|
59
163
|
/** Set the cursor, clamping out-of-range values (line to `[0, lineCount-1]`, column to end of line). */
|
|
60
164
|
setCursor(line, logical) {
|
|
61
165
|
const l = Math.max(0, Math.min(line, this._lines.length - 1));
|
|
62
|
-
const
|
|
63
|
-
|
|
166
|
+
const text = this._lines[l].text;
|
|
167
|
+
let col = Math.max(0, Math.min(logical, text.length));
|
|
168
|
+
// The cursor must never sit between the halves of a surrogate pair —
|
|
169
|
+
// typing there would split the emoji — so snap it to the pair start.
|
|
170
|
+
if (col > 0 &&
|
|
171
|
+
col < text.length &&
|
|
172
|
+
text.charCodeAt(col) >= 0xdc00 &&
|
|
173
|
+
text.charCodeAt(col) <= 0xdfff &&
|
|
174
|
+
text.charCodeAt(col - 1) >= 0xd800 &&
|
|
175
|
+
text.charCodeAt(col - 1) <= 0xdbff) {
|
|
176
|
+
col--;
|
|
177
|
+
}
|
|
178
|
+
this._cursor = { line: l, logical: col };
|
|
179
|
+
// Any cursor movement releases the manual view-scroll lock so the
|
|
180
|
+
// viewport follows the cursor again.
|
|
181
|
+
this._viewScrollTop = null;
|
|
64
182
|
}
|
|
65
183
|
/**
|
|
66
184
|
* Set the cursor by terminal column instead of code unit index — the
|
|
@@ -71,32 +189,70 @@ export class Document {
|
|
|
71
189
|
const l = Math.max(0, Math.min(line, this._lines.length - 1));
|
|
72
190
|
this.setCursor(l, this._lines[l].logicalAt(visual));
|
|
73
191
|
}
|
|
192
|
+
/** Terminal column on a logical line → nearest logical column. */
|
|
193
|
+
logicalAtVisual(line, visual) {
|
|
194
|
+
return this._lines[line].logicalAt(visual);
|
|
195
|
+
}
|
|
196
|
+
/** Logical column → terminal column on a logical line. */
|
|
197
|
+
visualAtLogical(line, logical) {
|
|
198
|
+
return this._lines[line].visualAt(logical);
|
|
199
|
+
}
|
|
74
200
|
moveLeft() {
|
|
75
201
|
const { line, logical } = this._cursor;
|
|
76
202
|
if (logical > 0) {
|
|
77
|
-
|
|
203
|
+
let target = logical - 1;
|
|
204
|
+
// Step over the whole emoji: stopping at its trailing half would
|
|
205
|
+
// leave the cursor inside the pair.
|
|
206
|
+
const ch = this._lines[line].text.charCodeAt(target);
|
|
207
|
+
if (target > 0 && ch >= 0xdc00 && ch <= 0xdfff) {
|
|
208
|
+
target--;
|
|
209
|
+
}
|
|
210
|
+
this.setCursor(line, target);
|
|
78
211
|
}
|
|
79
212
|
}
|
|
80
213
|
moveRight() {
|
|
81
214
|
const { line, logical } = this._cursor;
|
|
82
|
-
|
|
83
|
-
|
|
215
|
+
const text = this._lines[line].text;
|
|
216
|
+
if (logical < text.length) {
|
|
217
|
+
let target = logical + 1;
|
|
218
|
+
// Step over the whole emoji: stopping at its leading half would
|
|
219
|
+
// leave the cursor inside the pair.
|
|
220
|
+
const ch = text.charCodeAt(logical);
|
|
221
|
+
if (target < text.length && ch >= 0xd800 && ch <= 0xdbff) {
|
|
222
|
+
target++;
|
|
223
|
+
}
|
|
224
|
+
this.setCursor(line, target);
|
|
84
225
|
}
|
|
85
226
|
}
|
|
86
227
|
/** Vertical movement aligns by visual column, so wide-character lines do not drift. */
|
|
87
228
|
moveUp() {
|
|
88
229
|
const { line, logical } = this._cursor;
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
230
|
+
if (this.cursorVisualLine === 0) {
|
|
231
|
+
// Already on the first visual line: still move to the start of the
|
|
232
|
+
// current segment (like up-to-line-start in vim).
|
|
233
|
+
const segs = this.lineSegments(line);
|
|
234
|
+
let idx = 0;
|
|
235
|
+
while (idx < segs.length - 1 && logical >= segs[idx + 1].start) {
|
|
236
|
+
idx++;
|
|
237
|
+
}
|
|
238
|
+
this.setCursor(line, segs[idx].start);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const prev = this.visualLineAt(this.cursorVisualLine - 1);
|
|
242
|
+
if (!prev) {
|
|
243
|
+
return;
|
|
92
244
|
}
|
|
245
|
+
this.moveToSegment(prev);
|
|
93
246
|
}
|
|
94
247
|
moveDown() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const visual = this._lines[line].visualAt(logical);
|
|
98
|
-
this.setCursor(line + 1, this._lines[line + 1].logicalAt(visual));
|
|
248
|
+
if (this.cursorVisualLine >= this.visualLineCount - 1) {
|
|
249
|
+
return;
|
|
99
250
|
}
|
|
251
|
+
const next = this.visualLineAt(this.cursorVisualLine + 1);
|
|
252
|
+
if (!next) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.moveToSegment(next);
|
|
100
256
|
}
|
|
101
257
|
moveToLineStart() {
|
|
102
258
|
this.setCursor(this._cursor.line, 0);
|
|
@@ -140,34 +296,63 @@ export class Document {
|
|
|
140
296
|
}
|
|
141
297
|
this.setCursor(line, i);
|
|
142
298
|
}
|
|
143
|
-
/** Page up: move the cursor up by `height - 1` lines (one line of overlap), keeping the visual column. */
|
|
299
|
+
/** Page up: move the cursor up by `height - 1` visual lines (one line of overlap), keeping the visual column. */
|
|
144
300
|
movePageUp(height) {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
301
|
+
const target = Math.max(0, this.cursorVisualLine - Math.max(1, height - 1));
|
|
302
|
+
const seg = this.visualLineAt(target);
|
|
303
|
+
if (!seg) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.moveToSegment(seg);
|
|
149
307
|
}
|
|
150
308
|
movePageDown(height) {
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
309
|
+
const target = Math.min(this.visualLineCount - 1, this.cursorVisualLine + Math.max(1, height - 1));
|
|
310
|
+
const seg = this.visualLineAt(target);
|
|
311
|
+
if (!seg) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
this.moveToSegment(seg);
|
|
155
315
|
}
|
|
156
316
|
/**
|
|
157
|
-
* Scroll the viewport
|
|
158
|
-
*
|
|
159
|
-
*
|
|
317
|
+
* Scroll the viewport by `delta` visual lines without moving the cursor,
|
|
318
|
+
* clamped to the document range. Locks the view in place (Ctrl+wheel
|
|
319
|
+
* browsing) until the cursor moves.
|
|
320
|
+
*/
|
|
321
|
+
scrollView(delta, height) {
|
|
322
|
+
const total = this.visualLineCount;
|
|
323
|
+
const effectiveH = height > 0 ? height : total;
|
|
324
|
+
const maxScroll = Math.max(0, total - effectiveH);
|
|
325
|
+
const base = this._viewScrollTop ?? this._scrollTop;
|
|
326
|
+
const vs = Math.max(0, Math.min(base + delta, maxScroll));
|
|
327
|
+
this._viewScrollTop = vs;
|
|
328
|
+
this._scrollTop = vs;
|
|
329
|
+
return vs;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Scroll the viewport so the cursor stays visible, in visual-line terms:
|
|
333
|
+
* if the cursor's visual line is above the viewport, scroll to it; if
|
|
334
|
+
* below, scroll so it becomes the last visible line. Returns the new
|
|
335
|
+
* scrollTop (in visual lines). While the view is manually locked
|
|
336
|
+
* (`scrollView`), the position is kept instead and only clamped.
|
|
160
337
|
*/
|
|
161
338
|
updateScroll(height) {
|
|
162
|
-
const
|
|
339
|
+
const total = this.visualLineCount;
|
|
340
|
+
const effectiveH = height > 0 ? height : total;
|
|
341
|
+
const maxScroll = Math.max(0, total - effectiveH);
|
|
342
|
+
if (this._viewScrollTop !== null) {
|
|
343
|
+
const vs = Math.max(0, Math.min(this._viewScrollTop, maxScroll));
|
|
344
|
+
this._viewScrollTop = vs;
|
|
345
|
+
this._scrollTop = vs;
|
|
346
|
+
return vs;
|
|
347
|
+
}
|
|
348
|
+
const vline = this.cursorVisualLine;
|
|
163
349
|
let vs = this._scrollTop;
|
|
164
|
-
if (
|
|
165
|
-
vs =
|
|
350
|
+
if (vline < vs) {
|
|
351
|
+
vs = vline;
|
|
166
352
|
}
|
|
167
|
-
else if (
|
|
168
|
-
vs =
|
|
353
|
+
else if (vline >= vs + effectiveH) {
|
|
354
|
+
vs = vline - effectiveH + 1;
|
|
169
355
|
}
|
|
170
|
-
const maxScroll = Math.max(0, this._lines.length - effectiveH);
|
|
171
356
|
vs = Math.max(0, Math.min(vs, maxScroll));
|
|
172
357
|
this._scrollTop = vs;
|
|
173
358
|
return vs;
|
|
@@ -26,10 +26,15 @@ export class DeleteBeforeOp {
|
|
|
26
26
|
const { line, logical } = doc.cursor;
|
|
27
27
|
if (logical > 0) {
|
|
28
28
|
const cur = doc.getLine(line);
|
|
29
|
-
|
|
29
|
+
// Deleting only the trailing half of a surrogate pair would leave a
|
|
30
|
+
// broken half-rendering char, so delete the whole code point.
|
|
31
|
+
const last = cur.charCodeAt(logical - 1);
|
|
32
|
+
const isTrail = last >= 0xdc00 && last <= 0xdfff && logical >= 2;
|
|
33
|
+
const units = isTrail ? 2 : 1;
|
|
34
|
+
this._deletedChar = cur.slice(logical - units, logical);
|
|
30
35
|
this._joined = false;
|
|
31
|
-
doc.setLine(line, cur.slice(0, logical -
|
|
32
|
-
doc.setCursor(line, logical -
|
|
36
|
+
doc.setLine(line, cur.slice(0, logical - units) + cur.slice(logical));
|
|
37
|
+
doc.setCursor(line, logical - units);
|
|
33
38
|
}
|
|
34
39
|
else if (line > 0) {
|
|
35
40
|
const prev = doc.getLine(line - 1);
|
|
@@ -45,7 +50,7 @@ export class DeleteBeforeOp {
|
|
|
45
50
|
const { line, logical } = doc.cursor;
|
|
46
51
|
const cur = doc.getLine(line);
|
|
47
52
|
doc.setLine(line, cur.slice(0, logical) + this._deletedChar + cur.slice(logical));
|
|
48
|
-
doc.setCursor(line, logical +
|
|
53
|
+
doc.setCursor(line, logical + this._deletedChar.length);
|
|
49
54
|
}
|
|
50
55
|
else {
|
|
51
56
|
// Cursor sits at the end of the merged line; split it back at that point.
|
|
@@ -65,9 +70,14 @@ export class DeleteAfterOp {
|
|
|
65
70
|
const { line, logical } = doc.cursor;
|
|
66
71
|
const cur = doc.getLine(line);
|
|
67
72
|
if (logical < cur.length) {
|
|
68
|
-
|
|
73
|
+
// Deleting only the leading half of a surrogate pair would leave a
|
|
74
|
+
// broken half-rendering char, so delete the whole code point.
|
|
75
|
+
const first = cur.charCodeAt(logical);
|
|
76
|
+
const isLead = first >= 0xd800 && first <= 0xdbff && logical + 1 < cur.length;
|
|
77
|
+
const units = isLead ? 2 : 1;
|
|
78
|
+
this._deletedChar = cur.slice(logical, logical + units);
|
|
69
79
|
this._joined = false;
|
|
70
|
-
doc.setLine(line, cur.slice(0, logical) + cur.slice(logical +
|
|
80
|
+
doc.setLine(line, cur.slice(0, logical) + cur.slice(logical + units));
|
|
71
81
|
}
|
|
72
82
|
else if (line < doc.lineCount - 1) {
|
|
73
83
|
const next = doc.getLine(line + 1);
|
|
@@ -21,4 +21,17 @@ export declare class TextLine {
|
|
|
21
21
|
* (largest prefix not exceeding the target).
|
|
22
22
|
*/
|
|
23
23
|
logicalAt(visual: number): number;
|
|
24
|
+
/**
|
|
25
|
+
* Cut one soft-wrap segment starting at `startVisual`: the longest
|
|
26
|
+
* slice whose terminal width is at most `wrapWidth`. Returns the segment
|
|
27
|
+
* text plus the visual/logical position where it ends (the next call's
|
|
28
|
+
* `startVisual`). A single character wider than `wrapWidth` (or the
|
|
29
|
+
* trailing empty segment at line end) is returned on its own so the
|
|
30
|
+
* caller always makes progress.
|
|
31
|
+
*/
|
|
32
|
+
segmentFrom(startVisual: number, wrapWidth: number): {
|
|
33
|
+
text: string;
|
|
34
|
+
endVisual: number;
|
|
35
|
+
endLogical: number;
|
|
36
|
+
};
|
|
24
37
|
}
|
|
@@ -57,4 +57,35 @@ export class TextLine {
|
|
|
57
57
|
}
|
|
58
58
|
return lo;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Cut one soft-wrap segment starting at `startVisual`: the longest
|
|
62
|
+
* slice whose terminal width is at most `wrapWidth`. Returns the segment
|
|
63
|
+
* text plus the visual/logical position where it ends (the next call's
|
|
64
|
+
* `startVisual`). A single character wider than `wrapWidth` (or the
|
|
65
|
+
* trailing empty segment at line end) is returned on its own so the
|
|
66
|
+
* caller always makes progress.
|
|
67
|
+
*/
|
|
68
|
+
segmentFrom(startVisual, wrapWidth) {
|
|
69
|
+
const start = this.logicalAt(startVisual);
|
|
70
|
+
if (start >= this.text.length) {
|
|
71
|
+
return {
|
|
72
|
+
text: "",
|
|
73
|
+
endVisual: this.visualAt(start),
|
|
74
|
+
endLogical: start,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const startVisualActual = this.visualAt(start);
|
|
78
|
+
const limit = startVisualActual + Math.max(1, wrapWidth);
|
|
79
|
+
let end = this.logicalAt(limit);
|
|
80
|
+
// `logicalAt` may snap left onto a wide char; guarantee progress by
|
|
81
|
+
// taking at least one code point (a surrogate pair counts as one).
|
|
82
|
+
if (end <= start) {
|
|
83
|
+
end = start + (this.text.codePointAt(start) > 0xffff ? 2 : 1);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
text: this.text.slice(start, end),
|
|
87
|
+
endVisual: this.visualAt(end),
|
|
88
|
+
endLogical: end,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
60
91
|
}
|
|
@@ -80,11 +80,22 @@ export class EditorController {
|
|
|
80
80
|
const height = typeof args?.height === "number" ? args.height : 1;
|
|
81
81
|
doc.movePageDown(height);
|
|
82
82
|
});
|
|
83
|
-
// Mouse click: position the cursor by
|
|
83
|
+
// Mouse click: position the cursor by logical column (soft-wrap aware).
|
|
84
84
|
this.defineCommand("cursor.setPosition", (doc, args) => {
|
|
85
85
|
const line = typeof args?.line === "number" ? args.line : doc.cursor.line;
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
if (typeof args?.logical === "number") {
|
|
87
|
+
doc.setCursor(line, args.logical);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const visual = typeof args?.visual === "number" ? args.visual : doc.cursor.visual;
|
|
91
|
+
doc.setCursorAtVisual(line, visual);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// Ctrl+wheel: scroll the view without moving the cursor (clamped).
|
|
95
|
+
this.defineCommand("view.scroll", (doc, args) => {
|
|
96
|
+
const delta = typeof args?.delta === "number" ? args.delta : 0;
|
|
97
|
+
const height = typeof args?.height === "number" ? args.height : 1;
|
|
98
|
+
doc.scrollView(delta, height);
|
|
88
99
|
});
|
|
89
100
|
}
|
|
90
101
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import type { Document } from "../core/document/document.js";
|
|
1
2
|
export type ClickTarget = {
|
|
2
3
|
line: number;
|
|
3
|
-
|
|
4
|
+
logical: number;
|
|
4
5
|
};
|
|
5
6
|
/**
|
|
6
7
|
* Convert a 1-based terminal click (from `useMouseRegion`) into a document
|
|
7
|
-
* position, accounting for the line-number gutter
|
|
8
|
-
* offset.
|
|
8
|
+
* position, accounting for the line-number gutter, the vertical scroll
|
|
9
|
+
* offset, and soft-wrap segments.
|
|
9
10
|
*
|
|
10
11
|
* Assumes the region is the editor box itself with no padding, so
|
|
11
12
|
* `event.x - rect.x` is a 0-based cell column inside the region. Columns
|
|
12
13
|
* left of the gutter (clicks on the line numbers) snap to the text area's
|
|
13
14
|
* left edge; rows past the last document line clamp to it.
|
|
15
|
+
*
|
|
16
|
+
* The clicked visual line is mapped through `doc.visualLineAt`, so a click
|
|
17
|
+
* on a wrapped continuation lands on the same logical line with the segment
|
|
18
|
+
* offset translated back to a logical column.
|
|
14
19
|
*/
|
|
15
20
|
export declare function clickToPosition(event: {
|
|
16
21
|
x: number;
|
|
@@ -20,4 +25,4 @@ export declare function clickToPosition(event: {
|
|
|
20
25
|
y: number;
|
|
21
26
|
width: number;
|
|
22
27
|
height: number;
|
|
23
|
-
}, gutterWidth: number, visibleStart: number,
|
|
28
|
+
}, gutterWidth: number, visibleStart: number, doc: Document): ClickTarget;
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert a 1-based terminal click (from `useMouseRegion`) into a document
|
|
3
|
-
* position, accounting for the line-number gutter
|
|
4
|
-
* offset.
|
|
3
|
+
* position, accounting for the line-number gutter, the vertical scroll
|
|
4
|
+
* offset, and soft-wrap segments.
|
|
5
5
|
*
|
|
6
6
|
* Assumes the region is the editor box itself with no padding, so
|
|
7
7
|
* `event.x - rect.x` is a 0-based cell column inside the region. Columns
|
|
8
8
|
* left of the gutter (clicks on the line numbers) snap to the text area's
|
|
9
9
|
* left edge; rows past the last document line clamp to it.
|
|
10
|
+
*
|
|
11
|
+
* The clicked visual line is mapped through `doc.visualLineAt`, so a click
|
|
12
|
+
* on a wrapped continuation lands on the same logical line with the segment
|
|
13
|
+
* offset translated back to a logical column.
|
|
10
14
|
*/
|
|
11
|
-
export function clickToPosition(event, rect, gutterWidth, visibleStart,
|
|
15
|
+
export function clickToPosition(event, rect, gutterWidth, visibleStart, doc) {
|
|
12
16
|
const localX = event.x - rect.x;
|
|
13
17
|
const localY = event.y - rect.y;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
18
|
+
const vline = visibleStart + localY;
|
|
19
|
+
const seg = doc.visualLineAt(vline);
|
|
20
|
+
if (!seg) {
|
|
21
|
+
const last = doc.lineCount - 1;
|
|
22
|
+
return { line: last, logical: doc.getLine(last).length };
|
|
23
|
+
}
|
|
24
|
+
const segmentOffset = Math.max(0, localX - gutterWidth);
|
|
25
|
+
const segStartVisual = doc.visualAtLogical(seg.line, seg.start);
|
|
26
|
+
const col = doc.logicalAtVisual(seg.line, segStartVisual + segmentOffset);
|
|
27
|
+
const logical = Math.min(Math.max(col, seg.start), seg.end);
|
|
28
|
+
return { line: seg.line, logical };
|
|
17
29
|
}
|
package/dist/view/editor.js
CHANGED
|
@@ -110,44 +110,61 @@ export function Editor({ value: initialText = "", lineNumberRightSpacing = 1, cu
|
|
|
110
110
|
switchMode,
|
|
111
111
|
]);
|
|
112
112
|
const ref = useRef(null);
|
|
113
|
-
const { height } = useBoxMetrics(ref);
|
|
113
|
+
const { height, width } = useBoxMetrics(ref);
|
|
114
114
|
const doc = controller.document;
|
|
115
115
|
const lineNumberWidth = doc.getLineNumberWidth();
|
|
116
|
+
// Soft-wrap the text at the editable width minus one cell, leaving a
|
|
117
|
+
// right-hand margin so the last column never hugs the edge.
|
|
118
|
+
doc.setWrapWidth(width - lineNumberWidth - lineNumberRightSpacing - 1);
|
|
116
119
|
const visibleStart = doc.updateScroll(height);
|
|
117
|
-
const effectiveH = height > 0 ? height : doc.
|
|
118
|
-
const visibleLines = doc.lines.slice(visibleStart, visibleStart + effectiveH);
|
|
120
|
+
const effectiveH = height > 0 ? height : doc.visualLineCount;
|
|
119
121
|
const cursor = doc.cursor;
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
+
const cursorVisualLine = doc.cursorVisualLine;
|
|
123
|
+
const cursorX = doc.cursorSegmentVisual + lineNumberWidth + lineNumberRightSpacing + cursorOffset;
|
|
124
|
+
const cursorY = cursorVisualLine - visibleStart + cursorHeightOffset;
|
|
122
125
|
setCursorPosition({ x: cursorX, y: cursorY });
|
|
123
|
-
// Mouse: clicks position the cursor in any mode
|
|
124
|
-
//
|
|
126
|
+
// Mouse: clicks position the cursor in any mode. A plain wheel moves the
|
|
127
|
+
// cursor one visual line per notch; Ctrl+wheel scrolls the view without
|
|
128
|
+
// moving the cursor (clamped to the document). Callbacks close over this
|
|
129
|
+
// render's metrics.
|
|
125
130
|
const mouseRef = useMouseRegion({
|
|
126
131
|
onClick: (event, rect) => {
|
|
127
|
-
const target = clickToPosition(event, rect, lineNumberWidth + lineNumberRightSpacing, doc.scrollTop, doc
|
|
132
|
+
const target = clickToPosition(event, rect, lineNumberWidth + lineNumberRightSpacing, doc.scrollTop, doc);
|
|
128
133
|
controller.execute("cursor.setPosition", {
|
|
129
134
|
line: target.line,
|
|
130
|
-
|
|
135
|
+
logical: target.logical,
|
|
131
136
|
});
|
|
132
137
|
},
|
|
133
138
|
onWheel: (event) => {
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
const up = event.button === "wheel-up";
|
|
140
|
+
const down = event.button === "wheel-down";
|
|
141
|
+
if (!up && !down) {
|
|
142
|
+
return;
|
|
136
143
|
}
|
|
137
|
-
|
|
138
|
-
controller.execute("
|
|
144
|
+
if (event.ctrl) {
|
|
145
|
+
controller.execute("view.scroll", {
|
|
146
|
+
delta: up ? -1 : 1,
|
|
147
|
+
height,
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
139
150
|
}
|
|
151
|
+
controller.execute(up ? "cursor.pageUp" : "cursor.pageDown", {
|
|
152
|
+
height: 1,
|
|
153
|
+
});
|
|
140
154
|
},
|
|
141
155
|
});
|
|
142
156
|
const modeText = mode === "normal" ? t("editor.mode.normal") : t("editor.mode.insert");
|
|
143
157
|
return (React.createElement(Box, { flexDirection: "column", height: "100%", width: "100%" },
|
|
144
158
|
React.createElement(InformationBar, { mode: modeText, cursor: { line: cursor.line, column: cursor.visual } }),
|
|
145
159
|
React.createElement(Box, { ref: mouseRef, flexGrow: 1, width: "100%", backgroundColor: "#1e1e1e" },
|
|
146
|
-
React.createElement(Box, { ref: ref, height: "100%", width: "100%", flexDirection: "column" },
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
React.createElement(Box, { ref: ref, height: "100%", width: "100%", flexDirection: "column" }, Array.from({ length: effectiveH }, (_, i) => {
|
|
161
|
+
const vline = visibleStart + i;
|
|
162
|
+
const seg = doc.visualLineAt(vline);
|
|
163
|
+
if (!seg) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return (React.createElement(Box, { key: vline, flexDirection: "row" },
|
|
167
|
+
React.createElement(Box, { width: lineNumberWidth, justifyContent: "flex-end", marginRight: lineNumberRightSpacing }, seg.first ? (React.createElement(Text, { bold: cursor.line === seg.line }, seg.line)) : null),
|
|
168
|
+
React.createElement(Text, null, doc.getLine(seg.line).slice(seg.start, seg.end))));
|
|
152
169
|
})))));
|
|
153
170
|
}
|
package/dist/view/logo.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* To swap the style, replace these lines with your own art, keeping the
|
|
6
|
-
* white/gray per-line split.
|
|
2
|
+
* Pick the logo variant for the current terminal size. Wider screens get the
|
|
3
|
+
* words side by side; narrower screens stack them, and shrinking height steps
|
|
4
|
+
* the font down (block → simple → chrome → tiny) so buttons stay visible.
|
|
7
5
|
*/
|
|
8
|
-
export declare
|
|
9
|
-
export declare const LOGO_GRAY: string[];
|
|
6
|
+
export declare function getLogo(columns: number, rows: number): string;
|
package/dist/view/logo.js
CHANGED
|
@@ -1,23 +1,87 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import cfonts from "cfonts";
|
|
3
|
+
import gradient from "gradient-string";
|
|
1
4
|
/**
|
|
2
|
-
* "Blots Editor"
|
|
3
|
-
* shaded edges give a pseudo-3D look. "BLOTS" renders white, "EDITOR" gray.
|
|
5
|
+
* "Blots Editor" rainbow-gradient logo.
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* Generated with the same stack `oh-my-logo --filled` uses (cfonts block
|
|
8
|
+
* glyphs + gradient-string palette) but as a plain string, since
|
|
9
|
+
* oh-my-logo's own `renderFilled` renders through its own ink instance and
|
|
10
|
+
* cannot be embedded. `chalk.level` is forced so the gradient survives
|
|
11
|
+
* non-interactive output (pipes, tests).
|
|
12
|
+
*
|
|
13
|
+
* `getLogo(columns, rows)` picks a size tier from terminal dimensions so the
|
|
14
|
+
* logo shrinks instead of crowding out the menu buttons on small screens.
|
|
7
15
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
16
|
+
chalk.level = 3;
|
|
17
|
+
const RAINBOW = [
|
|
18
|
+
"#FF0000",
|
|
19
|
+
"#FF7F00",
|
|
20
|
+
"#FFFF00",
|
|
21
|
+
"#00FF00",
|
|
22
|
+
"#0000FF",
|
|
23
|
+
"#8B00FF",
|
|
15
24
|
];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const FONTS = {
|
|
26
|
+
block: "block",
|
|
27
|
+
simple: "simple",
|
|
28
|
+
chrome: "chrome",
|
|
29
|
+
tiny: "tiny",
|
|
30
|
+
};
|
|
31
|
+
const renderBlock = (text, font) => {
|
|
32
|
+
const result = cfonts.render(text, {
|
|
33
|
+
font: FONTS[font],
|
|
34
|
+
align: "left",
|
|
35
|
+
colors: ["system"],
|
|
36
|
+
backgroundColor: "transparent",
|
|
37
|
+
letterSpacing: 1,
|
|
38
|
+
lineHeight: 1,
|
|
39
|
+
space: true,
|
|
40
|
+
maxLength: 0,
|
|
41
|
+
});
|
|
42
|
+
// cfonts returns `false` when it cannot render; fall back to plain text.
|
|
43
|
+
const art = (result === false ? text : result.string).replace(/^\n+|\n+$/g, "");
|
|
44
|
+
return art.split("\n");
|
|
45
|
+
};
|
|
46
|
+
/** Side-by-side layout: "Blots Editor" on shared lines. */
|
|
47
|
+
const wide = (font) => {
|
|
48
|
+
const blots = renderBlock("Blots", font);
|
|
49
|
+
const editor = renderBlock("Editor", font);
|
|
50
|
+
return blots.map((line, i) => `${line} ${editor[i] ?? ""}`).join("\n");
|
|
51
|
+
};
|
|
52
|
+
/** Stacked layout: "Blots" above "Editor". */
|
|
53
|
+
const narrow = (font) => `${renderBlock("Blots", font).join("\n")}\n\n${renderBlock("Editor", font).join("\n")}`;
|
|
54
|
+
/** Same gradient pass across the whole logo, whichever layout/font. */
|
|
55
|
+
const toLogo = (plain) => gradient(RAINBOW).multiline(plain);
|
|
56
|
+
/** Size tiers: (min rows, min cols for side-by-side) → (layout, font). */
|
|
57
|
+
const TIERS = [
|
|
58
|
+
// Wide screens: words side by side; shrink the font as height drops.
|
|
59
|
+
{ minCols: 104, minRows: 26, build: () => wide("block") },
|
|
60
|
+
{ minCols: 104, minRows: 20, build: () => wide("simple") },
|
|
61
|
+
{ minCols: 104, minRows: 0, build: () => wide("tiny") },
|
|
62
|
+
// Narrow screens: words stacked; shrink the font as height drops.
|
|
63
|
+
{ minCols: 0, minRows: 30, build: () => narrow("block") },
|
|
64
|
+
{ minCols: 0, minRows: 24, build: () => narrow("simple") },
|
|
65
|
+
{ minCols: 0, minRows: 20, build: () => narrow("chrome") },
|
|
66
|
+
{ minCols: 0, minRows: 0, build: () => narrow("tiny") },
|
|
23
67
|
];
|
|
68
|
+
const cache = new Map();
|
|
69
|
+
/**
|
|
70
|
+
* Pick the logo variant for the current terminal size. Wider screens get the
|
|
71
|
+
* words side by side; narrower screens stack them, and shrinking height steps
|
|
72
|
+
* the font down (block → simple → chrome → tiny) so buttons stay visible.
|
|
73
|
+
*/
|
|
74
|
+
export function getLogo(columns, rows) {
|
|
75
|
+
for (const tier of TIERS) {
|
|
76
|
+
if (columns >= tier.minCols && rows >= tier.minRows) {
|
|
77
|
+
const key = tier.build.toString();
|
|
78
|
+
let logo = cache.get(key);
|
|
79
|
+
if (!logo) {
|
|
80
|
+
logo = toLogo(tier.build());
|
|
81
|
+
cache.set(key, logo);
|
|
82
|
+
}
|
|
83
|
+
return logo;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return toLogo(wide("block"));
|
|
87
|
+
}
|
package/dist/view/main-menu.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Box, Text } from "ink";
|
|
1
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
2
2
|
import { useI18n, useKeyboard, useMouseRegion } from "ink-cartridge";
|
|
3
3
|
import React, { useEffect, useState } from "react";
|
|
4
4
|
import { Editor } from "./editor.js";
|
|
5
5
|
import { Settings } from "./settings.js";
|
|
6
6
|
import { gotoScreen } from "ink-cartridge";
|
|
7
|
-
import {
|
|
7
|
+
import { getLogo } from "./logo.js";
|
|
8
8
|
/**
|
|
9
9
|
* A borderless text button: the plain Box provides the mouse hit area,
|
|
10
10
|
* hovering inverts the text as visual feedback, disabled buttons dim.
|
|
@@ -30,6 +30,11 @@ function MenuButton({ label, shortcut, disabled, onClick }) {
|
|
|
30
30
|
export function MainMenu() {
|
|
31
31
|
const { t } = useI18n();
|
|
32
32
|
const { boundKeyboard } = useKeyboard();
|
|
33
|
+
// The wide logo needs ≈100 columns; below that the words stack vertically.
|
|
34
|
+
// On short screens `getLogo` also shrinks the font so buttons stay visible.
|
|
35
|
+
const { columns, rows } = useWindowSize();
|
|
36
|
+
const narrow = columns < 104;
|
|
37
|
+
const logo = getLogo(columns, rows);
|
|
33
38
|
useEffect(() => {
|
|
34
39
|
const unbinds = [
|
|
35
40
|
boundKeyboard(["e"], () => gotoScreen(Editor, {})),
|
|
@@ -39,12 +44,9 @@ export function MainMenu() {
|
|
|
39
44
|
return () => unbinds.forEach((fn) => fn());
|
|
40
45
|
}, [boundKeyboard]);
|
|
41
46
|
return (React.createElement(Box, { flexDirection: "column", justifyContent: "center", alignItems: "center", width: "100%", height: "100%" },
|
|
42
|
-
React.createElement(Box, { flexDirection: "column",
|
|
43
|
-
React.createElement(Text,
|
|
44
|
-
|
|
45
|
-
" "),
|
|
46
|
-
React.createElement(Text, { color: "gray" }, LOGO_GRAY[i]))))),
|
|
47
|
-
React.createElement(Box, { flexDirection: "row", gap: 6 },
|
|
47
|
+
React.createElement(Box, { flexDirection: "column", marginBottom: narrow ? 2 : 6 },
|
|
48
|
+
React.createElement(Text, null, logo)),
|
|
49
|
+
React.createElement(Box, { flexDirection: narrow ? "column" : "row", gap: narrow ? 1 : 6, alignItems: "center" },
|
|
48
50
|
React.createElement(MenuButton, { label: t("menu.edit"), shortcut: "e", onClick: () => gotoScreen(Editor, {}) }),
|
|
49
51
|
React.createElement(MenuButton, { label: t("menu.settings"), shortcut: "s", onClick: () => gotoScreen(Settings, {}) }),
|
|
50
52
|
React.createElement(MenuButton, { label: t("menu.other"), disabled: true }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blots-editor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "A lightweight Markdown editor for the terminal, built on ink-cartridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -51,10 +51,15 @@
|
|
|
51
51
|
"react": ">=18"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"cfonts": "^3.3.1",
|
|
55
|
+
"chalk": "^4.1.2",
|
|
56
|
+
"gradient-string": "^2.0.2",
|
|
54
57
|
"ink-cartridge": "*",
|
|
58
|
+
"oh-my-logo": "^0.5.0",
|
|
55
59
|
"string-width": "^8.2.2"
|
|
56
60
|
},
|
|
57
61
|
"devDependencies": {
|
|
62
|
+
"@types/gradient-string": "^1.1.6",
|
|
58
63
|
"@types/node": "^20.19.39",
|
|
59
64
|
"@types/react": "^19.2.14",
|
|
60
65
|
"ink": "^7.1.1",
|