ethagent 4.3.1 → 4.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app/input/AppInputProvider.tsx +10 -129
- package/src/cli/main.tsx +3 -6
- package/src/cli/reset.ts +4 -9
- package/src/ui/TextArea.tsx +9 -0
- package/src/ui/TextInput.tsx +8 -0
- package/src/app/input/appInputParser.ts +0 -294
package/package.json
CHANGED
|
@@ -1,135 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react'
|
|
5
|
-
import { useStdin, useStdout } from 'ink'
|
|
6
|
-
import type { AppInputEvent } from './appInputParser.js'
|
|
7
|
-
import {
|
|
8
|
-
BRACKETED_PASTE_DISABLE,
|
|
9
|
-
BRACKETED_PASTE_ENABLE,
|
|
10
|
-
createAppInputParseState,
|
|
11
|
-
DISABLE_KITTY_KEYBOARD,
|
|
12
|
-
DISABLE_MODIFY_OTHER_KEYS,
|
|
13
|
-
ENABLE_KITTY_KEYBOARD,
|
|
14
|
-
ENABLE_MODIFY_OTHER_KEYS,
|
|
15
|
-
hasPendingAppInput,
|
|
16
|
-
parseAppInput,
|
|
17
|
-
} from './appInputParser.js'
|
|
1
|
+
import { useInput, type Key } from 'ink'
|
|
18
2
|
|
|
19
|
-
|
|
20
|
-
const DEBUG_LOG_PATH = path.join(os.homedir(), '.ethagent', 'input-debug.log')
|
|
3
|
+
export type { Key }
|
|
21
4
|
|
|
22
|
-
|
|
23
|
-
if (!DEBUG_INPUT) return
|
|
24
|
-
try {
|
|
25
|
-
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8')
|
|
26
|
-
const codepoints = [...text].map(c => c.codePointAt(0)?.toString(16).padStart(4, '0') ?? '????').join(' ')
|
|
27
|
-
const hex = Buffer.isBuffer(chunk) ? chunk.toString('hex') : Buffer.from(chunk, 'utf8').toString('hex')
|
|
28
|
-
const line = `[${new Date().toISOString()}] codepoints=${codepoints} hex=${hex} text=${JSON.stringify(text)}\n`
|
|
29
|
-
fs.appendFileSync(DEBUG_LOG_PATH, line)
|
|
30
|
-
} catch {
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type InputHandler = (input: string, key: AppInputEvent['key'], event: AppInputEvent) => void
|
|
35
|
-
|
|
36
|
-
type HandlerEntry = {
|
|
37
|
-
handlerRef: React.MutableRefObject<InputHandler>
|
|
38
|
-
isActiveRef: React.MutableRefObject<boolean>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type AppInputContextValue = {
|
|
42
|
-
register(entry: HandlerEntry): () => void
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const AppInputContext = createContext<AppInputContextValue | null>(null)
|
|
46
|
-
const PENDING_ESCAPE_FLUSH_MS = 50
|
|
47
|
-
|
|
48
|
-
export const AppInputProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
49
|
-
const { stdin } = useStdin()
|
|
50
|
-
const { stdout } = useStdout()
|
|
51
|
-
const handlersRef = useRef<Set<HandlerEntry>>(new Set())
|
|
52
|
-
const parseStateRef = useRef(createAppInputParseState())
|
|
53
|
-
const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
54
|
-
|
|
55
|
-
const dispatch = useCallback((event: AppInputEvent) => {
|
|
56
|
-
for (const entry of handlersRef.current) {
|
|
57
|
-
if (!entry.isActiveRef.current) continue
|
|
58
|
-
entry.handlerRef.current(event.input, event.key, event)
|
|
59
|
-
}
|
|
60
|
-
}, [])
|
|
5
|
+
export type InputHandler = (input: string, key: Key) => void
|
|
61
6
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const result = parseAppInput(parseStateRef.current, null)
|
|
65
|
-
parseStateRef.current = result.state
|
|
66
|
-
for (const event of result.events) dispatch(event)
|
|
67
|
-
}, [dispatch])
|
|
68
|
-
|
|
69
|
-
const scheduleFlush = useCallback(() => {
|
|
70
|
-
if (flushTimerRef.current) clearTimeout(flushTimerRef.current)
|
|
71
|
-
flushTimerRef.current = setTimeout(flushPending, PENDING_ESCAPE_FLUSH_MS)
|
|
72
|
-
}, [flushPending])
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') return
|
|
76
|
-
|
|
77
|
-
const handleData = (chunk: Buffer | string) => {
|
|
78
|
-
logRawChunk(chunk)
|
|
79
|
-
if (flushTimerRef.current) {
|
|
80
|
-
clearTimeout(flushTimerRef.current)
|
|
81
|
-
flushTimerRef.current = null
|
|
82
|
-
}
|
|
83
|
-
const result = parseAppInput(parseStateRef.current, chunk)
|
|
84
|
-
parseStateRef.current = result.state
|
|
85
|
-
for (const event of result.events) dispatch(event)
|
|
86
|
-
if (hasPendingAppInput(parseStateRef.current)) scheduleFlush()
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
stdin.setEncoding('utf8')
|
|
90
|
-
stdin.setRawMode(true)
|
|
91
|
-
stdin.ref()
|
|
92
|
-
stdin.on('data', handleData)
|
|
93
|
-
stdin.resume()
|
|
94
|
-
stdout.write(BRACKETED_PASTE_ENABLE)
|
|
95
|
-
stdout.write(ENABLE_KITTY_KEYBOARD)
|
|
96
|
-
stdout.write(ENABLE_MODIFY_OTHER_KEYS)
|
|
97
|
-
|
|
98
|
-
return () => {
|
|
99
|
-
if (flushTimerRef.current) clearTimeout(flushTimerRef.current)
|
|
100
|
-
stdout.write(DISABLE_MODIFY_OTHER_KEYS)
|
|
101
|
-
stdout.write(DISABLE_KITTY_KEYBOARD)
|
|
102
|
-
stdout.write(BRACKETED_PASTE_DISABLE)
|
|
103
|
-
stdin.off('data', handleData)
|
|
104
|
-
stdin.setRawMode(false)
|
|
105
|
-
stdin.pause()
|
|
106
|
-
stdin.unref()
|
|
107
|
-
}
|
|
108
|
-
}, [dispatch, scheduleFlush, stdin, stdout])
|
|
109
|
-
|
|
110
|
-
const value = useMemo<AppInputContextValue>(() => ({
|
|
111
|
-
register(entry) {
|
|
112
|
-
handlersRef.current.add(entry)
|
|
113
|
-
return () => {
|
|
114
|
-
handlersRef.current.delete(entry)
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
}), [])
|
|
118
|
-
|
|
119
|
-
return <AppInputContext.Provider value={value}>{children}</AppInputContext.Provider>
|
|
7
|
+
export type UseAppInputOptions = {
|
|
8
|
+
isActive?: boolean
|
|
120
9
|
}
|
|
121
10
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
): void {
|
|
126
|
-
|
|
127
|
-
if (!ctx) throw new Error('Hook useAppInput must be used inside AppInputProvider')
|
|
128
|
-
|
|
129
|
-
const handlerRef = useRef(handler)
|
|
130
|
-
const isActiveRef = useRef(options.isActive !== false)
|
|
131
|
-
|
|
132
|
-
useEffect(() => { handlerRef.current = handler }, [handler])
|
|
133
|
-
useEffect(() => { isActiveRef.current = options.isActive !== false }, [options.isActive])
|
|
134
|
-
useEffect(() => ctx.register({ handlerRef, isActiveRef }), [ctx])
|
|
11
|
+
// Thin wrapper over Ink's built-in useInput. Ink's parseKeypress already decodes
|
|
12
|
+
// modified arrows (shift/ctrl/meta), Home/End, and the Kitty keyboard protocol, so
|
|
13
|
+
// we no longer hand-roll terminal escape-sequence parsing or manage raw mode here.
|
|
14
|
+
export function useAppInput(handler: InputHandler, options: UseAppInputOptions = {}): void {
|
|
15
|
+
useInput(handler, options)
|
|
135
16
|
}
|
package/src/cli/main.tsx
CHANGED
|
@@ -7,7 +7,6 @@ import { fileURLToPath } from 'node:url'
|
|
|
7
7
|
import { theme } from '../ui/theme.js'
|
|
8
8
|
import { Spinner } from '../ui/Spinner.js'
|
|
9
9
|
import { KeybindingProvider } from '../app/keybindings/KeybindingProvider.js'
|
|
10
|
-
import { AppInputProvider } from '../app/input/AppInputProvider.js'
|
|
11
10
|
import { IdentityManager } from '../identity/manager/IdentityManager.js'
|
|
12
11
|
import type { IdentityManagerResult } from '../identity/manager/IdentityManager.js'
|
|
13
12
|
import { loadConfig, saveConfig, type EthagentConfig } from '../storage/config.js'
|
|
@@ -114,11 +113,9 @@ const Root: React.FC<RootProps> = ({ setExit, initialConfig }) => {
|
|
|
114
113
|
async function renderHub(initialConfig: EthagentConfig | null | undefined): Promise<number> {
|
|
115
114
|
let exitCode = 0
|
|
116
115
|
const instance = render(
|
|
117
|
-
<
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
</KeybindingProvider>
|
|
121
|
-
</AppInputProvider>,
|
|
116
|
+
<KeybindingProvider>
|
|
117
|
+
<Root setExit={n => { exitCode = n }} initialConfig={initialConfig} />
|
|
118
|
+
</KeybindingProvider>,
|
|
122
119
|
{ exitOnCtrlC: false },
|
|
123
120
|
)
|
|
124
121
|
const guard = (reason: unknown): void => {
|
package/src/cli/reset.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { render } from 'ink'
|
|
3
3
|
import { stdout, stderr } from 'node:process'
|
|
4
|
-
import { AppInputProvider } from '../app/input/AppInputProvider.js'
|
|
5
4
|
import { ResetConfirmView } from './ResetConfirmView.js'
|
|
6
5
|
import { resetPlan, runReset } from '../storage/reset.js'
|
|
7
6
|
import { clearHarnessManagedBlocks } from './syncAdapters/index.js'
|
|
@@ -44,14 +43,10 @@ async function finishReset(): Promise<void> {
|
|
|
44
43
|
async function confirmWithInk(configDir: string, secretAccounts: string[]): Promise<boolean> {
|
|
45
44
|
let confirmed = false
|
|
46
45
|
const instance = render(
|
|
47
|
-
React.createElement(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
plan: { configDir, secretAccounts },
|
|
52
|
-
onDone: (value: boolean) => { confirmed = value },
|
|
53
|
-
}),
|
|
54
|
-
),
|
|
46
|
+
React.createElement(ResetConfirmView, {
|
|
47
|
+
plan: { configDir, secretAccounts },
|
|
48
|
+
onDone: (value: boolean) => { confirmed = value },
|
|
49
|
+
}),
|
|
55
50
|
{ exitOnCtrlC: false },
|
|
56
51
|
)
|
|
57
52
|
try {
|
package/src/ui/TextArea.tsx
CHANGED
|
@@ -78,6 +78,15 @@ export function TextArea({
|
|
|
78
78
|
setCursor(lineStart)
|
|
79
79
|
return
|
|
80
80
|
}
|
|
81
|
+
if (key.home || (key.ctrl && input === 'a')) {
|
|
82
|
+
setCursor(Math.max(0, val.lastIndexOf('\n', cur - 1) + 1))
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
if (key.end || (key.ctrl && input === 'e')) {
|
|
86
|
+
const nextNewline = val.indexOf('\n', cur)
|
|
87
|
+
setCursor(nextNewline === -1 ? val.length : nextNewline)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
81
90
|
if (key.ctrl || key.meta || key.tab) return
|
|
82
91
|
if (input) {
|
|
83
92
|
const clean = input.replace(/\r/g, '')
|
package/src/ui/TextInput.tsx
CHANGED
|
@@ -109,6 +109,14 @@ export function TextInput({
|
|
|
109
109
|
if (error) setError(null)
|
|
110
110
|
return
|
|
111
111
|
}
|
|
112
|
+
if (key.home || (key.ctrl && input === 'a')) {
|
|
113
|
+
setCursor(0)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
if (key.end || (key.ctrl && input === 'e')) {
|
|
117
|
+
setCursor(val.length)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
112
120
|
if (key.ctrl || key.meta || key.upArrow || key.downArrow || key.tab) {
|
|
113
121
|
return
|
|
114
122
|
}
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import type { Key } from 'ink'
|
|
2
|
-
|
|
3
|
-
export const BRACKETED_PASTE_ENABLE = '\x1b[?2004h'
|
|
4
|
-
export const BRACKETED_PASTE_DISABLE = '\x1b[?2004l'
|
|
5
|
-
export const BRACKETED_PASTE_START = '\x1b[200~'
|
|
6
|
-
export const BRACKETED_PASTE_END = '\x1b[201~'
|
|
7
|
-
export const ENABLE_KITTY_KEYBOARD = '\x1b[>1u'
|
|
8
|
-
export const DISABLE_KITTY_KEYBOARD = '\x1b[<u'
|
|
9
|
-
export const ENABLE_MODIFY_OTHER_KEYS = '\x1b[>4;2m'
|
|
10
|
-
export const DISABLE_MODIFY_OTHER_KEYS = '\x1b[>4m'
|
|
11
|
-
|
|
12
|
-
export type AppInputEvent = {
|
|
13
|
-
input: string
|
|
14
|
-
key: Key
|
|
15
|
-
isPasted: boolean
|
|
16
|
-
raw: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type AppInputParseState = {
|
|
20
|
-
mode: 'normal' | 'paste'
|
|
21
|
-
pending: string
|
|
22
|
-
pasteBuffer: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type ParseResult = {
|
|
26
|
-
events: AppInputEvent[]
|
|
27
|
-
state: AppInputParseState
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const ESC = '\x1b'
|
|
31
|
-
const CSI_U_RE = /^\x1b\[(\d+)(?:;(\d+))?u/
|
|
32
|
-
const MODIFY_OTHER_KEYS_RE = /^\x1b\[27;(\d+);(\d+)~/
|
|
33
|
-
|
|
34
|
-
const KNOWN_SEQUENCES: Array<{ sequence: string; key: Partial<Key> }> = [
|
|
35
|
-
{ sequence: '\x1b[A', key: { upArrow: true } },
|
|
36
|
-
{ sequence: '\x1b[B', key: { downArrow: true } },
|
|
37
|
-
{ sequence: '\x1b[C', key: { rightArrow: true } },
|
|
38
|
-
{ sequence: '\x1b[D', key: { leftArrow: true } },
|
|
39
|
-
{ sequence: '\x1b[Z', key: { tab: true, shift: true } },
|
|
40
|
-
{ sequence: '\x1b[3~', key: { delete: true } },
|
|
41
|
-
{ sequence: '\x1b[5~', key: { pageUp: true } },
|
|
42
|
-
{ sequence: '\x1b[6~', key: { pageDown: true } },
|
|
43
|
-
{ sequence: '\x1b[H', key: { home: true } },
|
|
44
|
-
{ sequence: '\x1b[F', key: { end: true } },
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
const PENDING_PREFIXES = [
|
|
48
|
-
BRACKETED_PASTE_START,
|
|
49
|
-
BRACKETED_PASTE_END,
|
|
50
|
-
...KNOWN_SEQUENCES.map(entry => entry.sequence),
|
|
51
|
-
'\x1b[13;2u',
|
|
52
|
-
'\x1b[27;2;13~',
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
export function createAppInputParseState(): AppInputParseState {
|
|
56
|
-
return { mode: 'normal', pending: '', pasteBuffer: '' }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function hasPendingAppInput(state: AppInputParseState): boolean {
|
|
60
|
-
return state.pending.length > 0
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function parseAppInput(
|
|
64
|
-
previous: AppInputParseState,
|
|
65
|
-
chunk: Buffer | string | null,
|
|
66
|
-
): ParseResult {
|
|
67
|
-
const source = previous.pending + (chunk === null ? '' : stringifyChunk(chunk))
|
|
68
|
-
const events: AppInputEvent[] = []
|
|
69
|
-
const state: AppInputParseState = {
|
|
70
|
-
mode: previous.mode,
|
|
71
|
-
pending: '',
|
|
72
|
-
pasteBuffer: previous.pasteBuffer,
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let rest = source
|
|
76
|
-
while (rest.length > 0) {
|
|
77
|
-
if (state.mode === 'paste') {
|
|
78
|
-
const endIndex = rest.indexOf(BRACKETED_PASTE_END)
|
|
79
|
-
if (endIndex === -1) {
|
|
80
|
-
const pending = chunk === null ? '' : longestSuffixPrefix(rest, [BRACKETED_PASTE_END])
|
|
81
|
-
state.pasteBuffer += rest.slice(0, rest.length - pending.length)
|
|
82
|
-
state.pending = pending
|
|
83
|
-
rest = ''
|
|
84
|
-
break
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
state.pasteBuffer += rest.slice(0, endIndex)
|
|
88
|
-
events.push(createInputEvent(state.pasteBuffer, {}, state.pasteBuffer, true))
|
|
89
|
-
state.mode = 'normal'
|
|
90
|
-
state.pasteBuffer = ''
|
|
91
|
-
rest = rest.slice(endIndex + BRACKETED_PASTE_END.length)
|
|
92
|
-
continue
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (rest.startsWith(BRACKETED_PASTE_START)) {
|
|
96
|
-
state.mode = 'paste'
|
|
97
|
-
state.pasteBuffer = ''
|
|
98
|
-
rest = rest.slice(BRACKETED_PASTE_START.length)
|
|
99
|
-
continue
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (rest.startsWith(BRACKETED_PASTE_END)) {
|
|
103
|
-
rest = rest.slice(BRACKETED_PASTE_END.length)
|
|
104
|
-
continue
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const parsed = parseNormalInput(rest, chunk === null)
|
|
108
|
-
if (parsed.kind === 'pending') {
|
|
109
|
-
state.pending = parsed.pending
|
|
110
|
-
rest = ''
|
|
111
|
-
break
|
|
112
|
-
}
|
|
113
|
-
events.push(parsed.event)
|
|
114
|
-
rest = rest.slice(parsed.length)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { events, state }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
type NormalParseResult =
|
|
121
|
-
| { kind: 'event'; event: AppInputEvent; length: number }
|
|
122
|
-
| { kind: 'pending'; pending: string }
|
|
123
|
-
|
|
124
|
-
function parseNormalInput(source: string, flushing: boolean): NormalParseResult {
|
|
125
|
-
if (!source.startsWith(ESC)) {
|
|
126
|
-
const nextEscape = source.indexOf(ESC)
|
|
127
|
-
const text = nextEscape === -1 ? source : source.slice(0, nextEscape)
|
|
128
|
-
return { kind: 'event', event: createTextEvent(text), length: text.length }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!flushing && isPendingPrefix(source)) {
|
|
132
|
-
return { kind: 'pending', pending: source }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const csiU = CSI_U_RE.exec(source)
|
|
136
|
-
if (csiU) {
|
|
137
|
-
const codepoint = Number(csiU[1])
|
|
138
|
-
const modifier = csiU[2] ? Number(csiU[2]) : 1
|
|
139
|
-
return {
|
|
140
|
-
kind: 'event',
|
|
141
|
-
event: keycodeEvent(csiU[0], codepoint, modifier),
|
|
142
|
-
length: csiU[0].length,
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const modifyOtherKeys = MODIFY_OTHER_KEYS_RE.exec(source)
|
|
147
|
-
if (modifyOtherKeys) {
|
|
148
|
-
return {
|
|
149
|
-
kind: 'event',
|
|
150
|
-
event: keycodeEvent(
|
|
151
|
-
modifyOtherKeys[0],
|
|
152
|
-
Number(modifyOtherKeys[2]),
|
|
153
|
-
Number(modifyOtherKeys[1]),
|
|
154
|
-
),
|
|
155
|
-
length: modifyOtherKeys[0].length,
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const known = KNOWN_SEQUENCES.find(entry => source.startsWith(entry.sequence))
|
|
160
|
-
if (known) {
|
|
161
|
-
return {
|
|
162
|
-
kind: 'event',
|
|
163
|
-
event: createInputEvent('', known.key, known.sequence),
|
|
164
|
-
length: known.sequence.length,
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (source.startsWith('\x1b\r') || source.startsWith('\x1b\n')) {
|
|
169
|
-
return {
|
|
170
|
-
kind: 'event',
|
|
171
|
-
event: createInputEvent('', { meta: true, return: true }, source.slice(0, 2)),
|
|
172
|
-
length: 2,
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (source.length >= 2) {
|
|
177
|
-
const next = source.slice(1, 2)
|
|
178
|
-
const code = next.charCodeAt(0)
|
|
179
|
-
const altInput = code >= 0x20 && code <= 0x7e ? next : ''
|
|
180
|
-
return {
|
|
181
|
-
kind: 'event',
|
|
182
|
-
event: createInputEvent(altInput, { meta: true }, source.slice(0, 2)),
|
|
183
|
-
length: 2,
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
kind: 'event',
|
|
189
|
-
event: createInputEvent('', { escape: true, meta: true }, ESC),
|
|
190
|
-
length: 1,
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const PRINTABLE_CHAR_RE = /[\p{L}\p{N}\p{M}\p{P}\p{S}\p{Z}]/u
|
|
195
|
-
|
|
196
|
-
function stripNoise(text: string): string {
|
|
197
|
-
let out = ''
|
|
198
|
-
for (const ch of text) {
|
|
199
|
-
if (ch === '\uFFFD') continue
|
|
200
|
-
if (!PRINTABLE_CHAR_RE.test(ch)) continue
|
|
201
|
-
out += ch
|
|
202
|
-
}
|
|
203
|
-
return out
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function createTextEvent(text: string): AppInputEvent {
|
|
207
|
-
if (text === '\r' || text === '\n') return createInputEvent('', { return: true }, text)
|
|
208
|
-
if (text === '\t') return createInputEvent('', { tab: true }, text)
|
|
209
|
-
if (text === '\x7f' || text === '\b') return createInputEvent('', { backspace: true }, text)
|
|
210
|
-
if (text.length === 1) {
|
|
211
|
-
const code = text.charCodeAt(0)
|
|
212
|
-
if (code > 0 && code <= 26) {
|
|
213
|
-
return createInputEvent(
|
|
214
|
-
String.fromCharCode('a'.charCodeAt(0) + code - 1),
|
|
215
|
-
{ ctrl: true },
|
|
216
|
-
text,
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
if (/[A-Z]/.test(text)) return createInputEvent(text, { shift: true }, text)
|
|
220
|
-
}
|
|
221
|
-
const cleaned = stripNoise(text)
|
|
222
|
-
return createInputEvent(cleaned, {}, text)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function keycodeEvent(raw: string, codepoint: number, modifier: number): AppInputEvent {
|
|
226
|
-
const key = decodeModifier(modifier)
|
|
227
|
-
if (codepoint === 9) return createInputEvent('', { ...key, tab: true }, raw)
|
|
228
|
-
if (codepoint === 13) return createInputEvent('', { ...key, return: true }, raw)
|
|
229
|
-
if (codepoint === 27) return createInputEvent('', { ...key, escape: true, meta: true }, raw)
|
|
230
|
-
const printable = codepoint >= 0x20 && codepoint <= 0x7e
|
|
231
|
-
return createInputEvent(printable ? String.fromCodePoint(codepoint) : '', key, raw)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function decodeModifier(modifier: number): Partial<Key> {
|
|
235
|
-
const normalized = Math.max(1, modifier) - 1
|
|
236
|
-
return {
|
|
237
|
-
shift: Boolean(normalized & 1),
|
|
238
|
-
meta: Boolean(normalized & 2),
|
|
239
|
-
ctrl: Boolean(normalized & 4),
|
|
240
|
-
super: Boolean(normalized & 8),
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function createInputEvent(
|
|
245
|
-
input: string,
|
|
246
|
-
key: Partial<Key>,
|
|
247
|
-
raw: string,
|
|
248
|
-
isPasted = false,
|
|
249
|
-
): AppInputEvent {
|
|
250
|
-
return {
|
|
251
|
-
input,
|
|
252
|
-
key: {
|
|
253
|
-
upArrow: false,
|
|
254
|
-
downArrow: false,
|
|
255
|
-
leftArrow: false,
|
|
256
|
-
rightArrow: false,
|
|
257
|
-
pageDown: false,
|
|
258
|
-
pageUp: false,
|
|
259
|
-
home: false,
|
|
260
|
-
end: false,
|
|
261
|
-
return: false,
|
|
262
|
-
escape: false,
|
|
263
|
-
ctrl: false,
|
|
264
|
-
shift: false,
|
|
265
|
-
tab: false,
|
|
266
|
-
backspace: false,
|
|
267
|
-
delete: false,
|
|
268
|
-
meta: false,
|
|
269
|
-
...key,
|
|
270
|
-
} as Key,
|
|
271
|
-
raw,
|
|
272
|
-
isPasted,
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function stringifyChunk(chunk: Buffer | string): string {
|
|
277
|
-
return Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function isPendingPrefix(source: string): boolean {
|
|
281
|
-
return PENDING_PREFIXES.some(sequence => sequence.startsWith(source) && sequence !== source)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function longestSuffixPrefix(source: string, markers: string[]): string {
|
|
285
|
-
let best = ''
|
|
286
|
-
for (const marker of markers) {
|
|
287
|
-
const maxLength = Math.min(source.length, marker.length - 1)
|
|
288
|
-
for (let length = 1; length <= maxLength; length += 1) {
|
|
289
|
-
const suffix = source.slice(source.length - length)
|
|
290
|
-
if (marker.startsWith(suffix) && suffix.length > best.length) best = suffix
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return best
|
|
294
|
-
}
|