@trendr/core 0.2.10 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +284 -24
- package/index.js +6 -2
- package/package.json +29 -34
- package/src/animation.js +25 -12
- package/src/ansi.js +7 -0
- package/src/buffer.js +63 -50
- package/src/button.js +2 -2
- package/src/checkbox.js +2 -2
- package/src/diff-engine.js +313 -0
- package/src/diff-view.js +238 -0
- package/src/diff.js +2 -2
- package/src/dropdown.js +147 -0
- package/src/focus.js +89 -27
- package/src/hooks.js +43 -25
- package/src/hotkey.js +51 -16
- package/src/input.js +211 -72
- package/src/layout.js +34 -13
- package/src/list.js +29 -16
- package/src/markdown.js +177 -0
- package/src/menu.js +13 -6
- package/src/menubar.js +46 -115
- package/src/miller-nav.js +36 -11
- package/src/modal.js +3 -1
- package/src/pick-list.js +16 -17
- package/src/progress.js +3 -3
- package/src/radio.js +16 -5
- package/src/renderer.js +315 -58
- package/src/scheduler.js +28 -10
- package/src/scroll-box.js +12 -11
- package/src/scrollable-text.js +3 -2
- package/src/select.js +65 -173
- package/src/selection.js +92 -0
- package/src/shimmer.js +2 -2
- package/src/signal.js +20 -3
- package/src/table.js +8 -27
- package/src/tabs.js +10 -7
- package/src/task.js +3 -3
- package/src/text-area.js +96 -41
- package/src/text-input.js +70 -28
- package/src/wrap.js +205 -84
package/src/text-area.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
|
-
import { useInput, useLayout, useCursor } from './hooks.js'
|
|
3
|
+
import { useInput, useLayout, useCursor, useTheme } from './hooks.js'
|
|
4
4
|
import { registerHook } from './renderer.js'
|
|
5
|
+
import { charWidth, measureText } from './wrap.js'
|
|
6
|
+
|
|
7
|
+
function prevBoundary(s, i) {
|
|
8
|
+
if (i <= 0) return 0
|
|
9
|
+
const c = s.charCodeAt(i - 1)
|
|
10
|
+
return c >= 0xdc00 && c <= 0xdfff && i >= 2 ? i - 2 : i - 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function nextBoundary(s, i) {
|
|
14
|
+
if (i >= s.length) return s.length
|
|
15
|
+
const c = s.charCodeAt(i)
|
|
16
|
+
return c >= 0xd800 && c <= 0xdbff && i + 1 < s.length ? i + 2 : i + 1
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
export function wrapForEditor(text, width) {
|
|
7
20
|
if (width <= 0) return [{ start: 0, end: 0, hard: true }]
|
|
@@ -25,21 +38,32 @@ export function wrapForEditor(text, width) {
|
|
|
25
38
|
while (segStart < segment.length) {
|
|
26
39
|
const remaining = segment.slice(segStart)
|
|
27
40
|
|
|
28
|
-
if (remaining
|
|
41
|
+
if (measureText(remaining) <= width) {
|
|
29
42
|
lines.push({ start: pos + segStart, end: pos + segStart + remaining.length, hard: true })
|
|
30
43
|
segStart += remaining.length
|
|
31
44
|
break
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
let fit = 0
|
|
48
|
+
let col = 0
|
|
49
|
+
while (fit < remaining.length) {
|
|
50
|
+
const cp = remaining.codePointAt(fit)
|
|
51
|
+
const cw = charWidth(cp)
|
|
52
|
+
if (col + cw > width) break
|
|
53
|
+
col += cw
|
|
54
|
+
fit += cp > 0xffff ? 2 : 1
|
|
55
|
+
}
|
|
56
|
+
if (fit === 0) fit = nextBoundary(remaining, 0)
|
|
57
|
+
|
|
58
|
+
const chunk = remaining.slice(0, fit)
|
|
35
59
|
const lastSpace = chunk.lastIndexOf(' ')
|
|
36
60
|
|
|
37
61
|
if (lastSpace > 0) {
|
|
38
62
|
lines.push({ start: pos + segStart, end: pos + segStart + lastSpace, hard: false })
|
|
39
63
|
segStart += lastSpace + 1
|
|
40
64
|
} else {
|
|
41
|
-
lines.push({ start: pos + segStart, end: pos + segStart +
|
|
42
|
-
segStart +=
|
|
65
|
+
lines.push({ start: pos + segStart, end: pos + segStart + fit, hard: false })
|
|
66
|
+
segStart += fit
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
69
|
|
|
@@ -57,26 +81,39 @@ export function wrapForEditor(text, width) {
|
|
|
57
81
|
return lines
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
export function cursorToDisplay(cursor, lineMap) {
|
|
84
|
+
export function cursorToDisplay(cursor, lineMap, text) {
|
|
61
85
|
for (let row = 0; row < lineMap.length; row++) {
|
|
62
86
|
const line = lineMap[row]
|
|
63
87
|
if (cursor >= line.start && cursor <= line.end) {
|
|
64
88
|
if (cursor === line.end && !line.hard && row + 1 < lineMap.length) {
|
|
65
89
|
return { row: row + 1, col: 0 }
|
|
66
90
|
}
|
|
67
|
-
|
|
91
|
+
const col = text != null ? measureText(text.slice(line.start, cursor)) : cursor - line.start
|
|
92
|
+
return { row, col }
|
|
68
93
|
}
|
|
69
94
|
}
|
|
70
95
|
const last = lineMap[lineMap.length - 1]
|
|
71
|
-
|
|
96
|
+
const col = text != null ? measureText(text.slice(last.start, last.end)) : last.end - last.start
|
|
97
|
+
return { row: lineMap.length - 1, col }
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
export function displayToCursor(row, col, lineMap) {
|
|
100
|
+
export function displayToCursor(row, col, lineMap, text) {
|
|
75
101
|
const r = Math.max(0, Math.min(row, lineMap.length - 1))
|
|
76
102
|
const line = lineMap[r]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
if (text == null) {
|
|
104
|
+
const maxCol = line.end - line.start
|
|
105
|
+
return line.start + Math.max(0, Math.min(col, maxCol))
|
|
106
|
+
}
|
|
107
|
+
let i = line.start
|
|
108
|
+
let c = 0
|
|
109
|
+
while (i < line.end && c < col) {
|
|
110
|
+
const cp = text.codePointAt(i)
|
|
111
|
+
const w = charWidth(cp)
|
|
112
|
+
if (c + w > col) break
|
|
113
|
+
c += w
|
|
114
|
+
i += cp > 0xffff ? 2 : 1
|
|
115
|
+
}
|
|
116
|
+
return i
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
function ensureVisible(cursorRow, scroll, height, totalLines) {
|
|
@@ -92,9 +129,10 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
92
129
|
const [cursor, setCursor] = createSignal(0)
|
|
93
130
|
if (valueProp !== undefined && valueProp !== value()) {
|
|
94
131
|
setValue(valueProp)
|
|
95
|
-
setCursor(
|
|
132
|
+
setCursor(valueProp.length)
|
|
96
133
|
}
|
|
97
134
|
const ref = registerHook(() => ({ scroll: 0, goalCol: null }))
|
|
135
|
+
const { muted = 'gray' } = useTheme()
|
|
98
136
|
const layout = useLayout()
|
|
99
137
|
const { cursorStyle, reset: resetBlink } = useCursor(cursorProp, focused)
|
|
100
138
|
|
|
@@ -111,6 +149,7 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
111
149
|
|
|
112
150
|
if (onKeyDown) {
|
|
113
151
|
event.value = value()
|
|
152
|
+
event.cursor = cursor()
|
|
114
153
|
if (onKeyDown(event)) {
|
|
115
154
|
event.stopPropagation()
|
|
116
155
|
return
|
|
@@ -132,9 +171,9 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
132
171
|
|
|
133
172
|
const isSubmitKey = submitOnEnter
|
|
134
173
|
? (key === 'return' && !meta && !shift)
|
|
135
|
-
: (meta && key === '
|
|
174
|
+
: (meta && key === 'return')
|
|
136
175
|
const isNewlineKey = submitOnEnter
|
|
137
|
-
? ((shift && key === 'return') || (meta && key === '
|
|
176
|
+
? ((shift && key === 'return') || (meta && key === 'return'))
|
|
138
177
|
: (key === 'return')
|
|
139
178
|
|
|
140
179
|
if (isSubmitKey) {
|
|
@@ -160,26 +199,29 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
160
199
|
}
|
|
161
200
|
|
|
162
201
|
if (key === 'backspace') {
|
|
163
|
-
if (c > 0)
|
|
202
|
+
if (c > 0) {
|
|
203
|
+
const p = prevBoundary(v, c)
|
|
204
|
+
update(v.slice(0, p) + v.slice(c), p)
|
|
205
|
+
}
|
|
164
206
|
event.stopPropagation()
|
|
165
207
|
return
|
|
166
208
|
}
|
|
167
209
|
|
|
168
210
|
if (key === 'delete') {
|
|
169
|
-
if (c < v.length) update(v.slice(0, c) + v.slice(c
|
|
211
|
+
if (c < v.length) update(v.slice(0, c) + v.slice(nextBoundary(v, c)), c)
|
|
170
212
|
event.stopPropagation()
|
|
171
213
|
return
|
|
172
214
|
}
|
|
173
215
|
|
|
174
216
|
if (key === 'left') {
|
|
175
|
-
setCursor(
|
|
217
|
+
setCursor(prevBoundary(v, c))
|
|
176
218
|
ref.goalCol = null
|
|
177
219
|
event.stopPropagation()
|
|
178
220
|
return
|
|
179
221
|
}
|
|
180
222
|
|
|
181
223
|
if (key === 'right') {
|
|
182
|
-
setCursor(
|
|
224
|
+
setCursor(nextBoundary(v, c))
|
|
183
225
|
ref.goalCol = null
|
|
184
226
|
event.stopPropagation()
|
|
185
227
|
return
|
|
@@ -188,48 +230,59 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
188
230
|
if (key === 'up' || key === 'down') {
|
|
189
231
|
const w = layout.width || 80
|
|
190
232
|
const lineMap = wrapForEditor(v, w)
|
|
191
|
-
const pos = cursorToDisplay(c, lineMap)
|
|
233
|
+
const pos = cursorToDisplay(c, lineMap, v)
|
|
192
234
|
const goal = ref.goalCol !== null ? ref.goalCol : pos.col
|
|
193
235
|
ref.goalCol = goal
|
|
194
236
|
|
|
195
237
|
const newRow = key === 'up' ? pos.row - 1 : pos.row + 1
|
|
196
238
|
if (newRow >= 0 && newRow < lineMap.length) {
|
|
197
|
-
setCursor(displayToCursor(newRow, goal, lineMap))
|
|
239
|
+
setCursor(displayToCursor(newRow, goal, lineMap, v))
|
|
198
240
|
}
|
|
199
241
|
event.stopPropagation()
|
|
200
242
|
return
|
|
201
243
|
}
|
|
202
244
|
|
|
203
|
-
if (key === 'home' || (ctrl &&
|
|
245
|
+
if (key === 'home' || (ctrl && key === 'a')) {
|
|
204
246
|
const w = layout.width || 80
|
|
205
247
|
const lineMap = wrapForEditor(v, w)
|
|
206
|
-
const pos = cursorToDisplay(c, lineMap)
|
|
248
|
+
const pos = cursorToDisplay(c, lineMap, v)
|
|
207
249
|
setCursor(lineMap[pos.row].start)
|
|
208
250
|
ref.goalCol = null
|
|
209
251
|
event.stopPropagation()
|
|
210
252
|
return
|
|
211
253
|
}
|
|
212
254
|
|
|
213
|
-
if (key === 'end' || (ctrl &&
|
|
255
|
+
if (key === 'end' || (ctrl && key === 'e')) {
|
|
214
256
|
const w = layout.width || 80
|
|
215
257
|
const lineMap = wrapForEditor(v, w)
|
|
216
|
-
const pos = cursorToDisplay(c, lineMap)
|
|
258
|
+
const pos = cursorToDisplay(c, lineMap, v)
|
|
217
259
|
setCursor(lineMap[pos.row].end)
|
|
218
260
|
ref.goalCol = null
|
|
219
261
|
event.stopPropagation()
|
|
220
262
|
return
|
|
221
263
|
}
|
|
222
264
|
|
|
223
|
-
if (ctrl &&
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
265
|
+
if (ctrl && key === 'u') {
|
|
266
|
+
const lineStart = v.lastIndexOf('\n', c - 1) + 1
|
|
267
|
+
const rest = v.slice(c)
|
|
268
|
+
const lineEmptyAfter = rest.length === 0 || rest[0] === '\n'
|
|
269
|
+
if (c > lineStart && !(lineEmptyAfter && lineStart > 0)) {
|
|
270
|
+
// delete to start of the current line, staying on it
|
|
271
|
+
update(v.slice(0, lineStart) + rest, lineStart)
|
|
272
|
+
} else if (lineStart > 0) {
|
|
273
|
+
// nothing left to delete on this line (or the line is now empty): remove
|
|
274
|
+
// the line break above and land at the end of the previous line, so
|
|
275
|
+
// repeated ctrl+u keeps eating lines upward
|
|
276
|
+
update(v.slice(0, lineStart - 1) + rest, lineStart - 1)
|
|
277
|
+
} else if (c > lineStart) {
|
|
278
|
+
// first line: just clear it
|
|
279
|
+
update(rest, 0)
|
|
280
|
+
}
|
|
228
281
|
event.stopPropagation()
|
|
229
282
|
return
|
|
230
283
|
}
|
|
231
284
|
|
|
232
|
-
if (ctrl &&
|
|
285
|
+
if (ctrl && key === 'k') {
|
|
233
286
|
const after = v.slice(c)
|
|
234
287
|
const nlIdx = after.indexOf('\n')
|
|
235
288
|
const deleteEnd = nlIdx === -1 ? v.length : c + nlIdx
|
|
@@ -238,7 +291,7 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
238
291
|
return
|
|
239
292
|
}
|
|
240
293
|
|
|
241
|
-
if (ctrl &&
|
|
294
|
+
if (ctrl && key === 'w') {
|
|
242
295
|
const before = v.slice(0, c)
|
|
243
296
|
const after = v.slice(c)
|
|
244
297
|
const trimmed = before.replace(/\S+\s*$/, '')
|
|
@@ -247,7 +300,7 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
247
300
|
return
|
|
248
301
|
}
|
|
249
302
|
|
|
250
|
-
if (!ctrl && !meta && raw.length
|
|
303
|
+
if (!ctrl && !meta && raw.length >= 1 && raw >= ' ' && !raw.startsWith('\x1b')) {
|
|
251
304
|
update(v.slice(0, c) + raw + v.slice(c), c + raw.length)
|
|
252
305
|
event.stopPropagation()
|
|
253
306
|
}
|
|
@@ -259,12 +312,12 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
259
312
|
const cs = cursorStyle()
|
|
260
313
|
|
|
261
314
|
if (!v && placeholder && !focused) {
|
|
262
|
-
return jsx('text', { style: { color:
|
|
315
|
+
return jsx('text', { style: { color: muted, flexGrow: 1 }, children: placeholder })
|
|
263
316
|
}
|
|
264
317
|
|
|
265
318
|
const effectiveWidth = w || 80
|
|
266
319
|
const lineMap = wrapForEditor(v, effectiveWidth)
|
|
267
|
-
const displayPos = cursorToDisplay(c, lineMap)
|
|
320
|
+
const displayPos = cursorToDisplay(c, lineMap, v)
|
|
268
321
|
|
|
269
322
|
const displayHeight = Math.max(1, Math.min(lineMap.length, maxHeight))
|
|
270
323
|
ref.scroll = ensureVisible(displayPos.row, ref.scroll, displayHeight, lineMap.length)
|
|
@@ -273,13 +326,14 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
273
326
|
const visibleLines = lineMap.slice(scroll, scroll + displayHeight)
|
|
274
327
|
|
|
275
328
|
if (!v && placeholder && focused) {
|
|
329
|
+
const first = placeholder.slice(0, nextBoundary(placeholder, 0))
|
|
276
330
|
return jsx('box', {
|
|
277
331
|
style: { flexDirection: 'column', height: 1, minHeight: 1, flexGrow: 1 },
|
|
278
332
|
children: jsxs('box', {
|
|
279
333
|
style: { flexDirection: 'row', height: 1 },
|
|
280
334
|
children: [
|
|
281
|
-
jsx('text', { style: cs ? { ...cs, color: cs.color ??
|
|
282
|
-
placeholder.length >
|
|
335
|
+
jsx('text', { style: cs ? { ...cs, color: cs.color ?? muted } : { inverse: true, color: muted }, children: first }),
|
|
336
|
+
placeholder.length > first.length && jsx('text', { style: { color: muted }, children: placeholder.slice(first.length) }),
|
|
283
337
|
],
|
|
284
338
|
}),
|
|
285
339
|
})
|
|
@@ -294,10 +348,11 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
|
|
|
294
348
|
return jsx('text', { key: row, children: content || ' ' })
|
|
295
349
|
}
|
|
296
350
|
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
const
|
|
351
|
+
const cursorIdx = Math.max(line.start, Math.min(c, line.end))
|
|
352
|
+
const nb = cursorIdx < line.end ? nextBoundary(v, cursorIdx) : cursorIdx
|
|
353
|
+
const before = v.slice(line.start, cursorIdx)
|
|
354
|
+
const cursorChar = cursorIdx < line.end ? v.slice(cursorIdx, nb) : ' '
|
|
355
|
+
const after = v.slice(nb, line.end)
|
|
301
356
|
|
|
302
357
|
return jsxs('box', {
|
|
303
358
|
key: row,
|
package/src/text-input.js
CHANGED
|
@@ -1,13 +1,47 @@
|
|
|
1
1
|
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
|
-
import { useInput, useLayout, useCursor } from './hooks.js'
|
|
3
|
+
import { useInput, useLayout, useCursor, useTheme } from './hooks.js'
|
|
4
|
+
import { charWidth, measureText } from './wrap.js'
|
|
4
5
|
|
|
5
6
|
const BOX = { flexDirection: 'row', height: 1, minHeight: 1, flexGrow: 1 }
|
|
6
7
|
|
|
8
|
+
function prevBoundary(s, i) {
|
|
9
|
+
if (i <= 0) return 0
|
|
10
|
+
const c = s.charCodeAt(i - 1)
|
|
11
|
+
return c >= 0xdc00 && c <= 0xdfff && i >= 2 ? i - 2 : i - 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function nextBoundary(s, i) {
|
|
15
|
+
if (i >= s.length) return s.length
|
|
16
|
+
const c = s.charCodeAt(i)
|
|
17
|
+
return c >= 0xd800 && c <= 0xdbff && i + 1 < s.length ? i + 2 : i + 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sliceByColumns(s, startCol, endCol) {
|
|
21
|
+
let col = 0
|
|
22
|
+
let i = 0
|
|
23
|
+
let out = ''
|
|
24
|
+
while (i < s.length) {
|
|
25
|
+
const cp = s.codePointAt(i)
|
|
26
|
+
const len = cp > 0xffff ? 2 : 1
|
|
27
|
+
const w = charWidth(cp)
|
|
28
|
+
if (col + w > endCol) break
|
|
29
|
+
if (col >= startCol) out += s.slice(i, i + len)
|
|
30
|
+
col += w
|
|
31
|
+
i += len
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function firstCodePoint(s) {
|
|
37
|
+
return s.slice(0, nextBoundary(s, 0))
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused = true, initialValue, clearOnSubmit = false, cursor: cursorProp }) {
|
|
8
41
|
const init = initialValue ?? ''
|
|
9
42
|
const [value, setValue] = createSignal(init)
|
|
10
43
|
const [cursor, setCursor] = createSignal(init.length)
|
|
44
|
+
const { muted = 'gray' } = useTheme()
|
|
11
45
|
const layout = useLayout()
|
|
12
46
|
const { cursorStyle, reset: resetBlink } = useCursor(cursorProp, focused)
|
|
13
47
|
|
|
@@ -21,10 +55,17 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
|
|
|
21
55
|
if (!focused) return
|
|
22
56
|
resetBlink()
|
|
23
57
|
|
|
24
|
-
const { key, raw, ctrl } = event
|
|
58
|
+
const { key, raw, ctrl, meta } = event
|
|
25
59
|
const v = value()
|
|
26
60
|
const c = cursor()
|
|
27
61
|
|
|
62
|
+
if (key === 'paste') {
|
|
63
|
+
const pasted = (event.text || '').replace(/^\n+|\n+$/g, '').replace(/\n+/g, ' ')
|
|
64
|
+
if (pasted) update(v.slice(0, c) + pasted + v.slice(c), c + pasted.length)
|
|
65
|
+
event.stopPropagation()
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
28
69
|
if (key === 'return') {
|
|
29
70
|
if (onSubmit) {
|
|
30
71
|
onSubmit(v)
|
|
@@ -43,27 +84,30 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
|
|
|
43
84
|
}
|
|
44
85
|
|
|
45
86
|
if (key === 'backspace') {
|
|
46
|
-
if (c > 0)
|
|
87
|
+
if (c > 0) {
|
|
88
|
+
const p = prevBoundary(v, c)
|
|
89
|
+
update(v.slice(0, p) + v.slice(c), p)
|
|
90
|
+
}
|
|
47
91
|
event.stopPropagation()
|
|
48
92
|
return
|
|
49
93
|
}
|
|
50
94
|
|
|
51
95
|
if (key === 'delete') {
|
|
52
|
-
if (c < v.length) update(v.slice(0, c) + v.slice(c
|
|
96
|
+
if (c < v.length) update(v.slice(0, c) + v.slice(nextBoundary(v, c)), c)
|
|
53
97
|
event.stopPropagation()
|
|
54
98
|
return
|
|
55
99
|
}
|
|
56
100
|
|
|
57
|
-
if (key === 'left') { setCursor(
|
|
58
|
-
if (key === 'right') { setCursor(
|
|
101
|
+
if (key === 'left') { setCursor(prevBoundary(v, c)); event.stopPropagation(); return }
|
|
102
|
+
if (key === 'right') { setCursor(nextBoundary(v, c)); event.stopPropagation(); return }
|
|
59
103
|
|
|
60
|
-
if (key === 'home' || (ctrl &&
|
|
61
|
-
if (key === 'end' || (ctrl &&
|
|
104
|
+
if (key === 'home' || (ctrl && key === 'a')) { setCursor(0); event.stopPropagation(); return }
|
|
105
|
+
if (key === 'end' || (ctrl && key === 'e')) { setCursor(v.length); event.stopPropagation(); return }
|
|
62
106
|
|
|
63
|
-
if (ctrl &&
|
|
64
|
-
if (ctrl &&
|
|
107
|
+
if (ctrl && key === 'u') { update(v.slice(c), 0); event.stopPropagation(); return }
|
|
108
|
+
if (ctrl && key === 'k') { update(v.slice(0, c), c); event.stopPropagation(); return }
|
|
65
109
|
|
|
66
|
-
if (ctrl &&
|
|
110
|
+
if (ctrl && key === 'w') {
|
|
67
111
|
const before = v.slice(0, c)
|
|
68
112
|
const after = v.slice(c)
|
|
69
113
|
const trimmed = before.replace(/\S+\s*$/, '')
|
|
@@ -72,7 +116,7 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
|
|
|
72
116
|
return
|
|
73
117
|
}
|
|
74
118
|
|
|
75
|
-
if (!ctrl && raw.length
|
|
119
|
+
if (!ctrl && !meta && raw.length >= 1 && raw >= ' ' && !raw.startsWith('\x1b')) {
|
|
76
120
|
update(v.slice(0, c) + raw + v.slice(c), c + raw.length)
|
|
77
121
|
event.stopPropagation()
|
|
78
122
|
}
|
|
@@ -84,44 +128,42 @@ export function TextInput({ onSubmit, onCancel, onChange, placeholder, focused =
|
|
|
84
128
|
const cs = cursorStyle()
|
|
85
129
|
|
|
86
130
|
if (!v && placeholder && !focused) {
|
|
87
|
-
return jsx('text', { style: { color:
|
|
131
|
+
return jsx('text', { style: { color: muted, flexGrow: 1 }, children: placeholder })
|
|
88
132
|
}
|
|
89
133
|
|
|
90
134
|
if (!v && placeholder && focused) {
|
|
135
|
+
const first = firstCodePoint(placeholder)
|
|
91
136
|
return jsxs('box', {
|
|
92
137
|
style: BOX,
|
|
93
138
|
children: [
|
|
94
|
-
jsx('text', { style: cs ? { ...cs, color: cs.color ??
|
|
95
|
-
placeholder.length >
|
|
139
|
+
jsx('text', { style: cs ? { ...cs, color: cs.color ?? muted } : { inverse: true, color: muted }, children: first }),
|
|
140
|
+
placeholder.length > first.length && jsx('text', { style: { color: muted }, children: placeholder.slice(first.length) }),
|
|
96
141
|
],
|
|
97
142
|
})
|
|
98
143
|
}
|
|
99
144
|
|
|
100
|
-
const
|
|
145
|
+
const cursorCol = measureText(v.slice(0, c))
|
|
146
|
+
const contentWidth = measureText(v) + 1
|
|
101
147
|
const needsScroll = w > 0 && contentWidth > w
|
|
102
148
|
|
|
149
|
+
const cpAtCursor = c < v.length ? v.slice(c, nextBoundary(v, c)) : ''
|
|
150
|
+
const cursorChar = cpAtCursor || ' '
|
|
151
|
+
const cursorWidth = cpAtCursor ? measureText(cpAtCursor) : 1
|
|
152
|
+
|
|
103
153
|
if (!needsScroll) {
|
|
104
|
-
const cursorChar = v[c] || ' '
|
|
105
154
|
return jsxs('box', {
|
|
106
155
|
style: BOX,
|
|
107
156
|
children: [
|
|
108
157
|
v.slice(0, c) && jsx('text', { children: v.slice(0, c) }),
|
|
109
158
|
jsx('text', { style: cs ?? {}, children: cursorChar }),
|
|
110
|
-
v.slice(c
|
|
159
|
+
v.slice(nextBoundary(v, c)) && jsx('text', { children: v.slice(nextBoundary(v, c)) }),
|
|
111
160
|
],
|
|
112
161
|
})
|
|
113
162
|
}
|
|
114
163
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const visible = v.slice(scrollStart, scrollStart + w)
|
|
121
|
-
const cursorInView = c - scrollStart
|
|
122
|
-
const before = visible.slice(0, cursorInView)
|
|
123
|
-
const cursorChar = visible[cursorInView] || ' '
|
|
124
|
-
const after = visible.slice(cursorInView + 1)
|
|
164
|
+
const scrollStart = Math.max(0, cursorCol + cursorWidth - w)
|
|
165
|
+
const before = sliceByColumns(v, scrollStart, cursorCol)
|
|
166
|
+
const after = sliceByColumns(v, cursorCol + cursorWidth, scrollStart + w)
|
|
125
167
|
|
|
126
168
|
return jsxs('box', {
|
|
127
169
|
style: BOX,
|