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/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
+ }