@wizardingcode/shibaox-tui 0.1.3 → 0.1.5

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.
Files changed (65) hide show
  1. package/LICENSE +216 -0
  2. package/dist/main.js +8188 -0
  3. package/dist/main.js.map +67 -0
  4. package/package.json +9 -12
  5. package/src/app.tsx +0 -300
  6. package/src/component/dialogs/confirm.tsx +0 -29
  7. package/src/component/dialogs/diff.tsx +0 -143
  8. package/src/component/dialogs/help.tsx +0 -99
  9. package/src/component/dialogs/keys.tsx +0 -201
  10. package/src/component/dialogs/palette.tsx +0 -95
  11. package/src/component/dialogs/runs.tsx +0 -115
  12. package/src/component/dialogs/tiers.tsx +0 -240
  13. package/src/component/footer.tsx +0 -66
  14. package/src/component/logo.tsx +0 -29
  15. package/src/component/prompt/autocomplete.tsx +0 -44
  16. package/src/component/prompt/index.tsx +0 -213
  17. package/src/component/reconnecting.tsx +0 -44
  18. package/src/component/sidebar.tsx +0 -235
  19. package/src/component/tabs.tsx +0 -116
  20. package/src/component/too-small.tsx +0 -15
  21. package/src/context/client.tsx +0 -54
  22. package/src/context/commands.tsx +0 -44
  23. package/src/context/config.tsx +0 -22
  24. package/src/context/data-state.ts +0 -41
  25. package/src/context/data.tsx +0 -308
  26. package/src/context/exit.tsx +0 -13
  27. package/src/context/keys.tsx +0 -70
  28. package/src/context/poller.ts +0 -352
  29. package/src/context/prefs.tsx +0 -54
  30. package/src/context/route.tsx +0 -23
  31. package/src/main.tsx +0 -55
  32. package/src/model/format.ts +0 -38
  33. package/src/model/prompt-commands.ts +0 -315
  34. package/src/model/status.ts +0 -55
  35. package/src/model/stream.ts +0 -464
  36. package/src/motion/animation.ts +0 -235
  37. package/src/motion/config.tsx +0 -15
  38. package/src/motion/fade-in-text.tsx +0 -93
  39. package/src/motion/masked-text.ts +0 -62
  40. package/src/motion/one-cell-motion.ts +0 -100
  41. package/src/motion/pulse.ts +0 -38
  42. package/src/motion/shimmer-text.tsx +0 -78
  43. package/src/motion/spinner-renderable.ts +0 -171
  44. package/src/motion/spinner.tsx +0 -100
  45. package/src/motion/subcell.ts +0 -40
  46. package/src/routes/home.tsx +0 -365
  47. package/src/routes/session/approval-bar.tsx +0 -136
  48. package/src/routes/session/cards.tsx +0 -495
  49. package/src/routes/session/context-line.tsx +0 -129
  50. package/src/routes/session/continue.tsx +0 -104
  51. package/src/routes/session/index.tsx +0 -340
  52. package/src/routes/session/request.tsx +0 -87
  53. package/src/routes/session/timeline.tsx +0 -313
  54. package/src/testing/fake-client.ts +0 -249
  55. package/src/theme/context.tsx +0 -44
  56. package/src/theme/resolve.ts +0 -196
  57. package/src/theme/shibaox.json +0 -159
  58. package/src/ui/border.ts +0 -20
  59. package/src/ui/delayed-presence.ts +0 -23
  60. package/src/ui/dialog.tsx +0 -123
  61. package/src/ui/layout.ts +0 -23
  62. package/src/ui/marquee.ts +0 -75
  63. package/src/ui/pane-resize.ts +0 -77
  64. package/src/ui/toast.tsx +0 -203
  65. package/tsconfig.json +0 -19
@@ -1,93 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { type OptimizedBuffer, type RenderContext, RGBA, type TextOptions } from '@opentui/core';
3
- import { extend, type JSX } from '@opentui/solid';
4
- import { splitProps } from 'solid-js';
5
- import { useMotion } from './config.js';
6
- import { MaskedTextRenderable } from './masked-text.js';
7
- import { coast, smootherstep } from './pulse.js';
8
-
9
- type FadeInTextOptions = TextOptions & { backdrop?: RGBA; enabled?: boolean };
10
-
11
- const DURATION = 200;
12
- const FEATHER = 8;
13
- const clamp = (value: number) => Math.max(0, Math.min(1, value));
14
-
15
- class FadeInTextRenderable extends MaskedTextRenderable {
16
- private _backdrop = RGBA.defaultBackground();
17
- private _enabled = true;
18
- private elapsed = 0;
19
-
20
- constructor(ctx: RenderContext, options: FadeInTextOptions) {
21
- super(ctx, options);
22
- this.matrix[15] = 1;
23
- this.updateBackdrop();
24
- if (options.backdrop) this.backdrop = options.backdrop;
25
- if (options.enabled === false) this.enabled = false;
26
- this.live = this._enabled;
27
- }
28
-
29
- set backdrop(value: RGBA) {
30
- if (value.equals(this._backdrop)) return;
31
- this._backdrop = value;
32
- this.updateBackdrop();
33
- this.requestRender();
34
- }
35
-
36
- set enabled(value: boolean) {
37
- if (value === this._enabled) return;
38
- this._enabled = value;
39
- this.live = value && this.elapsed < DURATION;
40
- this.requestRender();
41
- }
42
-
43
- private updateBackdrop() {
44
- this.matrix[3] = this._backdrop.r;
45
- this.matrix[7] = this._backdrop.g;
46
- this.matrix[11] = this._backdrop.b;
47
- }
48
-
49
- override render(buffer: OptimizedBuffer, deltaTime: number): void {
50
- if (!this._enabled || this.elapsed >= DURATION) {
51
- super.render(buffer, deltaTime);
52
- return;
53
- }
54
- if (
55
- !this.visible ||
56
- this.isDestroyed ||
57
- !Number.isFinite(this.width) ||
58
- this.width <= 0 ||
59
- this.height <= 0
60
- )
61
- return;
62
- this.elapsed = Math.min(DURATION, this.elapsed + deltaTime);
63
- this.renderMasked(buffer, 1, (end) => {
64
- const progress = this.elapsed / DURATION;
65
- const front = -FEATHER + coast(progress) * (end + FEATHER * 2);
66
- return (column) => 1 - smootherstep(clamp((front - column) / FEATHER));
67
- });
68
- if (this.elapsed >= DURATION) this.live = false;
69
- }
70
- }
71
-
72
- extend({ fade_in_text: FadeInTextRenderable });
73
-
74
- declare module '@opentui/solid' {
75
- interface OpenTUIComponents {
76
- fade_in_text: typeof FadeInTextRenderable;
77
- }
78
- }
79
-
80
- type Props = Omit<JSX.IntrinsicElements['text'], 'ref'> & { animate?: boolean; backdrop?: RGBA };
81
-
82
- /** Text that sweeps in from the left over 200 ms when it first appears. */
83
- export function FadeInText(props: Props): JSX.Element {
84
- const motion = useMotion();
85
- const [local, text] = splitProps(props, ['animate', 'backdrop']);
86
- return (
87
- <fade_in_text
88
- {...text}
89
- backdrop={local.backdrop}
90
- enabled={(local.animate ?? true) && motion()}
91
- />
92
- );
93
- }
@@ -1,62 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { OptimizedBuffer, RGBA, TargetChannel, TextRenderable } from '@opentui/core';
3
-
4
- const TRANSPARENT = RGBA.fromValues(0, 0, 0, 0);
5
- const CONTINUATION = 0xc0000000 | 0;
6
-
7
- /** A text renderable that paints itself through a per-column intensity mask (shimmer, fade-in). */
8
- export class MaskedTextRenderable extends TextRenderable {
9
- protected readonly matrix = new Float32Array(16);
10
- private scratch: OptimizedBuffer | undefined;
11
- private mask = new Float32Array(0);
12
-
13
- protected renderMasked(
14
- buffer: OptimizedBuffer,
15
- initialStrength: number,
16
- shade: (width: number) => (column: number) => number,
17
- ): void {
18
- if (!this.scratch)
19
- this.scratch = OptimizedBuffer.create(this.width, this.height, this._ctx.widthMethod, {
20
- respectAlpha: true,
21
- });
22
- if (this.scratch.width !== this.width || this.scratch.height !== this.height)
23
- this.scratch.resize(this.width, this.height);
24
-
25
- this.scratch.clear(TRANSPARENT);
26
- this.scratch.drawTextBuffer(this.textBufferView, 0, 0);
27
- const characters = this.scratch.buffers.char;
28
- let end = 0;
29
- for (let row = 0; row < this.height; row++) {
30
- let column = this.width;
31
- while (
32
- column > 0 &&
33
- (characters[row * this.width + column - 1] === 32 ||
34
- characters[row * this.width + column - 1] === 0)
35
- )
36
- column--;
37
- end = Math.max(end, column);
38
- }
39
- const intensity = shade(end);
40
- if (this.mask.length !== this.width * this.height * 3)
41
- this.mask = new Float32Array(this.width * this.height * 3);
42
- let strength = initialStrength;
43
- for (let cell = 0; cell < characters.length; cell++) {
44
- const column = cell % this.width;
45
- // Wide glyph continuation cells retain the head cell's intensity.
46
- if (((characters[cell] ?? 0) & CONTINUATION) !== CONTINUATION) strength = intensity(column);
47
- this.mask[cell * 3] = column;
48
- this.mask[cell * 3 + 1] = Math.floor(cell / this.width);
49
- this.mask[cell * 3 + 2] = strength;
50
- }
51
- this.scratch.colorMatrix(this.matrix, this.mask, 1, TargetChannel.FG);
52
- buffer.drawFrameBuffer(this.screenX, this.screenY, this.scratch);
53
- this.markClean();
54
- this._ctx.addToHitGrid(this.screenX, this.screenY, this.width, this.height, this.num);
55
- }
56
-
57
- override destroy(): void {
58
- this.scratch?.destroy();
59
- this.scratch = undefined;
60
- super.destroy();
61
- }
62
- }
@@ -1,100 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { octantGlyph } from './subcell.js';
3
-
4
- export interface OneCellMotion {
5
- frames: string[];
6
- interval: number;
7
- levels?: number[];
8
- intro?: Pick<OneCellMotion, 'frames' | 'interval' | 'levels'>;
9
- once?: boolean;
10
- pace?: { initial: number; final: number; after: number; duration: number };
11
- }
12
-
13
- export function oneCellFrame(
14
- animation: OneCellMotion,
15
- elapsed: number,
16
- ): { glyph: string; level: number; complete: boolean } {
17
- const introDuration = animation.intro
18
- ? animation.intro.frames.length * animation.intro.interval
19
- : 0;
20
- const intro = elapsed < introDuration;
21
- const clip = intro && animation.intro ? animation.intro : animation;
22
- const pace = animation.pace;
23
- const progress = pace ? Math.max(0, Math.min(1, (elapsed - pace.after) / pace.duration)) : 0;
24
- // Integrate the changing rate so slowing down does not jump or restart the motion.
25
- const time = intro
26
- ? elapsed
27
- : !pace
28
- ? elapsed - introDuration
29
- : (elapsed - introDuration) * pace.initial +
30
- (pace.final - pace.initial) *
31
- (pace.duration * (progress ** 3 - progress ** 4 / 2) +
32
- Math.max(0, elapsed - pace.after - pace.duration));
33
- const frame = Math.floor((time * clip.frames.length) / (clip.interval * clip.frames.length));
34
- const index =
35
- !intro && animation.once ? Math.min(frame, clip.frames.length - 1) : frame % clip.frames.length;
36
- return {
37
- glyph: clip.frames[index] ?? ' ',
38
- level: clip.levels?.[index] ?? 1,
39
- complete: !intro && !!animation.once && frame >= clip.frames.length - 1,
40
- };
41
- }
42
-
43
- export const SEED_WORK: OneCellMotion = {
44
- frames: Array.from({ length: 40 }, () => '▪'),
45
- interval: 40,
46
- levels: Array.from(
47
- { length: 40 },
48
- (_, index) => 0.3 + (0.7 * (1 - Math.cos((index / 40) * 2 * Math.PI))) / 2,
49
- ),
50
- intro: {
51
- frames: Array.from({ length: 80 }, () => '▪'),
52
- interval: 40,
53
- levels: Array.from({ length: 80 }, (_, index) => {
54
- const phase = index / 80;
55
- return (
56
- 0.3 + 0.7 * (phase < 0.125 ? phase / 0.125 : phase < 0.375 ? 1 : ((1 - phase) / 0.625) ** 3)
57
- );
58
- }),
59
- },
60
- pace: { initial: 1.25, final: 0.5, after: 30_000, duration: 30_000 },
61
- };
62
-
63
- const WORK_PACE = { initial: 1.2, final: 0.96, after: 30_000, duration: 30_000 };
64
- const lower = [2, 3, 5, 7, 6, 4];
65
-
66
- export const BLOCK_LOW_COMET: OneCellMotion = {
67
- frames: lower.flatMap((point, index, path) => {
68
- const leading = (1 << point) | (1 << (path[(index + 5) % 6] ?? 0));
69
- const full = leading | (1 << (path[(index + 4) % 6] ?? 0));
70
- return [full, full, full, leading, leading].map(octantGlyph);
71
- }),
72
- interval: 40,
73
- pace: WORK_PACE,
74
- };
75
-
76
- export const BLOCK_SOFT_SWEEP: OneCellMotion = {
77
- frames: [0x14, 0x14, 0x14, 0x1c, 0x3c, 0x38, 0x28, 0x28, 0x28, 0x38, 0x3c, 0x1c].map(octantGlyph),
78
- interval: 100,
79
- pace: WORK_PACE,
80
- };
81
-
82
- export const BLOCK_SOFT_SLIDE: OneCellMotion = {
83
- frames: [0x14, 0x14, 0x14, 0x14, 0x14, 0x28, 0x28, 0x28, 0x28, 0x28].map(octantGlyph),
84
- interval: 100,
85
- levels: [0.55, 0.85, 1, 0.85, 0.55, 0.55, 0.85, 1, 0.85, 0.55],
86
- };
87
-
88
- export const WORK_SPINNERS = {
89
- 'block-soft-sweep': BLOCK_SOFT_SWEEP,
90
- 'block-soft-slide': BLOCK_SOFT_SLIDE,
91
- 'block-low-comet': BLOCK_LOW_COMET,
92
- seed: SEED_WORK,
93
- } satisfies Record<string, OneCellMotion>;
94
-
95
- export const SEED_LAUNCH: OneCellMotion = {
96
- frames: Array.from({ length: 21 }, (_, index) => (index < 10 ? '▫' : '▪')),
97
- interval: 40,
98
- levels: (SEED_WORK.levels ?? []).slice(0, 21),
99
- once: true,
100
- };
@@ -1,38 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- // The pulse math of opencode's tab-pulse: shaped sweeps and attack/decay envelopes.
3
- const clamp = (value: number) => Math.max(0, Math.min(1, value));
4
-
5
- export const smootherstep = (value: number): number =>
6
- value * value * value * (value * (value * 6 - 15) + 10);
7
-
8
- /** Intensity of a sweep with a soft head ahead of `front` and a long tail behind it. */
9
- export const intensityAt = (index: number, front: number, head: number, tail: number): number => {
10
- const distance = front - index;
11
- return distance < 0
12
- ? smootherstep(clamp(1 + distance / head))
13
- : smootherstep(clamp(1 - distance / tail));
14
- };
15
-
16
- /** Eases the ends of a 0..1 progress so a sweep starts and stops gently. */
17
- export const coast = (value: number): number => {
18
- const ramp = 0.2;
19
- if (value < ramp) return (value * value) / (2 * ramp * (1 - ramp));
20
- if (value > 1 - ramp) return 1 - ((1 - value) * (1 - value)) / (2 * ramp * (1 - ramp));
21
- return (value - ramp / 2) / (1 - ramp);
22
- };
23
-
24
- /** Rise to peak over the attack fraction, then settle to rest over the remainder. */
25
- export const attackDecay = (
26
- progress: number,
27
- attack: number,
28
- peak: number,
29
- rest: number,
30
- ): number =>
31
- progress < attack
32
- ? peak * smootherstep(clamp(progress / attack))
33
- : peak - (peak - rest) * smootherstep(clamp((progress - attack) / (1 - attack)));
34
-
35
- const COMPLETION_ATTACK = 0.12;
36
- /** Opacity of the completion pulse over a 0..1 progress. */
37
- export const completionPulseOpacity = (progress: number): number =>
38
- attackDecay(progress, COMPLETION_ATTACK, 1, 0);
@@ -1,78 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { type OptimizedBuffer, type RenderContext, RGBA, type TextOptions } from '@opentui/core';
3
- import { extend, type JSX } from '@opentui/solid';
4
- import { splitProps } from 'solid-js';
5
- import { useMotion } from './config.js';
6
- import { MaskedTextRenderable } from './masked-text.js';
7
- import { coast, intensityAt } from './pulse.js';
8
-
9
- type ShimmerTextOptions = TextOptions & { shimmer: RGBA; enabled?: boolean };
10
-
11
- const DURATION = 1200;
12
-
13
- class ShimmerTextRenderable extends MaskedTextRenderable {
14
- private _shimmer = RGBA.defaultForeground();
15
- private _enabled = true;
16
- private elapsed = 0;
17
-
18
- constructor(ctx: RenderContext, options: ShimmerTextOptions) {
19
- super(ctx, options);
20
- this.matrix[15] = 1;
21
- if (options.shimmer) this.shimmer = options.shimmer;
22
- if (options.enabled === false) this.enabled = false;
23
- this.live = this._enabled;
24
- }
25
-
26
- set shimmer(value: RGBA) {
27
- if (value.equals(this._shimmer)) return;
28
- this._shimmer = value;
29
- this.matrix[3] = value.r;
30
- this.matrix[7] = value.g;
31
- this.matrix[11] = value.b;
32
- this.requestRender();
33
- }
34
-
35
- set enabled(value: boolean) {
36
- if (value === this._enabled) return;
37
- this._enabled = value;
38
- this.live = value;
39
- this.requestRender();
40
- }
41
-
42
- override render(buffer: OptimizedBuffer, deltaTime: number): void {
43
- if (!this._enabled) {
44
- super.render(buffer, deltaTime);
45
- return;
46
- }
47
- if (
48
- !this.visible ||
49
- this.isDestroyed ||
50
- !Number.isFinite(this.width) ||
51
- this.width <= 0 ||
52
- this.height <= 0
53
- )
54
- return;
55
- this.elapsed = (this.elapsed + deltaTime) % DURATION;
56
- this.renderMasked(buffer, 0, (end) => {
57
- const front = -4 + coast(this.elapsed / DURATION) * (end + 22);
58
- return (column) => intensityAt(column, front, 4, 18);
59
- });
60
- }
61
- }
62
-
63
- extend({ shimmer_text: ShimmerTextRenderable });
64
-
65
- declare module '@opentui/solid' {
66
- interface OpenTUIComponents {
67
- shimmer_text: typeof ShimmerTextRenderable;
68
- }
69
- }
70
-
71
- type Props = Omit<JSX.IntrinsicElements['text'], 'ref'> & { shimmer: RGBA };
72
-
73
- /** Text with a light sweep travelling across it (the "working" affordance). */
74
- export function ShimmerText(props: Props): JSX.Element {
75
- const motion = useMotion();
76
- const [local, text] = splitProps(props, ['shimmer']);
77
- return <shimmer_text {...text} shimmer={local.shimmer} enabled={motion()} />;
78
- }
@@ -1,171 +0,0 @@
1
- // A small port of opentui-spinner (MIT, https://github.com/…/opentui-spinner): the package
2
- // pins its OpenTUI peers to 0.3, which an npm install resolves to a second OpenTUI copy
3
- // next to ours and the dashboard refuses to start. What we use fits in this file.
4
- import {
5
- type ColorInput,
6
- type OptimizedBuffer,
7
- parseColor,
8
- Renderable,
9
- type RenderableOptions,
10
- type RenderContext,
11
- type RGBA,
12
- resolveRenderLib,
13
- } from '@opentui/core';
14
- import { extend } from '@opentui/solid/components';
15
-
16
- export interface SpinnerOptions extends RenderableOptions<SpinnerRenderable> {
17
- frames?: string[];
18
- /** Milliseconds per frame (16–1000). */
19
- interval?: number;
20
- autoplay?: boolean;
21
- color?: ColorInput;
22
- backgroundColor?: ColorInput;
23
- }
24
-
25
- const DEFAULT_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
26
- const clampInterval = (ms: number) => Math.min(1000, Math.max(1000 / 60, ms));
27
- type Encoded = ReturnType<ReturnType<typeof resolveRenderLib>['encodeUnicode']>;
28
-
29
- /** One cell (or a few) that cycles through `frames` every `interval` ms while visible. */
30
- export class SpinnerRenderable extends Renderable {
31
- private _frames: string[];
32
- private _interval: number;
33
- private _running = false;
34
- private _index = 0;
35
- private _timer: ReturnType<typeof setInterval> | undefined;
36
- private _color: RGBA;
37
- private _background: RGBA;
38
- private readonly _lib = resolveRenderLib();
39
- private _encoded = new Map<string, NonNullable<Encoded>>();
40
-
41
- constructor(ctx: RenderContext, options: SpinnerOptions) {
42
- super(ctx, options);
43
- this._frames = options.frames?.length ? options.frames : DEFAULT_FRAMES;
44
- this._interval = clampInterval(options.interval ?? 80);
45
- this._color = parseColor(options.color ?? 'white');
46
- this._background = parseColor(options.backgroundColor ?? 'transparent');
47
- this.height = 1;
48
- this.encode();
49
- if (options.autoplay ?? true) this.start();
50
- }
51
-
52
- private encode(): void {
53
- let width = 0;
54
- for (const f of this._frames) {
55
- const e = this._encoded.get(f) ?? this._lib.encodeUnicode(f, this.ctx.widthMethod);
56
- if (!e) continue;
57
- this._encoded.set(f, e);
58
- width = Math.max(
59
- width,
60
- e.data.reduce((n: number, c: { width: number }) => n + c.width, 0),
61
- );
62
- }
63
- this.width = width;
64
- }
65
- private free(): void {
66
- for (const e of this._encoded.values()) this._lib.freeUnicode(e);
67
- this._encoded.clear();
68
- }
69
- private arm(): void {
70
- this.disarm();
71
- if (!this._running || !this.visible) return;
72
- this._timer = setInterval(() => {
73
- if (this.isDestroyed) return this.disarm();
74
- this._index = (this._index + 1) % this._frames.length;
75
- this.requestRender();
76
- }, this._interval);
77
- this._timer.unref?.();
78
- }
79
- private disarm(): void {
80
- if (this._timer !== undefined) clearInterval(this._timer);
81
- this._timer = undefined;
82
- }
83
-
84
- get frames(): string[] {
85
- return this._frames;
86
- }
87
- set frames(value: string[]) {
88
- const next = value.length ? value : DEFAULT_FRAMES;
89
- if (next.length === this._frames.length && next.every((f, i) => f === this._frames[i])) return;
90
- this.free();
91
- this._frames = next;
92
- this._index = 0;
93
- this.encode();
94
- this.requestRender();
95
- }
96
- get interval(): number {
97
- return this._interval;
98
- }
99
- set interval(ms: number) {
100
- this._interval = clampInterval(ms);
101
- this.arm();
102
- }
103
- get color(): RGBA {
104
- return this._color;
105
- }
106
- set color(value: ColorInput) {
107
- this._color = parseColor(value);
108
- this.requestRender();
109
- }
110
- get backgroundColor(): RGBA {
111
- return this._background;
112
- }
113
- set backgroundColor(value: ColorInput) {
114
- this._background = parseColor(value);
115
- this.requestRender();
116
- }
117
- get autoplay(): boolean {
118
- return this._running;
119
- }
120
- set autoplay(value: boolean) {
121
- if (value) this.start();
122
- else this.stop();
123
- }
124
- override get visible(): boolean {
125
- return super.visible;
126
- }
127
- override set visible(value: boolean) {
128
- super.visible = value;
129
- this.arm();
130
- }
131
- start(): void {
132
- if (this._running || this.isDestroyed) return;
133
- this._running = true;
134
- this.arm();
135
- }
136
- stop(): void {
137
- this._running = false;
138
- this.disarm();
139
- }
140
-
141
- protected override renderSelf(buffer: OptimizedBuffer): void {
142
- if (!this.visible) return;
143
- const frame = this._frames[this._index];
144
- const e = frame ? this._encoded.get(frame) : undefined;
145
- if (!e) return;
146
- let x = this.x;
147
- for (const c of e.data) {
148
- buffer.drawChar(c.char, x, this.y, this._color, this._background);
149
- x += c.width;
150
- }
151
- }
152
- protected override destroySelf(): void {
153
- this.stop();
154
- this.free();
155
- super.destroySelf();
156
- }
157
- }
158
-
159
- declare module '@opentui/solid' {
160
- interface OpenTUIComponents {
161
- spinner: typeof SpinnerRenderable;
162
- }
163
- }
164
-
165
- let registered = false;
166
- /** Makes `<spinner>` available to the Solid renderer (once). */
167
- export function registerSpinner(): void {
168
- if (registered) return;
169
- registered = true;
170
- extend({ spinner: SpinnerRenderable });
171
- }
@@ -1,100 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { type ColorInput, parseColor, RGBA } from '@opentui/core';
3
- import type { JSX } from '@opentui/solid';
4
- import { createEffect, createMemo, createSignal, onCleanup, Show } from 'solid-js';
5
- import { useMotion } from './config.js';
6
- import { type OneCellMotion, oneCellFrame } from './one-cell-motion.js';
7
- import { registerSpinner } from './spinner-renderable.js';
8
-
9
- registerSpinner();
10
-
11
- export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
12
-
13
- /** A braille spinner with an optional label; without motion it is a still `⋯`. */
14
- export function Spinner(props: { children?: JSX.Element; color?: ColorInput }): JSX.Element {
15
- const motion = useMotion();
16
- const color = () => props.color ?? RGBA.defaultForeground();
17
- return (
18
- <Show
19
- when={motion()}
20
- fallback={
21
- <text fg={color()}>
22
- {props.children ? (
23
- <>
24
- {'⋯ '}
25
- {props.children}
26
- </>
27
- ) : (
28
- '⋯'
29
- )}
30
- </text>
31
- }
32
- >
33
- <box flexDirection="row" gap={1} flexShrink={props.children ? 1 : 0}>
34
- <box flexShrink={0}>
35
- <spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
36
- </box>
37
- <Show when={props.children}>
38
- <text fg={color()}>{props.children}</text>
39
- </Show>
40
- </box>
41
- </Show>
42
- );
43
- }
44
-
45
- /** One cell of motion (octant sweeps) for a working node; without motion a still glyph. */
46
- export function OneCellSpinner(props: {
47
- animation: OneCellMotion;
48
- color: ColorInput;
49
- paused?: boolean;
50
- still?: string;
51
- }): JSX.Element {
52
- const motion = useMotion();
53
- const [elapsed, setElapsed] = createSignal(0);
54
- const sequenced = () =>
55
- !!props.animation.intro || !!props.animation.once || !!props.animation.pace;
56
- const frame = createMemo(() => oneCellFrame(props.animation, elapsed()));
57
- const base = createMemo(() => parseColor(props.color));
58
- const color = createMemo(() => {
59
- if (sequenced()) {
60
- const c = RGBA.clone(base());
61
- c.a *= frame().level;
62
- return c;
63
- }
64
- return base();
65
- });
66
-
67
- createEffect(() => {
68
- props.animation;
69
- setElapsed(0);
70
- });
71
- createEffect(() => {
72
- if (!sequenced() || !motion() || props.paused || frame().complete) return;
73
- let previous = performance.now();
74
- const timer = setInterval(
75
- () => {
76
- const now = performance.now();
77
- setElapsed((value) => value + (now - previous));
78
- previous = now;
79
- },
80
- Math.max(
81
- 1000 / 60,
82
- Math.min(40, props.animation.interval / (props.animation.pace?.initial ?? 1)),
83
- ),
84
- );
85
- onCleanup(() => clearInterval(timer));
86
- });
87
-
88
- return (
89
- <box width={1} height={1} flexShrink={0}>
90
- <Show when={motion()} fallback={<text fg={base()}>{props.still ?? '▪'}</text>}>
91
- <spinner
92
- frames={sequenced() ? [frame().glyph] : props.animation.frames}
93
- interval={props.animation.interval}
94
- autoplay={!props.paused && !sequenced()}
95
- color={color()}
96
- />
97
- </Show>
98
- </box>
99
- );
100
- }
@@ -1,40 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- // Masks use row-major bits in a 2x4 grid. Unicode reuses these older glyphs
3
- // instead of duplicating them in the otherwise mask-ordered octant block.
4
- const OCTANTS = new Map<number, string>([
5
- [0x00, ' '],
6
- [0x01, '\u{1cea8}'],
7
- [0x02, '\u{1ceab}'],
8
- [0x03, '\u{1fb82}'],
9
- [0x05, '▘'],
10
- [0x0a, '▝'],
11
- [0x0f, '▀'],
12
- [0x14, '\u{1fbe6}'],
13
- [0x28, '\u{1fbe7}'],
14
- [0x3f, '\u{1fb85}'],
15
- [0x40, '\u{1cea3}'],
16
- [0x50, '▖'],
17
- [0x55, '▌'],
18
- [0x5a, '▞'],
19
- [0x5f, '▛'],
20
- [0x80, '\u{1cea0}'],
21
- [0xa0, '▗'],
22
- [0xa5, '▚'],
23
- [0xaa, '▐'],
24
- [0xaf, '▜'],
25
- [0xc0, '▂'],
26
- [0xf0, '▄'],
27
- [0xf5, '▙'],
28
- [0xfa, '▟'],
29
- [0xfc, '▆'],
30
- [0xff, '█'],
31
- ]);
32
-
33
- export function octantGlyph(mask: number): string {
34
- return (
35
- OCTANTS.get(mask) ??
36
- String.fromCodePoint(
37
- 0x1cd00 + mask - [...OCTANTS.keys()].filter((value) => value < mask).length,
38
- )
39
- );
40
- }