@wizardingcode/shibaox-tui 0.1.3 → 0.1.4

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 (64) hide show
  1. package/dist/main.js +8188 -0
  2. package/dist/main.js.map +67 -0
  3. package/package.json +8 -12
  4. package/src/app.tsx +0 -300
  5. package/src/component/dialogs/confirm.tsx +0 -29
  6. package/src/component/dialogs/diff.tsx +0 -143
  7. package/src/component/dialogs/help.tsx +0 -99
  8. package/src/component/dialogs/keys.tsx +0 -201
  9. package/src/component/dialogs/palette.tsx +0 -95
  10. package/src/component/dialogs/runs.tsx +0 -115
  11. package/src/component/dialogs/tiers.tsx +0 -240
  12. package/src/component/footer.tsx +0 -66
  13. package/src/component/logo.tsx +0 -29
  14. package/src/component/prompt/autocomplete.tsx +0 -44
  15. package/src/component/prompt/index.tsx +0 -213
  16. package/src/component/reconnecting.tsx +0 -44
  17. package/src/component/sidebar.tsx +0 -235
  18. package/src/component/tabs.tsx +0 -116
  19. package/src/component/too-small.tsx +0 -15
  20. package/src/context/client.tsx +0 -54
  21. package/src/context/commands.tsx +0 -44
  22. package/src/context/config.tsx +0 -22
  23. package/src/context/data-state.ts +0 -41
  24. package/src/context/data.tsx +0 -308
  25. package/src/context/exit.tsx +0 -13
  26. package/src/context/keys.tsx +0 -70
  27. package/src/context/poller.ts +0 -352
  28. package/src/context/prefs.tsx +0 -54
  29. package/src/context/route.tsx +0 -23
  30. package/src/main.tsx +0 -55
  31. package/src/model/format.ts +0 -38
  32. package/src/model/prompt-commands.ts +0 -315
  33. package/src/model/status.ts +0 -55
  34. package/src/model/stream.ts +0 -464
  35. package/src/motion/animation.ts +0 -235
  36. package/src/motion/config.tsx +0 -15
  37. package/src/motion/fade-in-text.tsx +0 -93
  38. package/src/motion/masked-text.ts +0 -62
  39. package/src/motion/one-cell-motion.ts +0 -100
  40. package/src/motion/pulse.ts +0 -38
  41. package/src/motion/shimmer-text.tsx +0 -78
  42. package/src/motion/spinner-renderable.ts +0 -171
  43. package/src/motion/spinner.tsx +0 -100
  44. package/src/motion/subcell.ts +0 -40
  45. package/src/routes/home.tsx +0 -365
  46. package/src/routes/session/approval-bar.tsx +0 -136
  47. package/src/routes/session/cards.tsx +0 -495
  48. package/src/routes/session/context-line.tsx +0 -129
  49. package/src/routes/session/continue.tsx +0 -104
  50. package/src/routes/session/index.tsx +0 -340
  51. package/src/routes/session/request.tsx +0 -87
  52. package/src/routes/session/timeline.tsx +0 -313
  53. package/src/testing/fake-client.ts +0 -249
  54. package/src/theme/context.tsx +0 -44
  55. package/src/theme/resolve.ts +0 -196
  56. package/src/theme/shibaox.json +0 -159
  57. package/src/ui/border.ts +0 -20
  58. package/src/ui/delayed-presence.ts +0 -23
  59. package/src/ui/dialog.tsx +0 -123
  60. package/src/ui/layout.ts +0 -23
  61. package/src/ui/marquee.ts +0 -75
  62. package/src/ui/pane-resize.ts +0 -77
  63. package/src/ui/toast.tsx +0 -203
  64. package/tsconfig.json +0 -19
@@ -1,196 +0,0 @@
1
- import { RGBA, type StyleDefinitionInput } from '@opentui/core';
2
-
3
- export type Feedback = 'error' | 'warning' | 'success' | 'info' | 'running';
4
- export type SurfaceName = 'dialog' | 'sidebar' | 'toast';
5
- export type SyntaxToken =
6
- | 'keyword'
7
- | 'string'
8
- | 'comment'
9
- | 'function'
10
- | 'type'
11
- | 'number'
12
- | 'heading'
13
- | 'link';
14
-
15
- export interface ResolvedTheme {
16
- text: {
17
- base: RGBA;
18
- muted: RGBA;
19
- action: { primary: { base: RGBA; focused: RGBA; selected: RGBA; disabled: RGBA } };
20
- feedback: Record<Feedback, RGBA>;
21
- };
22
- background: {
23
- base: RGBA;
24
- raised: { base: RGBA; high: RGBA; max: RGBA };
25
- action: { primary: { hovered: RGBA; focused: RGBA; selected: RGBA } };
26
- feedback: Record<Feedback, RGBA>;
27
- };
28
- border: { base: RGBA };
29
- scrollbar: { base: RGBA };
30
- diff: { added: RGBA; removed: RGBA; addedBg: RGBA; removedBg: RGBA };
31
- syntax: Record<SyntaxToken, RGBA>;
32
- /** The same theme one level up: dialogs, sidebars and toasts sit on a raised surface. */
33
- surface(name: SurfaceName): ResolvedTheme;
34
- }
35
-
36
- type Json = string | number | boolean | null | Json[] | { [k: string]: Json };
37
-
38
- const FEEDBACK: Feedback[] = ['error', 'warning', 'success', 'info', 'running'];
39
- const SYNTAX: SyntaxToken[] = [
40
- 'keyword',
41
- 'string',
42
- 'comment',
43
- 'function',
44
- 'type',
45
- 'number',
46
- 'heading',
47
- 'link',
48
- ];
49
-
50
- function lookup(root: Json, path: string): Json | undefined {
51
- let cur: Json | undefined = root;
52
- for (const part of path.split('.')) {
53
- if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) return undefined;
54
- cur = (cur as { [k: string]: Json })[part];
55
- }
56
- return cur;
57
- }
58
-
59
- /** Turns `#rrggbb` or a `$a.b.c` reference (recursively) into an RGBA; throws on a dangling ref. */
60
- function color(root: Json, value: Json | undefined, ref: string, depth = 0): RGBA {
61
- if (typeof value !== 'string' || depth > 16) throw new Error(`theme: unresolved ${ref}`);
62
- if (value.startsWith('$')) {
63
- const target = lookup(root, value.slice(1));
64
- if (target === undefined) throw new Error(`theme: unresolved ${value}`);
65
- return color(root, target, value, depth + 1);
66
- }
67
- if (!/^#[0-9a-fA-F]{6}$/.test(value)) throw new Error(`theme: unresolved ${ref}`);
68
- return RGBA.fromHex(value);
69
- }
70
-
71
- function build(get: (path: string) => RGBA): ResolvedTheme {
72
- const feedback = (prefix: string): Record<Feedback, RGBA> =>
73
- Object.fromEntries(FEEDBACK.map((f) => [f, get(`${prefix}.${f}`)])) as Record<Feedback, RGBA>;
74
- const theme: ResolvedTheme = {
75
- text: {
76
- base: get('text.base'),
77
- muted: get('text.muted'),
78
- action: {
79
- primary: {
80
- base: get('text.action.primary.base'),
81
- focused: get('text.action.primary.focused'),
82
- selected: get('text.action.primary.selected'),
83
- disabled: get('text.action.primary.disabled'),
84
- },
85
- },
86
- feedback: feedback('text.feedback'),
87
- },
88
- background: {
89
- base: get('background.base'),
90
- raised: {
91
- base: get('background.raised.base'),
92
- high: get('background.raised.high'),
93
- max: get('background.raised.max'),
94
- },
95
- action: {
96
- primary: {
97
- hovered: get('background.action.primary.hovered'),
98
- focused: get('background.action.primary.focused'),
99
- selected: get('background.action.primary.selected'),
100
- },
101
- },
102
- feedback: feedback('background.feedback'),
103
- },
104
- border: { base: get('border.base') },
105
- scrollbar: { base: get('scrollbar.base') },
106
- diff: {
107
- added: get('diff.added'),
108
- removed: get('diff.removed'),
109
- addedBg: get('diff.addedBg'),
110
- removedBg: get('diff.removedBg'),
111
- },
112
- syntax: Object.fromEntries(SYNTAX.map((s) => [s, get(`syntax.${s}`)])) as Record<
113
- SyntaxToken,
114
- RGBA
115
- >,
116
- surface: () => raise(theme),
117
- };
118
- return theme;
119
- }
120
-
121
- /** Re-resolves the theme on a raised surface: each background level moves one step up. */
122
- function raise(base: ResolvedTheme): ResolvedTheme {
123
- const theme: ResolvedTheme = {
124
- ...base,
125
- background: {
126
- ...base.background,
127
- base: base.background.raised.base,
128
- raised: {
129
- base: base.background.raised.high,
130
- high: base.background.raised.max,
131
- max: base.background.raised.max,
132
- },
133
- },
134
- surface: () => raise(theme),
135
- };
136
- return theme;
137
- }
138
-
139
- /** Resolves a theme JSON (hue scales + semantic tokens with `$` references) into RGBA tokens. */
140
- export function resolveTheme(json: unknown): ResolvedTheme {
141
- const root = json as Json;
142
- const cache = new Map<string, RGBA>();
143
- const get = (path: string): RGBA => {
144
- let c = cache.get(path);
145
- if (!c) {
146
- c = color(root, lookup(root, path), `$${path}`);
147
- cache.set(path, c);
148
- }
149
- return c;
150
- };
151
- return build(get);
152
- }
153
-
154
- /** What the markdown, code and diff renderables ask a `SyntaxStyle` for. */
155
- export function syntaxStyles(t: ResolvedTheme): Record<string, StyleDefinitionInput> {
156
- const s = t.syntax;
157
- return {
158
- default: { fg: t.text.base },
159
- conceal: { fg: t.text.muted },
160
- keyword: { fg: s.keyword },
161
- 'keyword.function': { fg: s.keyword },
162
- 'keyword.return': { fg: s.keyword },
163
- 'keyword.import': { fg: s.keyword },
164
- string: { fg: s.string },
165
- 'string.special': { fg: s.string },
166
- comment: { fg: s.comment, italic: true },
167
- function: { fg: s.function },
168
- 'function.call': { fg: s.function },
169
- 'function.method': { fg: s.function },
170
- type: { fg: s.type },
171
- 'type.builtin': { fg: s.type },
172
- number: { fg: s.number },
173
- boolean: { fg: s.number },
174
- constant: { fg: s.number },
175
- operator: { fg: t.text.muted },
176
- punctuation: { fg: t.text.muted },
177
- 'punctuation.bracket': { fg: t.text.muted },
178
- variable: { fg: t.text.base },
179
- property: { fg: t.text.base },
180
- 'markup.heading': { fg: s.heading, bold: true },
181
- 'markup.bold': { fg: t.text.base, bold: true },
182
- 'markup.strong': { fg: t.text.base, bold: true },
183
- 'markup.italic': { fg: t.text.base, italic: true },
184
- 'markup.raw': { fg: s.string },
185
- 'markup.raw.block': { fg: t.text.base },
186
- 'markup.raw.inline': { fg: s.string },
187
- 'markup.link': { fg: s.link, underline: true },
188
- 'markup.link.url': { fg: s.link, underline: true },
189
- 'markup.link.label': { fg: s.link },
190
- 'markup.list': { fg: s.keyword },
191
- 'markup.quote': { fg: t.text.muted, italic: true },
192
- 'markup.strikethrough': { fg: t.text.muted },
193
- 'diff.plus': { fg: t.diff.added },
194
- 'diff.minus': { fg: t.diff.removed },
195
- };
196
- }
@@ -1,159 +0,0 @@
1
- {
2
- "name": "shibaox-ink",
3
- "hue": {
4
- "neutral": {
5
- "100": "#ffffff",
6
- "200": "#e6e6e6",
7
- "300": "#b8b8b8",
8
- "400": "#8c8c8c",
9
- "500": "#5c5c5c",
10
- "600": "#262626",
11
- "700": "#1c1c1c",
12
- "800": "#141414",
13
- "900": "#0a0a0a"
14
- },
15
- "interactive": {
16
- "100": "#ffc79a",
17
- "200": "#ffa15c",
18
- "300": "#f2842b",
19
- "400": "#db6716",
20
- "500": "#a94a0a",
21
- "600": "#3d2a17",
22
- "700": "#2c1f12",
23
- "800": "#1f1806",
24
- "900": "#140f04"
25
- },
26
- "green": {
27
- "100": "#a8e4bf",
28
- "200": "#6fcf97",
29
- "300": "#4fb374",
30
- "400": "#3a9459",
31
- "500": "#266e43",
32
- "600": "#12301e",
33
- "700": "#0e2617",
34
- "800": "#0a1c11",
35
- "900": "#06120b"
36
- },
37
- "red": {
38
- "100": "#ffb3bc",
39
- "200": "#ff6b6b",
40
- "300": "#e85a6c",
41
- "400": "#c42840",
42
- "500": "#961d31",
43
- "600": "#3a2024",
44
- "700": "#300e14",
45
- "800": "#230a0e",
46
- "900": "#160609"
47
- },
48
- "yellow": {
49
- "100": "#f8dc8f",
50
- "200": "#f2c150",
51
- "300": "#d9a52e",
52
- "400": "#b5861a",
53
- "500": "#8a5e00",
54
- "600": "#35290a",
55
- "700": "#2a2008",
56
- "800": "#1f1806",
57
- "900": "#140f04"
58
- },
59
- "blue": {
60
- "100": "#8fbaff",
61
- "200": "#5b9cff",
62
- "300": "#3f7fe6",
63
- "400": "#2f63b8",
64
- "500": "#244b8a",
65
- "600": "#1b2f3a",
66
- "700": "#111e3a",
67
- "800": "#0c162b",
68
- "900": "#080e1c"
69
- },
70
- "cyan": {
71
- "100": "#b0e6f0",
72
- "200": "#7fd3e6",
73
- "300": "#58b8cf",
74
- "400": "#3a97ad",
75
- "500": "#2a7185",
76
- "600": "#123038",
77
- "700": "#0e262c",
78
- "800": "#0a1c21",
79
- "900": "#061215"
80
- },
81
- "purple": {
82
- "100": "#dcc6ff",
83
- "200": "#c9a6ff",
84
- "300": "#ad83f0",
85
- "400": "#8b5cd6",
86
- "500": "#6a41a8",
87
- "600": "#2b1b48",
88
- "700": "#22153a",
89
- "800": "#19102b",
90
- "900": "#100a1c"
91
- },
92
- "orange": {
93
- "100": "#ffc79a",
94
- "200": "#ffa15c",
95
- "300": "#f2842b",
96
- "400": "#db6716",
97
- "500": "#a94a0a",
98
- "600": "#3b2213",
99
- "700": "#2c1a10",
100
- "800": "#22130b",
101
- "900": "#170c07"
102
- }
103
- },
104
- "text": {
105
- "base": "$hue.neutral.200",
106
- "muted": "$hue.neutral.400",
107
- "action": {
108
- "primary": {
109
- "base": "$text.base",
110
- "focused": "$hue.neutral.900",
111
- "selected": "$hue.interactive.200",
112
- "disabled": "$hue.neutral.500"
113
- }
114
- },
115
- "feedback": {
116
- "error": "$hue.red.200",
117
- "warning": "$hue.yellow.200",
118
- "success": "$hue.green.200",
119
- "info": "$hue.cyan.200",
120
- "running": "$hue.blue.200"
121
- }
122
- },
123
- "background": {
124
- "base": "$hue.neutral.900",
125
- "raised": { "base": "$hue.neutral.800", "high": "$hue.neutral.700", "max": "$hue.neutral.600" },
126
- "action": {
127
- "primary": {
128
- "hovered": "$hue.neutral.700",
129
- "focused": "$hue.interactive.200",
130
- "selected": "$hue.interactive.600"
131
- }
132
- },
133
- "feedback": {
134
- "error": "$hue.red.600",
135
- "warning": "$hue.yellow.600",
136
- "success": "$hue.green.600",
137
- "info": "$hue.cyan.600",
138
- "running": "$hue.blue.600"
139
- }
140
- },
141
- "border": { "base": "$hue.neutral.600" },
142
- "scrollbar": { "base": "$hue.neutral.600" },
143
- "diff": {
144
- "added": "$hue.green.200",
145
- "removed": "$hue.red.200",
146
- "addedBg": "$hue.blue.600",
147
- "removedBg": "$hue.red.600"
148
- },
149
- "syntax": {
150
- "keyword": "$hue.purple.200",
151
- "string": "$hue.green.200",
152
- "comment": "$hue.neutral.400",
153
- "function": "$hue.blue.200",
154
- "type": "$hue.cyan.200",
155
- "number": "$hue.yellow.200",
156
- "heading": "$hue.orange.200",
157
- "link": "$hue.cyan.200"
158
- }
159
- }
package/src/ui/border.ts DELETED
@@ -1,20 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- export const EmptyBorder = {
3
- topLeft: '',
4
- bottomLeft: '',
5
- vertical: '',
6
- topRight: '',
7
- bottomRight: '',
8
- horizontal: ' ',
9
- bottomT: '',
10
- topT: '',
11
- cross: '',
12
- leftT: '',
13
- rightT: '',
14
- };
15
-
16
- /** Only the left and right edges, drawn as a heavy bar: cards, toasts, the active tab. */
17
- export const SplitBorder = {
18
- border: ['left' as const, 'right' as const],
19
- customBorderChars: { ...EmptyBorder, vertical: '┃' },
20
- };
@@ -1,23 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { type Accessor, createEffect, createSignal, onCleanup } from 'solid-js';
3
-
4
- /** True once `source` has been defined for `delay` ms; resets whenever it changes (no flicker under the delay). */
5
- export function createDelayedPresence<T>(
6
- source: Accessor<T | undefined>,
7
- delay: number | ((value: T) => number),
8
- ): Accessor<boolean> {
9
- const [visible, setVisible] = createSignal(false);
10
- createEffect(() => {
11
- const current = source();
12
- setVisible(false);
13
- if (current === undefined) return;
14
- const remaining = typeof delay === 'function' ? delay(current) : delay;
15
- if (remaining <= 0) {
16
- setVisible(true);
17
- return;
18
- }
19
- const timer = setTimeout(() => setVisible(true), remaining);
20
- onCleanup(() => clearTimeout(timer));
21
- });
22
- return visible;
23
- }
package/src/ui/dialog.tsx DELETED
@@ -1,123 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { RGBA, TextAttributes } from '@opentui/core';
3
- import { useTerminalDimensions } from '@opentui/solid';
4
- import {
5
- type Accessor,
6
- createContext,
7
- For,
8
- type JSX,
9
- type ParentProps,
10
- useContext,
11
- } from 'solid-js';
12
- import { createStore } from 'solid-js/store';
13
- import { useKeys } from '../context/keys.js';
14
- import { useTheme } from '../theme/context.js';
15
-
16
- export type DialogSize = 'medium' | 'large' | 'xlarge';
17
-
18
- export function dialogWidth(size: DialogSize): 60 | 88 | 116 {
19
- if (size === 'xlarge') return 116;
20
- if (size === 'large') return 88;
21
- return 60;
22
- }
23
-
24
- /** A modal surface over a dimmed page; esc (or a click outside) calls onClose. */
25
- export function Dialog(
26
- props: ParentProps<{
27
- size?: DialogSize;
28
- centered?: boolean;
29
- onClose: () => void;
30
- title?: string;
31
- }>,
32
- ): JSX.Element {
33
- const dimensions = useTerminalDimensions();
34
- const theme = useTheme().surface('dialog');
35
- useKeys('dialog', (key) => {
36
- if (key.name === 'escape') {
37
- props.onClose();
38
- return true;
39
- }
40
- return false;
41
- });
42
- return (
43
- <box
44
- position="absolute"
45
- left={0}
46
- top={0}
47
- zIndex={3000}
48
- width={dimensions().width}
49
- height={dimensions().height}
50
- alignItems="center"
51
- justifyContent={props.centered ? 'center' : undefined}
52
- paddingTop={props.centered ? 0 : Math.floor(dimensions().height / 4)}
53
- backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
54
- onMouseUp={() => props.onClose()}
55
- >
56
- <box
57
- width={dialogWidth(props.size ?? 'medium')}
58
- maxWidth={dimensions().width - 2}
59
- backgroundColor={theme.background.base}
60
- paddingTop={1}
61
- paddingBottom={1}
62
- paddingLeft={2}
63
- paddingRight={2}
64
- flexDirection="column"
65
- onMouseUp={(e: { stopPropagation(): void }) => e.stopPropagation()}
66
- >
67
- {props.title ? (
68
- <box height={1} flexShrink={0} marginBottom={1}>
69
- <text attributes={TextAttributes.BOLD} fg={theme.text.base}>
70
- {props.title}
71
- </text>
72
- </box>
73
- ) : null}
74
- {props.children}
75
- </box>
76
- </box>
77
- );
78
- }
79
-
80
- export interface DialogApi {
81
- open(element: () => JSX.Element, o?: { onClose?: () => void }): void;
82
- close(): void;
83
- clear(): void;
84
- depth: Accessor<number>;
85
- }
86
-
87
- const Context = createContext<DialogApi>();
88
-
89
- /** Keeps the dialog stack and draws it above the page. */
90
- export function DialogProvider(props: ParentProps): JSX.Element {
91
- const [store, setStore] = createStore({
92
- stack: [] as { element: () => JSX.Element; onClose?: () => void }[],
93
- });
94
- const api: DialogApi = {
95
- open(element, o) {
96
- setStore('stack', (s) => [...s, { element, onClose: o?.onClose }]);
97
- },
98
- close() {
99
- const top = store.stack.at(-1);
100
- if (!top) return;
101
- setStore('stack', (s) => s.slice(0, -1));
102
- top.onClose?.();
103
- },
104
- clear() {
105
- const all = [...store.stack];
106
- setStore('stack', []);
107
- for (const d of all) d.onClose?.();
108
- },
109
- depth: () => store.stack.length,
110
- };
111
- return (
112
- <Context.Provider value={api}>
113
- {props.children}
114
- <For each={store.stack}>{(d) => d.element()}</For>
115
- </Context.Provider>
116
- );
117
- }
118
-
119
- export function useDialog(): DialogApi {
120
- const c = useContext(Context);
121
- if (!c) throw new Error('useDialog outside DialogProvider');
122
- return c;
123
- }
package/src/ui/layout.ts DELETED
@@ -1,23 +0,0 @@
1
- export const RAIL_WIDTH = 20;
2
- export const RAIL_BREAKPOINT = 106;
3
- export const SIDEBAR_WIDTH = 42;
4
- export const SIDEBAR_MIN = 24;
5
- export const SIDEBAR_MAX = 72;
6
- export const CONTENT_MIN = 44;
7
- export const SIDEBAR_AUTO_MIN = 120;
8
- export const MIN_COLS = 60;
9
- export const MIN_ROWS = 15;
10
-
11
- /** The tabs rail stands on the left from 106 columns; below that it is one line on top. */
12
- export const railVertical = (width: number): boolean => width >= RAIL_BREAKPOINT;
13
-
14
- /** The sidebar opens by itself when the content keeps 120 columns beside the rail. */
15
- export const sidebarAuto = (width: number, rail: number): boolean =>
16
- width - rail >= SIDEBAR_AUTO_MIN;
17
-
18
- /** A sidebar width between 24 and 72 that leaves the content at least 44 columns. */
19
- export const clampSidebarWidth = (w: number, total: number): number =>
20
- Math.max(SIDEBAR_MIN, Math.min(w, SIDEBAR_MAX, total - CONTENT_MIN));
21
-
22
- export const tooSmall = (width: number, height: number): boolean =>
23
- width < MIN_COLS || height < MIN_ROWS;
package/src/ui/marquee.ts DELETED
@@ -1,75 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- const GAP = ' · ';
3
- const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
4
-
5
- /** Terminal cell width of a grapheme: 2 for East Asian wide/fullwidth and most emoji, else 1. */
6
- export function graphemeWidth(g: string): number {
7
- const cp = g.codePointAt(0) ?? 0;
8
- if (cp < 0x1100) return 1;
9
- if (
10
- (cp >= 0x1100 && cp <= 0x115f) ||
11
- (cp >= 0x2e80 && cp <= 0xa4cf) ||
12
- (cp >= 0xac00 && cp <= 0xd7a3) ||
13
- (cp >= 0xf900 && cp <= 0xfaff) ||
14
- (cp >= 0xfe30 && cp <= 0xfe4f) ||
15
- (cp >= 0xff00 && cp <= 0xff60) ||
16
- (cp >= 0xffe0 && cp <= 0xffe6) ||
17
- (cp >= 0x1f300 && cp <= 0x1faff) ||
18
- (cp >= 0x20000 && cp <= 0x3fffd)
19
- )
20
- return 2;
21
- return 1;
22
- }
23
-
24
- export function graphemes(value: string): string[] {
25
- return [...segmenter.segment(value)].map((s) => s.segment);
26
- }
27
-
28
- export function stringWidth(value: string): number {
29
- return graphemes(value).reduce((w, g) => w + graphemeWidth(g), 0);
30
- }
31
-
32
- /** The longest prefix of `value` that fits in `width` cells. */
33
- export function takeWidth(value: string, width: number): string {
34
- let out = '';
35
- let w = 0;
36
- for (const g of graphemes(value)) {
37
- const gw = graphemeWidth(g);
38
- if (w + gw > width) break;
39
- out += g;
40
- w += gw;
41
- }
42
- return out;
43
- }
44
-
45
- export function marqueeCycleWidth(value: string): number {
46
- return stringWidth(value + GAP);
47
- }
48
-
49
- export function marqueeOverflows(value: string, width: number): boolean {
50
- return stringWidth(value) > width;
51
- }
52
-
53
- /** `value` scrolled by `offset` cells inside `width`, looping with a ` · ` gap; unscrolled when it fits. */
54
- export function marqueeText(value: string, width: number, offset: number): string {
55
- if (width <= 0) return '';
56
- if (stringWidth(value) <= width || offset <= 0) return takeWidth(value, width);
57
- const loop = [...graphemes(value), ...graphemes(GAP)];
58
- const cursor = offset % marqueeCycleWidth(value);
59
- const parts = [...loop, ...loop];
60
- let start = 0;
61
- let skipped = 0;
62
- while (start < parts.length && skipped < cursor) {
63
- skipped += graphemeWidth(parts[start] ?? '');
64
- start++;
65
- }
66
- let out = '';
67
- let w = 0;
68
- for (const g of parts.slice(start)) {
69
- const gw = graphemeWidth(g);
70
- if (w + gw > width) break;
71
- out += g;
72
- w += gw;
73
- }
74
- return out;
75
- }
@@ -1,77 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { MouseButton, type MouseEvent } from '@opentui/core';
3
- import { createEffect, createSignal } from 'solid-js';
4
-
5
- /** Drag state for a pane edge: the size while dragging, and a commit when the pointer releases. */
6
- export function createPaneResize(options: {
7
- value: () => number;
8
- defaultValue: () => number;
9
- clamp: (size: number) => number;
10
- fromMouse: (event: MouseEvent) => number;
11
- contains: (event: MouseEvent, size: number) => boolean;
12
- onCommit: (size: number) => void;
13
- }) {
14
- const [preferredSize, setPreferredSize] = createSignal(options.value());
15
- const [hovered, setHovered] = createSignal(false);
16
- const [resizing, setResizing] = createSignal(false);
17
- let requestedSize = options.value();
18
- createEffect(() => {
19
- if (resizing()) return;
20
- requestedSize = options.value();
21
- setPreferredSize(requestedSize);
22
- });
23
- const size = () => options.clamp(preferredSize());
24
- const commit = (value: number) => {
25
- const next = options.clamp(value);
26
- setPreferredSize(next);
27
- if (requestedSize === next) return;
28
- requestedSize = next;
29
- options.onCommit(next);
30
- };
31
- let moved = false;
32
- let lastBoundaryClick = 0;
33
- const finish = (event: MouseEvent) => {
34
- if (!resizing()) return;
35
- const next = moved ? options.fromMouse(event) : size();
36
- setResizing(false);
37
- lastBoundaryClick = moved ? 0 : Date.now();
38
- commit(next);
39
- setHovered(options.contains(event, options.clamp(next)));
40
- event.stopPropagation();
41
- };
42
- // Bind drag/release on the parent so resizing continues outside the handle.
43
- return {
44
- preferredSize,
45
- size,
46
- hovered,
47
- resizing,
48
- onMouseOver: () => setHovered(true),
49
- onMouseOut: () => setHovered(false),
50
- onMouseDown: (event: MouseEvent) => {
51
- if (event.button !== MouseButton.LEFT) return;
52
- const now = Date.now();
53
- if (now - lastBoundaryClick < 300) {
54
- lastBoundaryClick = 0;
55
- setResizing(false);
56
- setHovered(false);
57
- commit(options.defaultValue());
58
- event.preventDefault();
59
- event.stopPropagation();
60
- return;
61
- }
62
- moved = false;
63
- setResizing(true);
64
- event.preventDefault();
65
- event.stopPropagation();
66
- },
67
- onMouseDrag: (event: MouseEvent) => {
68
- if (!resizing()) return;
69
- moved = true;
70
- lastBoundaryClick = 0;
71
- setPreferredSize(options.clamp(options.fromMouse(event)));
72
- event.stopPropagation();
73
- },
74
- onMouseDragEnd: finish,
75
- onMouseUp: finish,
76
- };
77
- }