oc-plugin-smoke-test 0.0.1
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 +3 -0
- package/bun.lock +432 -0
- package/index.tsx +812 -0
- package/package.json +22 -0
- package/smoke-theme.json +223 -0
- package/tsconfig.json +23 -0
package/index.tsx
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
|
3
|
+
import { RGBA, VignetteEffect } from "@opentui/core"
|
|
4
|
+
import type { TuiApi, TuiKeybindSet, TuiPluginInit, TuiPluginInput } from "@opencode-ai/plugin/tui"
|
|
5
|
+
|
|
6
|
+
const tabs = ["overview", "counter", "help"]
|
|
7
|
+
const bind = {
|
|
8
|
+
modal: "ctrl+shift+m",
|
|
9
|
+
screen: "ctrl+shift+o",
|
|
10
|
+
home: "escape,ctrl+h",
|
|
11
|
+
left: "left,h",
|
|
12
|
+
right: "right,l",
|
|
13
|
+
up: "up,k",
|
|
14
|
+
down: "down,j",
|
|
15
|
+
alert: "a",
|
|
16
|
+
confirm: "c",
|
|
17
|
+
prompt: "p",
|
|
18
|
+
select: "s",
|
|
19
|
+
modal_accept: "enter,return",
|
|
20
|
+
modal_close: "escape",
|
|
21
|
+
dialog_close: "escape",
|
|
22
|
+
local: "x",
|
|
23
|
+
local_push: "enter,return",
|
|
24
|
+
local_close: "q,backspace",
|
|
25
|
+
host: "z",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pick = (value: unknown, fallback: string) => {
|
|
29
|
+
if (typeof value !== "string") return fallback
|
|
30
|
+
if (!value.trim()) return fallback
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const num = (value: unknown, fallback: number) => {
|
|
35
|
+
if (typeof value !== "number") return fallback
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rec = (value: unknown) => {
|
|
40
|
+
if (!value || typeof value !== "object") return
|
|
41
|
+
return value as Record<string, unknown>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Cfg = {
|
|
45
|
+
label: string
|
|
46
|
+
route: string
|
|
47
|
+
vignette: number
|
|
48
|
+
keybinds: Record<string, unknown> | undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Route = {
|
|
52
|
+
modal: string
|
|
53
|
+
screen: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type State = {
|
|
57
|
+
tab: number
|
|
58
|
+
count: number
|
|
59
|
+
source: string
|
|
60
|
+
note: string
|
|
61
|
+
selected: string
|
|
62
|
+
local: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cfg = (options: Record<string, unknown> | undefined) => {
|
|
66
|
+
return {
|
|
67
|
+
label: pick(options?.label, "smoke"),
|
|
68
|
+
route: pick(options?.route, "workspace-smoke"),
|
|
69
|
+
vignette: Math.max(0, num(options?.vignette, 0.35)),
|
|
70
|
+
keybinds: rec(options?.keybinds),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const names = (input: Cfg) => {
|
|
75
|
+
return {
|
|
76
|
+
modal: `${input.route}.modal`,
|
|
77
|
+
screen: `${input.route}.screen`,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type Keys = TuiKeybindSet
|
|
82
|
+
const ui = {
|
|
83
|
+
panel: "#1d1d1d",
|
|
84
|
+
border: "#4a4a4a",
|
|
85
|
+
text: "#f0f0f0",
|
|
86
|
+
muted: "#a5a5a5",
|
|
87
|
+
accent: "#5f87ff",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type Color = RGBA | string
|
|
91
|
+
|
|
92
|
+
const tone = (api: TuiApi) => {
|
|
93
|
+
const map = api.theme.current as Record<string, unknown>
|
|
94
|
+
const get = (name: string, fallback: string): Color => {
|
|
95
|
+
const value = map[name]
|
|
96
|
+
if (typeof value === "string") return value
|
|
97
|
+
if (value && typeof value === "object") return value as RGBA
|
|
98
|
+
return fallback
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
panel: get("backgroundPanel", ui.panel),
|
|
102
|
+
border: get("border", ui.border),
|
|
103
|
+
text: get("text", ui.text),
|
|
104
|
+
muted: get("textMuted", ui.muted),
|
|
105
|
+
accent: get("primary", ui.accent),
|
|
106
|
+
selected: get("selectedListItemText", ui.text),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type Skin = {
|
|
111
|
+
panel: Color
|
|
112
|
+
border: Color
|
|
113
|
+
text: Color
|
|
114
|
+
muted: Color
|
|
115
|
+
accent: Color
|
|
116
|
+
selected: Color
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const Btn = (props: { txt: string; run: () => void; skin: Skin; on?: boolean }) => {
|
|
120
|
+
return (
|
|
121
|
+
<box
|
|
122
|
+
onMouseUp={() => {
|
|
123
|
+
props.run()
|
|
124
|
+
}}
|
|
125
|
+
backgroundColor={props.on ? props.skin.accent : props.skin.border}
|
|
126
|
+
paddingLeft={1}
|
|
127
|
+
paddingRight={1}
|
|
128
|
+
>
|
|
129
|
+
<text fg={props.on ? props.skin.selected : props.skin.text}>{props.txt}</text>
|
|
130
|
+
</box>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const parse = (params: Record<string, unknown> | undefined) => {
|
|
135
|
+
const tab = typeof params?.tab === "number" ? params.tab : 0
|
|
136
|
+
const count = typeof params?.count === "number" ? params.count : 0
|
|
137
|
+
const source = typeof params?.source === "string" ? params.source : "unknown"
|
|
138
|
+
const note = typeof params?.note === "string" ? params.note : ""
|
|
139
|
+
const selected = typeof params?.selected === "string" ? params.selected : ""
|
|
140
|
+
const local = typeof params?.local === "number" ? params.local : 0
|
|
141
|
+
return {
|
|
142
|
+
tab: Math.max(0, Math.min(tab, tabs.length - 1)),
|
|
143
|
+
count,
|
|
144
|
+
source,
|
|
145
|
+
note,
|
|
146
|
+
selected,
|
|
147
|
+
local: Math.max(0, local),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const current = (api: TuiApi, route: Route) => {
|
|
152
|
+
const value = api.route.current
|
|
153
|
+
const ok = Object.values(route).includes(value.name)
|
|
154
|
+
if (!ok) return parse(undefined)
|
|
155
|
+
if (!("params" in value)) return parse(undefined)
|
|
156
|
+
return parse(value.params)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const opts = [
|
|
160
|
+
{
|
|
161
|
+
title: "Overview",
|
|
162
|
+
value: 0,
|
|
163
|
+
description: "Switch to overview tab",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
title: "Counter",
|
|
167
|
+
value: 1,
|
|
168
|
+
description: "Switch to counter tab",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
title: "Help",
|
|
172
|
+
value: 2,
|
|
173
|
+
description: "Switch to help tab",
|
|
174
|
+
},
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
const host = (api: TuiApi, input: Cfg, skin: Skin) => {
|
|
178
|
+
api.ui.dialog.setSize("medium")
|
|
179
|
+
api.ui.dialog.replace(() => (
|
|
180
|
+
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
|
181
|
+
<text fg={skin.text}>
|
|
182
|
+
<b>{input.label} host overlay</b>
|
|
183
|
+
</text>
|
|
184
|
+
<text fg={skin.muted}>Using api.ui.dialog stack with built-in backdrop</text>
|
|
185
|
+
<text fg={skin.muted}>esc closes · depth {api.ui.dialog.depth}</text>
|
|
186
|
+
<box flexDirection="row" gap={1}>
|
|
187
|
+
<Btn txt="close" run={() => api.ui.dialog.clear()} skin={skin} on />
|
|
188
|
+
</box>
|
|
189
|
+
</box>
|
|
190
|
+
))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const warn = (api: TuiApi, route: Route, value: State) => {
|
|
194
|
+
const DialogAlert = api.ui.DialogAlert
|
|
195
|
+
api.ui.dialog.setSize("medium")
|
|
196
|
+
api.ui.dialog.replace(() => (
|
|
197
|
+
<DialogAlert
|
|
198
|
+
title="Smoke alert"
|
|
199
|
+
message="Testing built-in alert dialog"
|
|
200
|
+
onConfirm={() => api.route.navigate(route.screen, { ...value, source: "alert" })}
|
|
201
|
+
/>
|
|
202
|
+
))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const check = (api: TuiApi, route: Route, value: State) => {
|
|
206
|
+
const DialogConfirm = api.ui.DialogConfirm
|
|
207
|
+
api.ui.dialog.setSize("medium")
|
|
208
|
+
api.ui.dialog.replace(() => (
|
|
209
|
+
<DialogConfirm
|
|
210
|
+
title="Smoke confirm"
|
|
211
|
+
message="Apply +1 to counter?"
|
|
212
|
+
onConfirm={() => api.route.navigate(route.screen, { ...value, count: value.count + 1, source: "confirm" })}
|
|
213
|
+
onCancel={() => api.route.navigate(route.screen, { ...value, source: "confirm-cancel" })}
|
|
214
|
+
/>
|
|
215
|
+
))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const entry = (api: TuiApi, route: Route, value: State) => {
|
|
219
|
+
const DialogPrompt = api.ui.DialogPrompt
|
|
220
|
+
api.ui.dialog.setSize("medium")
|
|
221
|
+
api.ui.dialog.replace(() => (
|
|
222
|
+
<DialogPrompt
|
|
223
|
+
title="Smoke prompt"
|
|
224
|
+
value={value.note}
|
|
225
|
+
onConfirm={(note) => {
|
|
226
|
+
api.ui.dialog.clear()
|
|
227
|
+
api.route.navigate(route.screen, { ...value, note, source: "prompt" })
|
|
228
|
+
}}
|
|
229
|
+
onCancel={() => {
|
|
230
|
+
api.ui.dialog.clear()
|
|
231
|
+
api.route.navigate(route.screen, value)
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const picker = (api: TuiApi, route: Route, value: State) => {
|
|
238
|
+
const DialogSelect = api.ui.DialogSelect
|
|
239
|
+
api.ui.dialog.setSize("medium")
|
|
240
|
+
api.ui.dialog.replace(() => (
|
|
241
|
+
<DialogSelect
|
|
242
|
+
title="Smoke select"
|
|
243
|
+
options={opts}
|
|
244
|
+
current={value.tab}
|
|
245
|
+
onSelect={(item) => {
|
|
246
|
+
api.ui.dialog.clear()
|
|
247
|
+
api.route.navigate(route.screen, {
|
|
248
|
+
...value,
|
|
249
|
+
tab: typeof item.value === "number" ? item.value : value.tab,
|
|
250
|
+
selected: item.title,
|
|
251
|
+
source: "select",
|
|
252
|
+
})
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const Screen = (props: {
|
|
259
|
+
api: TuiApi
|
|
260
|
+
input: Cfg
|
|
261
|
+
route: Route
|
|
262
|
+
keys: Keys
|
|
263
|
+
meta: TuiPluginInit
|
|
264
|
+
params?: Record<string, unknown>
|
|
265
|
+
}) => {
|
|
266
|
+
const dim = useTerminalDimensions()
|
|
267
|
+
const value = parse(props.params)
|
|
268
|
+
const skin = tone(props.api)
|
|
269
|
+
const set = (local: number, base?: State) => {
|
|
270
|
+
const next = base ?? current(props.api, props.route)
|
|
271
|
+
props.api.route.navigate(props.route.screen, { ...next, local: Math.max(0, local), source: "local" })
|
|
272
|
+
}
|
|
273
|
+
const push = (base?: State) => {
|
|
274
|
+
const next = base ?? current(props.api, props.route)
|
|
275
|
+
set(next.local + 1, next)
|
|
276
|
+
}
|
|
277
|
+
const open = () => {
|
|
278
|
+
const next = current(props.api, props.route)
|
|
279
|
+
if (next.local > 0) return
|
|
280
|
+
set(1, next)
|
|
281
|
+
}
|
|
282
|
+
const pop = (base?: State) => {
|
|
283
|
+
const next = base ?? current(props.api, props.route)
|
|
284
|
+
const local = Math.max(0, next.local - 1)
|
|
285
|
+
set(local, next)
|
|
286
|
+
}
|
|
287
|
+
const show = () => {
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
open()
|
|
290
|
+
}, 0)
|
|
291
|
+
}
|
|
292
|
+
useKeyboard((evt) => {
|
|
293
|
+
if (props.api.route.current.name !== props.route.screen) return
|
|
294
|
+
const next = current(props.api, props.route)
|
|
295
|
+
if (props.api.ui.dialog.open) {
|
|
296
|
+
if (props.keys.match("dialog_close", evt)) {
|
|
297
|
+
evt.preventDefault()
|
|
298
|
+
evt.stopPropagation()
|
|
299
|
+
props.api.ui.dialog.clear()
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (next.local > 0) {
|
|
306
|
+
if (evt.name === "escape" || props.keys.match("local_close", evt)) {
|
|
307
|
+
evt.preventDefault()
|
|
308
|
+
evt.stopPropagation()
|
|
309
|
+
pop(next)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (props.keys.match("local_push", evt)) {
|
|
314
|
+
evt.preventDefault()
|
|
315
|
+
evt.stopPropagation()
|
|
316
|
+
push(next)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (props.keys.match("home", evt)) {
|
|
323
|
+
evt.preventDefault()
|
|
324
|
+
evt.stopPropagation()
|
|
325
|
+
props.api.route.navigate("home")
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (props.keys.match("left", evt)) {
|
|
330
|
+
evt.preventDefault()
|
|
331
|
+
evt.stopPropagation()
|
|
332
|
+
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length })
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (props.keys.match("right", evt)) {
|
|
337
|
+
evt.preventDefault()
|
|
338
|
+
evt.stopPropagation()
|
|
339
|
+
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length })
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (props.keys.match("up", evt)) {
|
|
344
|
+
evt.preventDefault()
|
|
345
|
+
evt.stopPropagation()
|
|
346
|
+
props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 })
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (props.keys.match("down", evt)) {
|
|
351
|
+
evt.preventDefault()
|
|
352
|
+
evt.stopPropagation()
|
|
353
|
+
props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 })
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (props.keys.match("modal", evt)) {
|
|
358
|
+
evt.preventDefault()
|
|
359
|
+
evt.stopPropagation()
|
|
360
|
+
props.api.route.navigate(props.route.modal, next)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (props.keys.match("local", evt)) {
|
|
365
|
+
evt.preventDefault()
|
|
366
|
+
evt.stopPropagation()
|
|
367
|
+
open()
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (props.keys.match("host", evt)) {
|
|
372
|
+
evt.preventDefault()
|
|
373
|
+
evt.stopPropagation()
|
|
374
|
+
host(props.api, props.input, skin)
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (props.keys.match("alert", evt)) {
|
|
379
|
+
evt.preventDefault()
|
|
380
|
+
evt.stopPropagation()
|
|
381
|
+
warn(props.api, props.route, next)
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (props.keys.match("confirm", evt)) {
|
|
386
|
+
evt.preventDefault()
|
|
387
|
+
evt.stopPropagation()
|
|
388
|
+
check(props.api, props.route, next)
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (props.keys.match("prompt", evt)) {
|
|
393
|
+
evt.preventDefault()
|
|
394
|
+
evt.stopPropagation()
|
|
395
|
+
entry(props.api, props.route, next)
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (props.keys.match("select", evt)) {
|
|
400
|
+
evt.preventDefault()
|
|
401
|
+
evt.stopPropagation()
|
|
402
|
+
picker(props.api, props.route, next)
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<box width={dim().width} height={dim().height} backgroundColor={skin.panel} position="relative">
|
|
408
|
+
<box
|
|
409
|
+
flexDirection="column"
|
|
410
|
+
width="100%"
|
|
411
|
+
height="100%"
|
|
412
|
+
paddingTop={1}
|
|
413
|
+
paddingBottom={1}
|
|
414
|
+
paddingLeft={2}
|
|
415
|
+
paddingRight={2}
|
|
416
|
+
>
|
|
417
|
+
<box flexDirection="row" justifyContent="space-between" paddingBottom={1}>
|
|
418
|
+
<text fg={skin.text}>
|
|
419
|
+
<b>{props.input.label} screen</b>
|
|
420
|
+
<span style={{ fg: skin.muted }}> plugin route</span>
|
|
421
|
+
</text>
|
|
422
|
+
<text fg={skin.muted}>{props.keys.print("home")} home</text>
|
|
423
|
+
</box>
|
|
424
|
+
|
|
425
|
+
<box flexDirection="row" gap={1} paddingBottom={1}>
|
|
426
|
+
{tabs.map((item, i) => {
|
|
427
|
+
const on = value.tab === i
|
|
428
|
+
return (
|
|
429
|
+
<Btn
|
|
430
|
+
txt={item}
|
|
431
|
+
run={() => props.api.route.navigate(props.route.screen, { ...value, tab: i })}
|
|
432
|
+
skin={skin}
|
|
433
|
+
on={on}
|
|
434
|
+
/>
|
|
435
|
+
)
|
|
436
|
+
})}
|
|
437
|
+
</box>
|
|
438
|
+
|
|
439
|
+
<box
|
|
440
|
+
border
|
|
441
|
+
borderColor={skin.border}
|
|
442
|
+
paddingTop={1}
|
|
443
|
+
paddingBottom={1}
|
|
444
|
+
paddingLeft={2}
|
|
445
|
+
paddingRight={2}
|
|
446
|
+
flexGrow={1}
|
|
447
|
+
>
|
|
448
|
+
{value.tab === 0 ? (
|
|
449
|
+
<box flexDirection="column" gap={1}>
|
|
450
|
+
<text fg={skin.text}>Route: {props.route.screen}</text>
|
|
451
|
+
<text fg={skin.muted}>plugin state: {props.meta.state}</text>
|
|
452
|
+
<text fg={skin.muted}>
|
|
453
|
+
first: {props.meta.state === "first" ? "yes" : "no"} · updated:{" "}
|
|
454
|
+
{props.meta.state === "updated" ? "yes" : "no"} · loads: {props.meta.entry.load_count}
|
|
455
|
+
</text>
|
|
456
|
+
<text fg={skin.muted}>plugin source: {props.meta.entry.source}</text>
|
|
457
|
+
<text fg={skin.muted}>source: {value.source}</text>
|
|
458
|
+
<text fg={skin.muted}>note: {value.note || "(none)"}</text>
|
|
459
|
+
<text fg={skin.muted}>selected: {value.selected || "(none)"}</text>
|
|
460
|
+
<text fg={skin.muted}>local stack depth: {value.local}</text>
|
|
461
|
+
<text fg={skin.muted}>host stack open: {props.api.ui.dialog.open ? "yes" : "no"}</text>
|
|
462
|
+
</box>
|
|
463
|
+
) : null}
|
|
464
|
+
|
|
465
|
+
{value.tab === 1 ? (
|
|
466
|
+
<box flexDirection="column" gap={1}>
|
|
467
|
+
<text fg={skin.text}>Counter: {value.count}</text>
|
|
468
|
+
<text fg={skin.muted}>
|
|
469
|
+
{props.keys.print("up")} / {props.keys.print("down")} change value
|
|
470
|
+
</text>
|
|
471
|
+
</box>
|
|
472
|
+
) : null}
|
|
473
|
+
|
|
474
|
+
{value.tab === 2 ? (
|
|
475
|
+
<box flexDirection="column" gap={1}>
|
|
476
|
+
<text fg={skin.muted}>
|
|
477
|
+
{props.keys.print("modal")} modal | {props.keys.print("alert")} alert | {props.keys.print("confirm")}{" "}
|
|
478
|
+
confirm | {props.keys.print("prompt")} prompt | {props.keys.print("select")} select
|
|
479
|
+
</text>
|
|
480
|
+
<text fg={skin.muted}>
|
|
481
|
+
{props.keys.print("local")} local stack | {props.keys.print("host")} host stack
|
|
482
|
+
</text>
|
|
483
|
+
<text fg={skin.muted}>
|
|
484
|
+
local open: {props.keys.print("local_push")} push nested · esc or {props.keys.print("local_close")}{" "}
|
|
485
|
+
close
|
|
486
|
+
</text>
|
|
487
|
+
<text fg={skin.muted}>{props.keys.print("home")} returns home</text>
|
|
488
|
+
</box>
|
|
489
|
+
) : null}
|
|
490
|
+
</box>
|
|
491
|
+
|
|
492
|
+
<box flexDirection="row" gap={1} paddingTop={1}>
|
|
493
|
+
<Btn txt="go home" run={() => props.api.route.navigate("home")} skin={skin} />
|
|
494
|
+
<Btn txt="modal" run={() => props.api.route.navigate(props.route.modal, value)} skin={skin} on />
|
|
495
|
+
<Btn txt="local overlay" run={show} skin={skin} />
|
|
496
|
+
<Btn txt="host overlay" run={() => host(props.api, props.input, skin)} skin={skin} />
|
|
497
|
+
<Btn txt="alert" run={() => warn(props.api, props.route, value)} skin={skin} />
|
|
498
|
+
<Btn txt="confirm" run={() => check(props.api, props.route, value)} skin={skin} />
|
|
499
|
+
<Btn txt="prompt" run={() => entry(props.api, props.route, value)} skin={skin} />
|
|
500
|
+
<Btn txt="select" run={() => picker(props.api, props.route, value)} skin={skin} />
|
|
501
|
+
</box>
|
|
502
|
+
</box>
|
|
503
|
+
|
|
504
|
+
<box
|
|
505
|
+
visible={value.local > 0}
|
|
506
|
+
width={dim().width}
|
|
507
|
+
height={dim().height}
|
|
508
|
+
alignItems="center"
|
|
509
|
+
position="absolute"
|
|
510
|
+
zIndex={3000}
|
|
511
|
+
paddingTop={dim().height / 4}
|
|
512
|
+
left={0}
|
|
513
|
+
top={0}
|
|
514
|
+
backgroundColor={RGBA.fromInts(0, 0, 0, 160)}
|
|
515
|
+
onMouseUp={() => {
|
|
516
|
+
pop()
|
|
517
|
+
}}
|
|
518
|
+
>
|
|
519
|
+
<box
|
|
520
|
+
onMouseUp={(evt) => {
|
|
521
|
+
evt.stopPropagation()
|
|
522
|
+
}}
|
|
523
|
+
width={60}
|
|
524
|
+
maxWidth={dim().width - 2}
|
|
525
|
+
backgroundColor={skin.panel}
|
|
526
|
+
border
|
|
527
|
+
borderColor={skin.border}
|
|
528
|
+
paddingTop={1}
|
|
529
|
+
paddingBottom={1}
|
|
530
|
+
paddingLeft={2}
|
|
531
|
+
paddingRight={2}
|
|
532
|
+
gap={1}
|
|
533
|
+
flexDirection="column"
|
|
534
|
+
>
|
|
535
|
+
<text fg={skin.text}>
|
|
536
|
+
<b>{props.input.label} local overlay</b>
|
|
537
|
+
</text>
|
|
538
|
+
<text fg={skin.muted}>Plugin-owned stack depth: {value.local}</text>
|
|
539
|
+
<text fg={skin.muted}>
|
|
540
|
+
{props.keys.print("local_push")} push nested · {props.keys.print("local_close")} pop/close
|
|
541
|
+
</text>
|
|
542
|
+
<box flexDirection="row" gap={1}>
|
|
543
|
+
<Btn txt="push" run={push} skin={skin} on />
|
|
544
|
+
<Btn txt="pop" run={pop} skin={skin} />
|
|
545
|
+
</box>
|
|
546
|
+
</box>
|
|
547
|
+
</box>
|
|
548
|
+
</box>
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const Modal = (props: { api: TuiApi; input: Cfg; route: Route; keys: Keys; params?: Record<string, unknown> }) => {
|
|
553
|
+
const Dialog = props.api.ui.Dialog
|
|
554
|
+
const value = parse(props.params)
|
|
555
|
+
const skin = tone(props.api)
|
|
556
|
+
|
|
557
|
+
useKeyboard((evt) => {
|
|
558
|
+
if (props.api.route.current.name !== props.route.modal) return
|
|
559
|
+
|
|
560
|
+
if (props.keys.match("modal_accept", evt)) {
|
|
561
|
+
evt.preventDefault()
|
|
562
|
+
evt.stopPropagation()
|
|
563
|
+
props.api.route.navigate(props.route.screen, { ...value, source: "modal" })
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (props.keys.match("modal_close", evt)) {
|
|
568
|
+
evt.preventDefault()
|
|
569
|
+
evt.stopPropagation()
|
|
570
|
+
props.api.route.navigate("home")
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
return (
|
|
575
|
+
<box width="100%" height="100%" backgroundColor={skin.panel}>
|
|
576
|
+
<Dialog onClose={() => props.api.route.navigate("home")}>
|
|
577
|
+
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
|
|
578
|
+
<text fg={skin.text}>
|
|
579
|
+
<b>{props.input.label} modal</b>
|
|
580
|
+
</text>
|
|
581
|
+
<text fg={skin.muted}>{props.keys.print("modal")} modal command</text>
|
|
582
|
+
<text fg={skin.muted}>{props.keys.print("screen")} screen command</text>
|
|
583
|
+
<text fg={skin.muted}>
|
|
584
|
+
{props.keys.print("modal_accept")} opens screen · {props.keys.print("modal_close")} closes
|
|
585
|
+
</text>
|
|
586
|
+
<box flexDirection="row" gap={1}>
|
|
587
|
+
<Btn
|
|
588
|
+
txt="open screen"
|
|
589
|
+
run={() => props.api.route.navigate(props.route.screen, { ...value, source: "modal" })}
|
|
590
|
+
skin={skin}
|
|
591
|
+
on
|
|
592
|
+
/>
|
|
593
|
+
<Btn txt="cancel" run={() => props.api.route.navigate("home")} skin={skin} />
|
|
594
|
+
</box>
|
|
595
|
+
</box>
|
|
596
|
+
</Dialog>
|
|
597
|
+
</box>
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const slot = (input: Cfg) => ({
|
|
602
|
+
id: "workspace-smoke",
|
|
603
|
+
slots: {
|
|
604
|
+
home_logo(ctx) {
|
|
605
|
+
const map = ctx.theme.current as Record<string, unknown>
|
|
606
|
+
const get = (name: string, fallback: string) => {
|
|
607
|
+
const value = map[name]
|
|
608
|
+
if (typeof value === "string") return value
|
|
609
|
+
if (value && typeof value === "object") return value as RGBA
|
|
610
|
+
return fallback
|
|
611
|
+
}
|
|
612
|
+
const art = [
|
|
613
|
+
" $$\\",
|
|
614
|
+
" $$ |",
|
|
615
|
+
" $$$$$$$\\ $$$$$$\\$$$$\\ $$$$$$\\ $$ | $$\\ $$$$$$\\",
|
|
616
|
+
"$$ _____|$$ _$$ _$$\\ $$ __$$\\ $$ | $$ |$$ __$$\\",
|
|
617
|
+
"\\$$$$$$\\ $$ / $$ / $$ |$$ / $$ |$$$$$$ / $$$$$$$$ |",
|
|
618
|
+
" \\____$$\\ $$ | $$ | $$ |$$ | $$ |$$ _$$< $$ ____|",
|
|
619
|
+
"$$$$$$$ |$$ | $$ | $$ |\\$$$$$$ |$$ | \\$$\\ \\$$$$$$$\\",
|
|
620
|
+
"\\_______/ \\__| \\__| \\__| \\______/ \\__| \\__| \\_______|",
|
|
621
|
+
]
|
|
622
|
+
const ink = [
|
|
623
|
+
get("primary", ui.accent),
|
|
624
|
+
get("textMuted", ui.muted),
|
|
625
|
+
get("info", ui.accent),
|
|
626
|
+
get("text", ui.text),
|
|
627
|
+
get("success", ui.accent),
|
|
628
|
+
get("warning", ui.accent),
|
|
629
|
+
get("secondary", ui.accent),
|
|
630
|
+
get("error", ui.accent),
|
|
631
|
+
]
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<box flexDirection="column">
|
|
635
|
+
{art.map((line, i) => (
|
|
636
|
+
<text fg={ink[i]}>{line}</text>
|
|
637
|
+
))}
|
|
638
|
+
</box>
|
|
639
|
+
)
|
|
640
|
+
},
|
|
641
|
+
sidebar_top(ctx, value) {
|
|
642
|
+
const map = ctx.theme.current as Record<string, unknown>
|
|
643
|
+
const get = (name: string, fallback: string) => {
|
|
644
|
+
const item = map[name]
|
|
645
|
+
if (typeof item === "string") return item
|
|
646
|
+
if (item && typeof item === "object") return item as RGBA
|
|
647
|
+
return fallback
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return (
|
|
651
|
+
<box
|
|
652
|
+
border
|
|
653
|
+
borderColor={get("border", ui.border)}
|
|
654
|
+
backgroundColor={get("backgroundPanel", ui.panel)}
|
|
655
|
+
paddingTop={1}
|
|
656
|
+
paddingBottom={1}
|
|
657
|
+
paddingLeft={2}
|
|
658
|
+
paddingRight={2}
|
|
659
|
+
flexDirection="column"
|
|
660
|
+
gap={1}
|
|
661
|
+
>
|
|
662
|
+
<text fg={get("primary", ui.accent)}>
|
|
663
|
+
<b>{input.label}</b>
|
|
664
|
+
</text>
|
|
665
|
+
<text fg={get("text", ui.text)}>sidebar slot active</text>
|
|
666
|
+
<text fg={get("textMuted", ui.muted)}>session {value.session_id.slice(0, 8)}</text>
|
|
667
|
+
</box>
|
|
668
|
+
)
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
const reg = (api: TuiApi, input: Cfg, keys: Keys) => {
|
|
674
|
+
const route = names(input)
|
|
675
|
+
api.command.register(() => [
|
|
676
|
+
{
|
|
677
|
+
title: `${input.label} modal`,
|
|
678
|
+
value: "plugin.smoke.modal",
|
|
679
|
+
keybind: keys.get("modal"),
|
|
680
|
+
category: "Plugin",
|
|
681
|
+
slash: {
|
|
682
|
+
name: "smoke",
|
|
683
|
+
},
|
|
684
|
+
onSelect: () => {
|
|
685
|
+
api.route.navigate(route.modal, { source: "command" })
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
title: `${input.label} screen`,
|
|
690
|
+
value: "plugin.smoke.screen",
|
|
691
|
+
keybind: keys.get("screen"),
|
|
692
|
+
category: "Plugin",
|
|
693
|
+
slash: {
|
|
694
|
+
name: "smoke-screen",
|
|
695
|
+
},
|
|
696
|
+
onSelect: () => {
|
|
697
|
+
api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 })
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
title: `${input.label} alert dialog`,
|
|
702
|
+
value: "plugin.smoke.alert",
|
|
703
|
+
category: "Plugin",
|
|
704
|
+
slash: {
|
|
705
|
+
name: "smoke-alert",
|
|
706
|
+
},
|
|
707
|
+
onSelect: () => {
|
|
708
|
+
warn(api, route, current(api, route))
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
title: `${input.label} confirm dialog`,
|
|
713
|
+
value: "plugin.smoke.confirm",
|
|
714
|
+
category: "Plugin",
|
|
715
|
+
slash: {
|
|
716
|
+
name: "smoke-confirm",
|
|
717
|
+
},
|
|
718
|
+
onSelect: () => {
|
|
719
|
+
check(api, route, current(api, route))
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
title: `${input.label} prompt dialog`,
|
|
724
|
+
value: "plugin.smoke.prompt",
|
|
725
|
+
category: "Plugin",
|
|
726
|
+
slash: {
|
|
727
|
+
name: "smoke-prompt",
|
|
728
|
+
},
|
|
729
|
+
onSelect: () => {
|
|
730
|
+
entry(api, route, current(api, route))
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
title: `${input.label} select dialog`,
|
|
735
|
+
value: "plugin.smoke.select",
|
|
736
|
+
category: "Plugin",
|
|
737
|
+
slash: {
|
|
738
|
+
name: "smoke-select",
|
|
739
|
+
},
|
|
740
|
+
onSelect: () => {
|
|
741
|
+
picker(api, route, current(api, route))
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
title: `${input.label} host overlay`,
|
|
746
|
+
value: "plugin.smoke.host",
|
|
747
|
+
keybind: keys.get("host"),
|
|
748
|
+
category: "Plugin",
|
|
749
|
+
slash: {
|
|
750
|
+
name: "smoke-host",
|
|
751
|
+
},
|
|
752
|
+
onSelect: () => {
|
|
753
|
+
host(api, input, tone(api))
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
title: `${input.label} go home`,
|
|
758
|
+
value: "plugin.smoke.home",
|
|
759
|
+
category: "Plugin",
|
|
760
|
+
enabled: api.route.current.name !== "home",
|
|
761
|
+
onSelect: () => {
|
|
762
|
+
api.route.navigate("home")
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
title: `${input.label} toast`,
|
|
767
|
+
value: "plugin.smoke.toast",
|
|
768
|
+
category: "Plugin",
|
|
769
|
+
onSelect: () => {
|
|
770
|
+
api.ui.toast({
|
|
771
|
+
variant: "info",
|
|
772
|
+
title: "Smoke",
|
|
773
|
+
message: "Plugin toast works",
|
|
774
|
+
duration: 2000,
|
|
775
|
+
})
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
])
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const tui = async (input: TuiPluginInput, options: Record<string, unknown> | null, meta: TuiPluginInit) => {
|
|
782
|
+
if (options?.enabled === false) return
|
|
783
|
+
|
|
784
|
+
await input.api.theme.install("./smoke-theme.json")
|
|
785
|
+
input.api.theme.set("smoke-theme")
|
|
786
|
+
|
|
787
|
+
const value = cfg(options ?? undefined)
|
|
788
|
+
const route = names(value)
|
|
789
|
+
const keys = input.api.keybind.create(bind, value.keybinds)
|
|
790
|
+
const fx = new VignetteEffect(value.vignette)
|
|
791
|
+
input.renderer.addPostProcessFn(fx.apply.bind(fx))
|
|
792
|
+
|
|
793
|
+
input.api.route.register([
|
|
794
|
+
{
|
|
795
|
+
name: route.screen,
|
|
796
|
+
render: ({ params }) => (
|
|
797
|
+
<Screen api={input.api} input={value} route={route} keys={keys} meta={meta} params={params} />
|
|
798
|
+
),
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: route.modal,
|
|
802
|
+
render: ({ params }) => <Modal api={input.api} input={value} route={route} keys={keys} params={params} />,
|
|
803
|
+
},
|
|
804
|
+
])
|
|
805
|
+
|
|
806
|
+
reg(input.api, value, keys)
|
|
807
|
+
input.slots.register(slot(value))
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export default {
|
|
811
|
+
tui,
|
|
812
|
+
}
|