@vsuryav/agent-sim 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/bin/agent-sim.js +25 -0
- package/package.json +72 -0
- package/src/app-paths.ts +29 -0
- package/src/app-sync.test.ts +75 -0
- package/src/app-sync.ts +110 -0
- package/src/cli.ts +129 -0
- package/src/collector/claude-code.test.ts +102 -0
- package/src/collector/claude-code.ts +133 -0
- package/src/collector/codex-cli.test.ts +116 -0
- package/src/collector/codex-cli.ts +149 -0
- package/src/collector/db.test.ts +59 -0
- package/src/collector/db.ts +125 -0
- package/src/collector/names.test.ts +21 -0
- package/src/collector/names.ts +28 -0
- package/src/collector/personality.test.ts +40 -0
- package/src/collector/personality.ts +46 -0
- package/src/collector/remote-sync.test.ts +31 -0
- package/src/collector/remote-sync.ts +171 -0
- package/src/collector/sync.test.ts +67 -0
- package/src/collector/sync.ts +148 -0
- package/src/collector/types.ts +1 -0
- package/src/engine/bootstrap/state.ts +3 -0
- package/src/engine/buddy/CompanionSprite.tsx +371 -0
- package/src/engine/buddy/companion.ts +133 -0
- package/src/engine/buddy/prompt.ts +36 -0
- package/src/engine/buddy/sprites.ts +514 -0
- package/src/engine/buddy/types.ts +148 -0
- package/src/engine/buddy/useBuddyNotification.tsx +98 -0
- package/src/engine/ink/Ansi.tsx +292 -0
- package/src/engine/ink/bidi.ts +139 -0
- package/src/engine/ink/clearTerminal.ts +74 -0
- package/src/engine/ink/colorize.ts +231 -0
- package/src/engine/ink/components/AlternateScreen.tsx +80 -0
- package/src/engine/ink/components/App.tsx +658 -0
- package/src/engine/ink/components/AppContext.ts +21 -0
- package/src/engine/ink/components/Box.tsx +214 -0
- package/src/engine/ink/components/Button.tsx +192 -0
- package/src/engine/ink/components/ClockContext.tsx +112 -0
- package/src/engine/ink/components/CursorDeclarationContext.ts +32 -0
- package/src/engine/ink/components/ErrorOverview.tsx +109 -0
- package/src/engine/ink/components/Link.tsx +42 -0
- package/src/engine/ink/components/Newline.tsx +39 -0
- package/src/engine/ink/components/NoSelect.tsx +68 -0
- package/src/engine/ink/components/RawAnsi.tsx +57 -0
- package/src/engine/ink/components/ScrollBox.tsx +237 -0
- package/src/engine/ink/components/Spacer.tsx +20 -0
- package/src/engine/ink/components/StdinContext.ts +49 -0
- package/src/engine/ink/components/TerminalFocusContext.tsx +52 -0
- package/src/engine/ink/components/TerminalSizeContext.tsx +7 -0
- package/src/engine/ink/components/Text.tsx +254 -0
- package/src/engine/ink/constants.ts +2 -0
- package/src/engine/ink/dom.ts +484 -0
- package/src/engine/ink/events/click-event.ts +38 -0
- package/src/engine/ink/events/dispatcher.ts +233 -0
- package/src/engine/ink/events/emitter.ts +39 -0
- package/src/engine/ink/events/event-handlers.ts +73 -0
- package/src/engine/ink/events/event.ts +11 -0
- package/src/engine/ink/events/focus-event.ts +21 -0
- package/src/engine/ink/events/input-event.ts +205 -0
- package/src/engine/ink/events/keyboard-event.ts +51 -0
- package/src/engine/ink/events/terminal-event.ts +107 -0
- package/src/engine/ink/events/terminal-focus-event.ts +19 -0
- package/src/engine/ink/focus.ts +181 -0
- package/src/engine/ink/frame.ts +124 -0
- package/src/engine/ink/get-max-width.ts +27 -0
- package/src/engine/ink/global.d.ts +18 -0
- package/src/engine/ink/hit-test.ts +130 -0
- package/src/engine/ink/hooks/use-animation-frame.ts +57 -0
- package/src/engine/ink/hooks/use-app.ts +8 -0
- package/src/engine/ink/hooks/use-declared-cursor.ts +73 -0
- package/src/engine/ink/hooks/use-input.ts +92 -0
- package/src/engine/ink/hooks/use-interval.ts +67 -0
- package/src/engine/ink/hooks/use-search-highlight.ts +53 -0
- package/src/engine/ink/hooks/use-selection.ts +104 -0
- package/src/engine/ink/hooks/use-stdin.ts +8 -0
- package/src/engine/ink/hooks/use-tab-status.ts +72 -0
- package/src/engine/ink/hooks/use-terminal-focus.ts +16 -0
- package/src/engine/ink/hooks/use-terminal-title.ts +31 -0
- package/src/engine/ink/hooks/use-terminal-viewport.ts +96 -0
- package/src/engine/ink/ink.tsx +1723 -0
- package/src/engine/ink/instances.ts +10 -0
- package/src/engine/ink/layout/engine.ts +6 -0
- package/src/engine/ink/layout/geometry.ts +97 -0
- package/src/engine/ink/layout/node.ts +152 -0
- package/src/engine/ink/layout/yoga.ts +308 -0
- package/src/engine/ink/line-width-cache.ts +24 -0
- package/src/engine/ink/log-update.ts +773 -0
- package/src/engine/ink/measure-element.ts +23 -0
- package/src/engine/ink/measure-text.ts +47 -0
- package/src/engine/ink/node-cache.ts +54 -0
- package/src/engine/ink/optimizer.ts +93 -0
- package/src/engine/ink/output.ts +797 -0
- package/src/engine/ink/parse-keypress.ts +801 -0
- package/src/engine/ink/reconciler.ts +512 -0
- package/src/engine/ink/render-border.ts +231 -0
- package/src/engine/ink/render-node-to-output.ts +1462 -0
- package/src/engine/ink/render-to-screen.ts +231 -0
- package/src/engine/ink/renderer.ts +178 -0
- package/src/engine/ink/root.ts +184 -0
- package/src/engine/ink/screen.ts +1486 -0
- package/src/engine/ink/searchHighlight.ts +93 -0
- package/src/engine/ink/selection.ts +917 -0
- package/src/engine/ink/squash-text-nodes.ts +92 -0
- package/src/engine/ink/stringWidth.ts +222 -0
- package/src/engine/ink/styles.ts +771 -0
- package/src/engine/ink/supports-hyperlinks.ts +57 -0
- package/src/engine/ink/tabstops.ts +46 -0
- package/src/engine/ink/terminal-focus-state.ts +47 -0
- package/src/engine/ink/terminal-querier.ts +212 -0
- package/src/engine/ink/terminal.ts +248 -0
- package/src/engine/ink/termio/ansi.ts +75 -0
- package/src/engine/ink/termio/csi.ts +319 -0
- package/src/engine/ink/termio/dec.ts +60 -0
- package/src/engine/ink/termio/esc.ts +67 -0
- package/src/engine/ink/termio/osc.ts +493 -0
- package/src/engine/ink/termio/parser.ts +394 -0
- package/src/engine/ink/termio/sgr.ts +308 -0
- package/src/engine/ink/termio/tokenize.ts +319 -0
- package/src/engine/ink/termio/types.ts +236 -0
- package/src/engine/ink/useTerminalNotification.ts +126 -0
- package/src/engine/ink/warn.ts +9 -0
- package/src/engine/ink/widest-line.ts +19 -0
- package/src/engine/ink/wrap-text.ts +74 -0
- package/src/engine/ink/wrapAnsi.ts +20 -0
- package/src/engine/native-ts/yoga-layout/enums.ts +134 -0
- package/src/engine/native-ts/yoga-layout/index.ts +2578 -0
- package/src/engine/stubs/bootstrap-state.ts +4 -0
- package/src/engine/stubs/debug.ts +6 -0
- package/src/engine/stubs/log.ts +4 -0
- package/src/engine/utils/debug.ts +5 -0
- package/src/engine/utils/earlyInput.ts +4 -0
- package/src/engine/utils/env.ts +15 -0
- package/src/engine/utils/envUtils.ts +4 -0
- package/src/engine/utils/execFileNoThrow.ts +24 -0
- package/src/engine/utils/fullscreen.ts +4 -0
- package/src/engine/utils/intl.ts +9 -0
- package/src/engine/utils/log.ts +3 -0
- package/src/engine/utils/semver.ts +13 -0
- package/src/engine/utils/sliceAnsi.ts +10 -0
- package/src/engine/utils/theme.ts +17 -0
- package/src/game/App.tsx +141 -0
- package/src/game/agents/behavior.ts +249 -0
- package/src/game/agents/speech.ts +57 -0
- package/src/game/canvas.ts +98 -0
- package/src/game/launch.ts +36 -0
- package/src/game/ship/ShipView.tsx +145 -0
- package/src/game/ship/ship-map.ts +172 -0
- package/src/game/ui/AgentBio.tsx +72 -0
- package/src/game/ui/HUD.tsx +63 -0
- package/src/game/ui/StatusBar.tsx +49 -0
- package/src/game/useKeyboard.ts +62 -0
- package/src/main.tsx +22 -0
- package/src/run-interactive.ts +74 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContinuousEventPriority,
|
|
3
|
+
DefaultEventPriority,
|
|
4
|
+
DiscreteEventPriority,
|
|
5
|
+
NoEventPriority,
|
|
6
|
+
} from 'react-reconciler/constants.js'
|
|
7
|
+
import { logError } from '../../utils/log.js'
|
|
8
|
+
import { HANDLER_FOR_EVENT } from './event-handlers.js'
|
|
9
|
+
import type { EventTarget, TerminalEvent } from './terminal-event.js'
|
|
10
|
+
|
|
11
|
+
// --
|
|
12
|
+
|
|
13
|
+
type DispatchListener = {
|
|
14
|
+
node: EventTarget
|
|
15
|
+
handler: (event: TerminalEvent) => void
|
|
16
|
+
phase: 'capturing' | 'at_target' | 'bubbling'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getHandler(
|
|
20
|
+
node: EventTarget,
|
|
21
|
+
eventType: string,
|
|
22
|
+
capture: boolean,
|
|
23
|
+
): ((event: TerminalEvent) => void) | undefined {
|
|
24
|
+
const handlers = node._eventHandlers
|
|
25
|
+
if (!handlers) return undefined
|
|
26
|
+
|
|
27
|
+
const mapping = HANDLER_FOR_EVENT[eventType]
|
|
28
|
+
if (!mapping) return undefined
|
|
29
|
+
|
|
30
|
+
const propName = capture ? mapping.capture : mapping.bubble
|
|
31
|
+
if (!propName) return undefined
|
|
32
|
+
|
|
33
|
+
return handlers[propName] as ((event: TerminalEvent) => void) | undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Collect all listeners for an event in dispatch order.
|
|
38
|
+
*
|
|
39
|
+
* Uses react-dom's two-phase accumulation pattern:
|
|
40
|
+
* - Walk from target to root
|
|
41
|
+
* - Capture handlers are prepended (unshift) → root-first
|
|
42
|
+
* - Bubble handlers are appended (push) → target-first
|
|
43
|
+
*
|
|
44
|
+
* Result: [root-cap, ..., parent-cap, target-cap, target-bub, parent-bub, ..., root-bub]
|
|
45
|
+
*/
|
|
46
|
+
function collectListeners(
|
|
47
|
+
target: EventTarget,
|
|
48
|
+
event: TerminalEvent,
|
|
49
|
+
): DispatchListener[] {
|
|
50
|
+
const listeners: DispatchListener[] = []
|
|
51
|
+
|
|
52
|
+
let node: EventTarget | undefined = target
|
|
53
|
+
while (node) {
|
|
54
|
+
const isTarget = node === target
|
|
55
|
+
|
|
56
|
+
const captureHandler = getHandler(node, event.type, true)
|
|
57
|
+
const bubbleHandler = getHandler(node, event.type, false)
|
|
58
|
+
|
|
59
|
+
if (captureHandler) {
|
|
60
|
+
listeners.unshift({
|
|
61
|
+
node,
|
|
62
|
+
handler: captureHandler,
|
|
63
|
+
phase: isTarget ? 'at_target' : 'capturing',
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (bubbleHandler && (event.bubbles || isTarget)) {
|
|
68
|
+
listeners.push({
|
|
69
|
+
node,
|
|
70
|
+
handler: bubbleHandler,
|
|
71
|
+
phase: isTarget ? 'at_target' : 'bubbling',
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
node = node.parentNode
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return listeners
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Execute collected listeners with propagation control.
|
|
83
|
+
*
|
|
84
|
+
* Before each handler, calls event._prepareForTarget(node) so event
|
|
85
|
+
* subclasses can do per-node setup.
|
|
86
|
+
*/
|
|
87
|
+
function processDispatchQueue(
|
|
88
|
+
listeners: DispatchListener[],
|
|
89
|
+
event: TerminalEvent,
|
|
90
|
+
): void {
|
|
91
|
+
let previousNode: EventTarget | undefined
|
|
92
|
+
|
|
93
|
+
for (const { node, handler, phase } of listeners) {
|
|
94
|
+
if (event._isImmediatePropagationStopped()) {
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (event._isPropagationStopped() && node !== previousNode) {
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
event._setEventPhase(phase)
|
|
103
|
+
event._setCurrentTarget(node)
|
|
104
|
+
event._prepareForTarget(node)
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
handler(event)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logError(error)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
previousNode = node
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Map terminal event types to React scheduling priorities.
|
|
120
|
+
* Mirrors react-dom's getEventPriority() switch.
|
|
121
|
+
*/
|
|
122
|
+
function getEventPriority(eventType: string): number {
|
|
123
|
+
switch (eventType) {
|
|
124
|
+
case 'keydown':
|
|
125
|
+
case 'keyup':
|
|
126
|
+
case 'click':
|
|
127
|
+
case 'focus':
|
|
128
|
+
case 'blur':
|
|
129
|
+
case 'paste':
|
|
130
|
+
return DiscreteEventPriority as number
|
|
131
|
+
case 'resize':
|
|
132
|
+
case 'scroll':
|
|
133
|
+
case 'mousemove':
|
|
134
|
+
return ContinuousEventPriority as number
|
|
135
|
+
default:
|
|
136
|
+
return DefaultEventPriority as number
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --
|
|
141
|
+
|
|
142
|
+
type DiscreteUpdates = <A, B>(
|
|
143
|
+
fn: (a: A, b: B) => boolean,
|
|
144
|
+
a: A,
|
|
145
|
+
b: B,
|
|
146
|
+
c: undefined,
|
|
147
|
+
d: undefined,
|
|
148
|
+
) => boolean
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Owns event dispatch state and the capture/bubble dispatch loop.
|
|
152
|
+
*
|
|
153
|
+
* The reconciler host config reads currentEvent and currentUpdatePriority
|
|
154
|
+
* to implement resolveUpdatePriority, resolveEventType, and
|
|
155
|
+
* resolveEventTimeStamp — mirroring how react-dom's host config reads
|
|
156
|
+
* ReactDOMSharedInternals and window.event.
|
|
157
|
+
*
|
|
158
|
+
* discreteUpdates is injected after construction (by InkReconciler)
|
|
159
|
+
* to break the import cycle.
|
|
160
|
+
*/
|
|
161
|
+
export class Dispatcher {
|
|
162
|
+
currentEvent: TerminalEvent | null = null
|
|
163
|
+
currentUpdatePriority: number = DefaultEventPriority as number
|
|
164
|
+
discreteUpdates: DiscreteUpdates | null = null
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Infer event priority from the currently-dispatching event.
|
|
168
|
+
* Called by the reconciler host config's resolveUpdatePriority
|
|
169
|
+
* when no explicit priority has been set.
|
|
170
|
+
*/
|
|
171
|
+
resolveEventPriority(): number {
|
|
172
|
+
if (this.currentUpdatePriority !== (NoEventPriority as number)) {
|
|
173
|
+
return this.currentUpdatePriority
|
|
174
|
+
}
|
|
175
|
+
if (this.currentEvent) {
|
|
176
|
+
return getEventPriority(this.currentEvent.type)
|
|
177
|
+
}
|
|
178
|
+
return DefaultEventPriority as number
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Dispatch an event through capture and bubble phases.
|
|
183
|
+
* Returns true if preventDefault() was NOT called.
|
|
184
|
+
*/
|
|
185
|
+
dispatch(target: EventTarget, event: TerminalEvent): boolean {
|
|
186
|
+
const previousEvent = this.currentEvent
|
|
187
|
+
this.currentEvent = event
|
|
188
|
+
try {
|
|
189
|
+
event._setTarget(target)
|
|
190
|
+
|
|
191
|
+
const listeners = collectListeners(target, event)
|
|
192
|
+
processDispatchQueue(listeners, event)
|
|
193
|
+
|
|
194
|
+
event._setEventPhase('none')
|
|
195
|
+
event._setCurrentTarget(null)
|
|
196
|
+
|
|
197
|
+
return !event.defaultPrevented
|
|
198
|
+
} finally {
|
|
199
|
+
this.currentEvent = previousEvent
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Dispatch with discrete (sync) priority.
|
|
205
|
+
* For user-initiated events: keyboard, click, focus, paste.
|
|
206
|
+
*/
|
|
207
|
+
dispatchDiscrete(target: EventTarget, event: TerminalEvent): boolean {
|
|
208
|
+
if (!this.discreteUpdates) {
|
|
209
|
+
return this.dispatch(target, event)
|
|
210
|
+
}
|
|
211
|
+
return this.discreteUpdates(
|
|
212
|
+
(t, e) => this.dispatch(t, e),
|
|
213
|
+
target,
|
|
214
|
+
event,
|
|
215
|
+
undefined,
|
|
216
|
+
undefined,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Dispatch with continuous priority.
|
|
222
|
+
* For high-frequency events: resize, scroll, mouse move.
|
|
223
|
+
*/
|
|
224
|
+
dispatchContinuous(target: EventTarget, event: TerminalEvent): boolean {
|
|
225
|
+
const previousPriority = this.currentUpdatePriority
|
|
226
|
+
try {
|
|
227
|
+
this.currentUpdatePriority = ContinuousEventPriority as number
|
|
228
|
+
return this.dispatch(target, event)
|
|
229
|
+
} finally {
|
|
230
|
+
this.currentUpdatePriority = previousPriority
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EventEmitter as NodeEventEmitter } from 'events'
|
|
2
|
+
import { Event } from './event.js'
|
|
3
|
+
|
|
4
|
+
// Similar to node's builtin EventEmitter, but is also aware of our `Event`
|
|
5
|
+
// class, and so `emit` respects `stopImmediatePropagation()`.
|
|
6
|
+
export class EventEmitter extends NodeEventEmitter {
|
|
7
|
+
constructor() {
|
|
8
|
+
super()
|
|
9
|
+
// Disable the default maxListeners warning. In React, many components
|
|
10
|
+
// can legitimately listen to the same event (e.g., useInput hooks).
|
|
11
|
+
// The default limit of 10 causes spurious warnings.
|
|
12
|
+
this.setMaxListeners(0)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override emit(type: string | symbol, ...args: unknown[]): boolean {
|
|
16
|
+
// Delegate to node for `error`, since it's not treated like a normal event
|
|
17
|
+
if (type === 'error') {
|
|
18
|
+
return super.emit(type, ...args)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const listeners = this.rawListeners(type)
|
|
22
|
+
|
|
23
|
+
if (listeners.length === 0) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ccEvent = args[0] instanceof Event ? args[0] : null
|
|
28
|
+
|
|
29
|
+
for (const listener of listeners) {
|
|
30
|
+
listener.apply(this, args)
|
|
31
|
+
|
|
32
|
+
if (ccEvent?.didStopImmediatePropagation()) {
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ClickEvent } from './click-event.js'
|
|
2
|
+
import type { FocusEvent } from './focus-event.js'
|
|
3
|
+
import type { KeyboardEvent } from './keyboard-event.js'
|
|
4
|
+
import type { PasteEvent } from './paste-event.js'
|
|
5
|
+
import type { ResizeEvent } from './resize-event.js'
|
|
6
|
+
|
|
7
|
+
type KeyboardEventHandler = (event: KeyboardEvent) => void
|
|
8
|
+
type FocusEventHandler = (event: FocusEvent) => void
|
|
9
|
+
type PasteEventHandler = (event: PasteEvent) => void
|
|
10
|
+
type ResizeEventHandler = (event: ResizeEvent) => void
|
|
11
|
+
type ClickEventHandler = (event: ClickEvent) => void
|
|
12
|
+
type HoverEventHandler = () => void
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Props for event handlers on Box and other host components.
|
|
16
|
+
*
|
|
17
|
+
* Follows the React/DOM naming convention:
|
|
18
|
+
* - onEventName: handler for bubble phase
|
|
19
|
+
* - onEventNameCapture: handler for capture phase
|
|
20
|
+
*/
|
|
21
|
+
export type EventHandlerProps = {
|
|
22
|
+
onKeyDown?: KeyboardEventHandler
|
|
23
|
+
onKeyDownCapture?: KeyboardEventHandler
|
|
24
|
+
|
|
25
|
+
onFocus?: FocusEventHandler
|
|
26
|
+
onFocusCapture?: FocusEventHandler
|
|
27
|
+
onBlur?: FocusEventHandler
|
|
28
|
+
onBlurCapture?: FocusEventHandler
|
|
29
|
+
|
|
30
|
+
onPaste?: PasteEventHandler
|
|
31
|
+
onPasteCapture?: PasteEventHandler
|
|
32
|
+
|
|
33
|
+
onResize?: ResizeEventHandler
|
|
34
|
+
|
|
35
|
+
onClick?: ClickEventHandler
|
|
36
|
+
onMouseEnter?: HoverEventHandler
|
|
37
|
+
onMouseLeave?: HoverEventHandler
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reverse lookup: event type string → handler prop names.
|
|
42
|
+
* Used by the dispatcher for O(1) handler lookup per node.
|
|
43
|
+
*/
|
|
44
|
+
export const HANDLER_FOR_EVENT: Record<
|
|
45
|
+
string,
|
|
46
|
+
{ bubble?: keyof EventHandlerProps; capture?: keyof EventHandlerProps }
|
|
47
|
+
> = {
|
|
48
|
+
keydown: { bubble: 'onKeyDown', capture: 'onKeyDownCapture' },
|
|
49
|
+
focus: { bubble: 'onFocus', capture: 'onFocusCapture' },
|
|
50
|
+
blur: { bubble: 'onBlur', capture: 'onBlurCapture' },
|
|
51
|
+
paste: { bubble: 'onPaste', capture: 'onPasteCapture' },
|
|
52
|
+
resize: { bubble: 'onResize' },
|
|
53
|
+
click: { bubble: 'onClick' },
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Set of all event handler prop names, for the reconciler to detect
|
|
58
|
+
* event props and store them in _eventHandlers instead of attributes.
|
|
59
|
+
*/
|
|
60
|
+
export const EVENT_HANDLER_PROPS = new Set<string>([
|
|
61
|
+
'onKeyDown',
|
|
62
|
+
'onKeyDownCapture',
|
|
63
|
+
'onFocus',
|
|
64
|
+
'onFocusCapture',
|
|
65
|
+
'onBlur',
|
|
66
|
+
'onBlurCapture',
|
|
67
|
+
'onPaste',
|
|
68
|
+
'onPasteCapture',
|
|
69
|
+
'onResize',
|
|
70
|
+
'onClick',
|
|
71
|
+
'onMouseEnter',
|
|
72
|
+
'onMouseLeave',
|
|
73
|
+
])
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type EventTarget, TerminalEvent } from './terminal-event.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Focus event for component focus changes.
|
|
5
|
+
*
|
|
6
|
+
* Dispatched when focus moves between elements. 'focus' fires on the
|
|
7
|
+
* newly focused element, 'blur' fires on the previously focused one.
|
|
8
|
+
* Both bubble, matching react-dom's use of focusin/focusout semantics
|
|
9
|
+
* so parent components can observe descendant focus changes.
|
|
10
|
+
*/
|
|
11
|
+
export class FocusEvent extends TerminalEvent {
|
|
12
|
+
readonly relatedTarget: EventTarget | null
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
type: 'focus' | 'blur',
|
|
16
|
+
relatedTarget: EventTarget | null = null,
|
|
17
|
+
) {
|
|
18
|
+
super(type, { bubbles: true, cancelable: false })
|
|
19
|
+
this.relatedTarget = relatedTarget
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { nonAlphanumericKeys, type ParsedKey } from '../parse-keypress.js'
|
|
2
|
+
import { Event } from './event.js'
|
|
3
|
+
|
|
4
|
+
export type Key = {
|
|
5
|
+
upArrow: boolean
|
|
6
|
+
downArrow: boolean
|
|
7
|
+
leftArrow: boolean
|
|
8
|
+
rightArrow: boolean
|
|
9
|
+
pageDown: boolean
|
|
10
|
+
pageUp: boolean
|
|
11
|
+
wheelUp: boolean
|
|
12
|
+
wheelDown: boolean
|
|
13
|
+
home: boolean
|
|
14
|
+
end: boolean
|
|
15
|
+
return: boolean
|
|
16
|
+
escape: boolean
|
|
17
|
+
ctrl: boolean
|
|
18
|
+
shift: boolean
|
|
19
|
+
fn: boolean
|
|
20
|
+
tab: boolean
|
|
21
|
+
backspace: boolean
|
|
22
|
+
delete: boolean
|
|
23
|
+
meta: boolean
|
|
24
|
+
super: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseKey(keypress: ParsedKey): [Key, string] {
|
|
28
|
+
const key: Key = {
|
|
29
|
+
upArrow: keypress.name === 'up',
|
|
30
|
+
downArrow: keypress.name === 'down',
|
|
31
|
+
leftArrow: keypress.name === 'left',
|
|
32
|
+
rightArrow: keypress.name === 'right',
|
|
33
|
+
pageDown: keypress.name === 'pagedown',
|
|
34
|
+
pageUp: keypress.name === 'pageup',
|
|
35
|
+
wheelUp: keypress.name === 'wheelup',
|
|
36
|
+
wheelDown: keypress.name === 'wheeldown',
|
|
37
|
+
home: keypress.name === 'home',
|
|
38
|
+
end: keypress.name === 'end',
|
|
39
|
+
return: keypress.name === 'return',
|
|
40
|
+
escape: keypress.name === 'escape',
|
|
41
|
+
fn: keypress.fn,
|
|
42
|
+
ctrl: keypress.ctrl,
|
|
43
|
+
shift: keypress.shift,
|
|
44
|
+
tab: keypress.name === 'tab',
|
|
45
|
+
backspace: keypress.name === 'backspace',
|
|
46
|
+
delete: keypress.name === 'delete',
|
|
47
|
+
// `parseKeypress` parses \u001B\u001B[A (meta + up arrow) as meta = false
|
|
48
|
+
// but with option = true, so we need to take this into account here
|
|
49
|
+
// to avoid breaking changes in Ink.
|
|
50
|
+
// TODO(vadimdemedes): consider removing this in the next major version.
|
|
51
|
+
meta: keypress.meta || keypress.name === 'escape' || keypress.option,
|
|
52
|
+
// Super (Cmd on macOS / Win key) — only arrives via kitty keyboard
|
|
53
|
+
// protocol CSI u sequences. Distinct from meta (Alt/Option) so
|
|
54
|
+
// bindings like cmd+c can be expressed separately from opt+c.
|
|
55
|
+
super: keypress.super,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let input = keypress.ctrl ? keypress.name : keypress.sequence
|
|
59
|
+
|
|
60
|
+
// Handle undefined input case
|
|
61
|
+
if (input === undefined) {
|
|
62
|
+
input = ''
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// When ctrl is set, keypress.name for space is the literal word "space".
|
|
66
|
+
// Convert to actual space character for consistency with the CSI u branch
|
|
67
|
+
// (which maps 'space' → ' '). Without this, ctrl+space leaks the literal
|
|
68
|
+
// word "space" into text input.
|
|
69
|
+
if (keypress.ctrl && input === 'space') {
|
|
70
|
+
input = ' '
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Suppress unrecognized escape sequences that were parsed as function keys
|
|
74
|
+
// (matched by FN_KEY_RE) but have no name in the keyName map.
|
|
75
|
+
// Examples: ESC[25~ (F13/Right Alt on Windows), ESC[26~ (F14), etc.
|
|
76
|
+
// Without this, the ESC prefix is stripped below and the remainder (e.g.,
|
|
77
|
+
// "[25~") leaks into the input as literal text.
|
|
78
|
+
if (keypress.code && !keypress.name) {
|
|
79
|
+
input = ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Suppress ESC-less SGR mouse fragments. When a heavy React commit blocks
|
|
83
|
+
// the event loop past App's 50ms NORMAL_TIMEOUT flush, a CSI split across
|
|
84
|
+
// stdin chunks gets its buffered ESC flushed as a lone Escape key, and the
|
|
85
|
+
// continuation arrives as a text token with name='' — which falls through
|
|
86
|
+
// all of parseKeypress's ESC-anchored regexes and the nonAlphanumericKeys
|
|
87
|
+
// clear below (name is falsy). The fragment then leaks into the prompt as
|
|
88
|
+
// literal `[<64;74;16M`. This is the same defensive sink as the F13 guard
|
|
89
|
+
// above; the underlying tokenizer-flush race is upstream of this layer.
|
|
90
|
+
if (!keypress.name && /^\[<\d+;\d+;\d+[Mm]/.test(input)) {
|
|
91
|
+
input = ''
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Strip meta if it's still remaining after `parseKeypress`
|
|
95
|
+
// TODO(vadimdemedes): remove this in the next major version.
|
|
96
|
+
if (input.startsWith('\u001B')) {
|
|
97
|
+
input = input.slice(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Track whether we've already processed this as a special sequence
|
|
101
|
+
// that converted input to the key name (CSI u or application keypad mode).
|
|
102
|
+
// For these, we don't want to clear input with nonAlphanumericKeys check.
|
|
103
|
+
let processedAsSpecialSequence = false
|
|
104
|
+
|
|
105
|
+
// Handle CSI u sequences (Kitty keyboard protocol): after stripping ESC,
|
|
106
|
+
// we're left with "[codepoint;modifieru" (e.g., "[98;3u" for Alt+b).
|
|
107
|
+
// Use the parsed key name instead for input handling. Require a digit
|
|
108
|
+
// after [ — real CSI u is always [<digits>…u, and a bare startsWith('[')
|
|
109
|
+
// false-matches X10 mouse at row 85 (Cy = 85+32 = 'u'), leaking the
|
|
110
|
+
// literal text "mouse" into the prompt via processedAsSpecialSequence.
|
|
111
|
+
if (/^\[\d/.test(input) && input.endsWith('u')) {
|
|
112
|
+
if (!keypress.name) {
|
|
113
|
+
// Unmapped Kitty functional key (Caps Lock 57358, F13–F35, KP nav,
|
|
114
|
+
// bare modifiers, etc.) — keycodeToName() returned undefined. Swallow
|
|
115
|
+
// so the raw "[57358u" doesn't leak into the prompt. See #38781.
|
|
116
|
+
input = ''
|
|
117
|
+
} else {
|
|
118
|
+
// 'space' → ' '; 'escape' → '' (key.escape carries it;
|
|
119
|
+
// processedAsSpecialSequence bypasses the nonAlphanumericKeys
|
|
120
|
+
// clear below, so we must handle it explicitly here);
|
|
121
|
+
// otherwise use key name.
|
|
122
|
+
input =
|
|
123
|
+
keypress.name === 'space'
|
|
124
|
+
? ' '
|
|
125
|
+
: keypress.name === 'escape'
|
|
126
|
+
? ''
|
|
127
|
+
: keypress.name
|
|
128
|
+
}
|
|
129
|
+
processedAsSpecialSequence = true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle xterm modifyOtherKeys sequences: after stripping ESC, we're left
|
|
133
|
+
// with "[27;modifier;keycode~" (e.g., "[27;3;98~" for Alt+b). Same
|
|
134
|
+
// extraction as CSI u — without this, printable-char keycodes (single-letter
|
|
135
|
+
// names) skip the nonAlphanumericKeys clear and leak "[27;..." as input.
|
|
136
|
+
if (input.startsWith('[27;') && input.endsWith('~')) {
|
|
137
|
+
if (!keypress.name) {
|
|
138
|
+
// Unmapped modifyOtherKeys keycode — swallow for consistency with
|
|
139
|
+
// the CSI u handler above. Practically untriggerable today (xterm
|
|
140
|
+
// modifyOtherKeys only sends ASCII keycodes, all mapped), but
|
|
141
|
+
// guards against future terminal behavior.
|
|
142
|
+
input = ''
|
|
143
|
+
} else {
|
|
144
|
+
input =
|
|
145
|
+
keypress.name === 'space'
|
|
146
|
+
? ' '
|
|
147
|
+
: keypress.name === 'escape'
|
|
148
|
+
? ''
|
|
149
|
+
: keypress.name
|
|
150
|
+
}
|
|
151
|
+
processedAsSpecialSequence = true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Handle application keypad mode sequences: after stripping ESC,
|
|
155
|
+
// we're left with "O<letter>" (e.g., "Op" for numpad 0, "Oy" for numpad 9).
|
|
156
|
+
// Use the parsed key name (the digit character) for input handling.
|
|
157
|
+
if (
|
|
158
|
+
input.startsWith('O') &&
|
|
159
|
+
input.length === 2 &&
|
|
160
|
+
keypress.name &&
|
|
161
|
+
keypress.name.length === 1
|
|
162
|
+
) {
|
|
163
|
+
input = keypress.name
|
|
164
|
+
processedAsSpecialSequence = true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clear input for non-alphanumeric keys (arrows, function keys, etc.)
|
|
168
|
+
// Skip this for CSI u and application keypad mode sequences since
|
|
169
|
+
// those were already converted to their proper input characters.
|
|
170
|
+
if (
|
|
171
|
+
!processedAsSpecialSequence &&
|
|
172
|
+
keypress.name &&
|
|
173
|
+
nonAlphanumericKeys.includes(keypress.name)
|
|
174
|
+
) {
|
|
175
|
+
input = ''
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Set shift=true for uppercase letters (A-Z)
|
|
179
|
+
// Must check it's actually a letter, not just any char unchanged by toUpperCase
|
|
180
|
+
if (
|
|
181
|
+
input.length === 1 &&
|
|
182
|
+
typeof input[0] === 'string' &&
|
|
183
|
+
input[0] >= 'A' &&
|
|
184
|
+
input[0] <= 'Z'
|
|
185
|
+
) {
|
|
186
|
+
key.shift = true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return [key, input]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export class InputEvent extends Event {
|
|
193
|
+
readonly keypress: ParsedKey
|
|
194
|
+
readonly key: Key
|
|
195
|
+
readonly input: string
|
|
196
|
+
|
|
197
|
+
constructor(keypress: ParsedKey) {
|
|
198
|
+
super()
|
|
199
|
+
const [key, input] = parseKey(keypress)
|
|
200
|
+
|
|
201
|
+
this.keypress = keypress
|
|
202
|
+
this.key = key
|
|
203
|
+
this.input = input
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ParsedKey } from '../parse-keypress.js'
|
|
2
|
+
import { TerminalEvent } from './terminal-event.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Keyboard event dispatched through the DOM tree via capture/bubble.
|
|
6
|
+
*
|
|
7
|
+
* Follows browser KeyboardEvent semantics: `key` is the literal character
|
|
8
|
+
* for printable keys ('a', '3', ' ', '/') and a multi-char name for
|
|
9
|
+
* special keys ('down', 'return', 'escape', 'f1'). The idiomatic
|
|
10
|
+
* printable-char check is `e.key.length === 1`.
|
|
11
|
+
*/
|
|
12
|
+
export class KeyboardEvent extends TerminalEvent {
|
|
13
|
+
readonly key: string
|
|
14
|
+
readonly ctrl: boolean
|
|
15
|
+
readonly shift: boolean
|
|
16
|
+
readonly meta: boolean
|
|
17
|
+
readonly superKey: boolean
|
|
18
|
+
readonly fn: boolean
|
|
19
|
+
|
|
20
|
+
constructor(parsedKey: ParsedKey) {
|
|
21
|
+
super('keydown', { bubbles: true, cancelable: true })
|
|
22
|
+
|
|
23
|
+
this.key = keyFromParsed(parsedKey)
|
|
24
|
+
this.ctrl = parsedKey.ctrl
|
|
25
|
+
this.shift = parsedKey.shift
|
|
26
|
+
this.meta = parsedKey.meta || parsedKey.option
|
|
27
|
+
this.superKey = parsedKey.super
|
|
28
|
+
this.fn = parsedKey.fn
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function keyFromParsed(parsed: ParsedKey): string {
|
|
33
|
+
const seq = parsed.sequence ?? ''
|
|
34
|
+
const name = parsed.name ?? ''
|
|
35
|
+
|
|
36
|
+
// Ctrl combos: sequence is a control byte (\x03 for ctrl+c), name is the
|
|
37
|
+
// letter. Browsers report e.key === 'c' with e.ctrlKey === true.
|
|
38
|
+
if (parsed.ctrl) return name
|
|
39
|
+
|
|
40
|
+
// Single printable char (space through ~, plus anything above ASCII):
|
|
41
|
+
// use the literal char. Browsers report e.key === '3', not 'Digit3'.
|
|
42
|
+
if (seq.length === 1) {
|
|
43
|
+
const code = seq.charCodeAt(0)
|
|
44
|
+
if (code >= 0x20 && code !== 0x7f) return seq
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Special keys (arrows, F-keys, return, tab, escape, etc.): sequence is
|
|
48
|
+
// either an escape sequence (\x1b[B) or a control byte (\r, \t), so use
|
|
49
|
+
// the parsed name. Browsers report e.key === 'ArrowDown'.
|
|
50
|
+
return name || seq
|
|
51
|
+
}
|