@silvery/tea 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +54 -0
- package/src/core/index.ts +225 -0
- package/src/core/slice.ts +69 -0
- package/src/create-command-registry.ts +106 -0
- package/src/effects.ts +145 -0
- package/src/focus-events.ts +243 -0
- package/src/focus-manager.ts +491 -0
- package/src/focus-queries.ts +241 -0
- package/src/index.ts +213 -0
- package/src/keys.ts +1382 -0
- package/src/pipe.ts +110 -0
- package/src/plugins.ts +119 -0
- package/src/store/index.ts +306 -0
- package/src/streams/index.ts +405 -0
- package/src/tea/README.md +208 -0
- package/src/tea/index.ts +174 -0
- package/src/text-cursor.ts +206 -0
- package/src/text-decorations.ts +253 -0
- package/src/text-ops.ts +150 -0
- package/src/tree-utils.ts +27 -0
- package/src/types.ts +670 -0
- package/src/with-commands.ts +337 -0
- package/src/with-diagnostics.ts +955 -0
- package/src/with-dom-events.ts +168 -0
- package/src/with-focus.ts +162 -0
- package/src/with-keybindings.ts +180 -0
- package/src/with-react.ts +92 -0
- package/src/with-render.ts +92 -0
- package/src/with-terminal.ts +219 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* withTerminal(process, opts?) — Plugin: ALL terminal I/O
|
|
3
|
+
*
|
|
4
|
+
* This plugin represents the terminal I/O layer in silvery's plugin
|
|
5
|
+
* composition model. It wraps all terminal concerns:
|
|
6
|
+
* - stdin → typed events (term:key, term:mouse, term:paste)
|
|
7
|
+
* - stdout → alternate screen, raw mode, incremental diff output
|
|
8
|
+
* - SIGWINCH → term:resize
|
|
9
|
+
* - Lifecycle (Ctrl+Z suspend/resume, Ctrl+C exit)
|
|
10
|
+
* - Protocols (SGR mouse, Kitty keyboard, bracketed paste)
|
|
11
|
+
*
|
|
12
|
+
* In the current architecture, terminal I/O is handled by createApp()
|
|
13
|
+
* and the TermProvider. This plugin provides the declarative interface
|
|
14
|
+
* for pipe() composition:
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const app = pipe(
|
|
18
|
+
* createApp(store),
|
|
19
|
+
* withReact(<Board />),
|
|
20
|
+
* withTerminal(process, { mouse: true, kitty: true }),
|
|
21
|
+
* withFocus(),
|
|
22
|
+
* withDomEvents(),
|
|
23
|
+
* )
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import { pipe, withTerminal } from '@silvery/tea'
|
|
29
|
+
*
|
|
30
|
+
* // All protocols enabled by default
|
|
31
|
+
* const app = pipe(baseApp, withTerminal(process))
|
|
32
|
+
*
|
|
33
|
+
* // Customize terminal options
|
|
34
|
+
* const app = pipe(baseApp, withTerminal(process, {
|
|
35
|
+
* mouse: true,
|
|
36
|
+
* kitty: true,
|
|
37
|
+
* paste: true,
|
|
38
|
+
* onSuspend: () => saveState(),
|
|
39
|
+
* onResume: () => restoreState(),
|
|
40
|
+
* }))
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Types
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Process-like object that provides stdin/stdout streams.
|
|
50
|
+
* Accepts the Node.js global `process` or a mock.
|
|
51
|
+
*/
|
|
52
|
+
export interface ProcessLike {
|
|
53
|
+
stdin: NodeJS.ReadStream
|
|
54
|
+
stdout: NodeJS.WriteStream
|
|
55
|
+
[key: string]: unknown
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Options for withTerminal.
|
|
60
|
+
*/
|
|
61
|
+
export interface WithTerminalOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Enable SGR mouse tracking.
|
|
64
|
+
* Default: true
|
|
65
|
+
*/
|
|
66
|
+
mouse?: boolean
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Enable Kitty keyboard protocol.
|
|
70
|
+
* - `true`: auto-detect and enable with DISAMBIGUATE flag
|
|
71
|
+
* - number: enable with specific KittyFlags bitfield
|
|
72
|
+
* - `false`: don't enable
|
|
73
|
+
* Default: true
|
|
74
|
+
*/
|
|
75
|
+
kitty?: boolean | number
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enable bracketed paste mode.
|
|
79
|
+
* Default: true
|
|
80
|
+
*/
|
|
81
|
+
paste?: boolean
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Enter alternate screen buffer.
|
|
85
|
+
* Default: true
|
|
86
|
+
*/
|
|
87
|
+
alternateScreen?: boolean
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handle Ctrl+Z by suspending the process.
|
|
91
|
+
* Default: true
|
|
92
|
+
*/
|
|
93
|
+
suspendOnCtrlZ?: boolean
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle Ctrl+C by restoring terminal and exiting.
|
|
97
|
+
* Default: true
|
|
98
|
+
*/
|
|
99
|
+
exitOnCtrlC?: boolean
|
|
100
|
+
|
|
101
|
+
/** Called before suspend. Return false to prevent. */
|
|
102
|
+
onSuspend?: () => boolean | void
|
|
103
|
+
|
|
104
|
+
/** Called after resume from suspend. */
|
|
105
|
+
onResume?: () => void
|
|
106
|
+
|
|
107
|
+
/** Called on Ctrl+C. Return false to prevent exit. */
|
|
108
|
+
onInterrupt?: () => boolean | void
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Enable Kitty text sizing protocol for PUA characters.
|
|
112
|
+
* Default: false
|
|
113
|
+
*/
|
|
114
|
+
textSizing?: boolean | "auto"
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Enable terminal focus reporting.
|
|
118
|
+
* Default: false
|
|
119
|
+
*/
|
|
120
|
+
focusReporting?: boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* App enhanced with terminal configuration.
|
|
125
|
+
*/
|
|
126
|
+
export interface AppWithTerminal {
|
|
127
|
+
/** The terminal options for this app */
|
|
128
|
+
readonly terminalOptions: WithTerminalOptions & { proc: ProcessLike }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Minimal app shape that withTerminal can enhance.
|
|
133
|
+
*/
|
|
134
|
+
interface RunnableApp {
|
|
135
|
+
run(...args: unknown[]): unknown
|
|
136
|
+
[key: string]: unknown
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Implementation
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Configure terminal I/O for an app.
|
|
145
|
+
*
|
|
146
|
+
* In pipe() composition, this captures the process streams and options
|
|
147
|
+
* so that run() configures terminal I/O correctly.
|
|
148
|
+
*
|
|
149
|
+
* The plugin wraps `run()` to inject terminal options:
|
|
150
|
+
* - stdin/stdout from the process object
|
|
151
|
+
* - Protocol options (mouse, kitty, paste)
|
|
152
|
+
* - Lifecycle handlers (suspend, resume, interrupt)
|
|
153
|
+
*
|
|
154
|
+
* @param proc - Process object with stdin/stdout (typically `process`)
|
|
155
|
+
* @param options - Terminal configuration
|
|
156
|
+
* @returns Plugin function that binds terminal config to the app
|
|
157
|
+
*/
|
|
158
|
+
export function withTerminal<T extends RunnableApp>(
|
|
159
|
+
proc: ProcessLike,
|
|
160
|
+
options: WithTerminalOptions = {},
|
|
161
|
+
): (app: T) => T & AppWithTerminal {
|
|
162
|
+
const termConfig = {
|
|
163
|
+
mouse: options.mouse ?? true,
|
|
164
|
+
kitty: options.kitty ?? true,
|
|
165
|
+
paste: options.paste ?? true,
|
|
166
|
+
alternateScreen: options.alternateScreen ?? true,
|
|
167
|
+
suspendOnCtrlZ: options.suspendOnCtrlZ ?? true,
|
|
168
|
+
exitOnCtrlC: options.exitOnCtrlC ?? true,
|
|
169
|
+
...options,
|
|
170
|
+
proc,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (app: T): T & AppWithTerminal => {
|
|
174
|
+
const originalRun = app.run
|
|
175
|
+
|
|
176
|
+
return Object.assign(Object.create(app), {
|
|
177
|
+
terminalOptions: termConfig,
|
|
178
|
+
run(...args: unknown[]) {
|
|
179
|
+
// Inject terminal options into the run call
|
|
180
|
+
// The first arg after element is typically options
|
|
181
|
+
const runOptions: Record<string, unknown> = {}
|
|
182
|
+
|
|
183
|
+
// Find or create options argument
|
|
184
|
+
let existingOptions: Record<string, unknown> | undefined
|
|
185
|
+
if (args.length > 0 && typeof args[args.length - 1] === "object" && args[args.length - 1] !== null) {
|
|
186
|
+
existingOptions = args[args.length - 1] as Record<string, unknown>
|
|
187
|
+
// Don't treat React elements as options
|
|
188
|
+
if ("type" in existingOptions && "props" in existingOptions) {
|
|
189
|
+
existingOptions = undefined
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Merge terminal options
|
|
194
|
+
Object.assign(runOptions, existingOptions ?? {}, {
|
|
195
|
+
stdin: proc.stdin,
|
|
196
|
+
stdout: proc.stdout,
|
|
197
|
+
mouse: termConfig.mouse,
|
|
198
|
+
kitty: termConfig.kitty,
|
|
199
|
+
alternateScreen: termConfig.alternateScreen,
|
|
200
|
+
suspendOnCtrlZ: termConfig.suspendOnCtrlZ,
|
|
201
|
+
exitOnCtrlC: termConfig.exitOnCtrlC,
|
|
202
|
+
textSizing: termConfig.textSizing,
|
|
203
|
+
focusReporting: termConfig.focusReporting,
|
|
204
|
+
onSuspend: termConfig.onSuspend,
|
|
205
|
+
onResume: termConfig.onResume,
|
|
206
|
+
onInterrupt: termConfig.onInterrupt,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Replace options in args
|
|
210
|
+
if (existingOptions) {
|
|
211
|
+
const newArgs = [...args]
|
|
212
|
+
newArgs[newArgs.length - 1] = runOptions
|
|
213
|
+
return originalRun.apply(app, newArgs)
|
|
214
|
+
}
|
|
215
|
+
return originalRun.call(app, ...args, runOptions)
|
|
216
|
+
},
|
|
217
|
+
}) as T & AppWithTerminal
|
|
218
|
+
}
|
|
219
|
+
}
|