@textui/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/app/app.d.ts +200 -0
- package/dist/app/app.d.ts.map +1 -0
- package/dist/app/app.js +980 -0
- package/dist/core/animation.d.ts +34 -0
- package/dist/core/animation.d.ts.map +1 -0
- package/dist/core/animation.js +125 -0
- package/dist/core/clipboard.d.ts +29 -0
- package/dist/core/clipboard.d.ts.map +1 -0
- package/dist/core/clipboard.js +30 -0
- package/dist/core/commands.d.ts +42 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +146 -0
- package/dist/core/components.d.ts +27 -0
- package/dist/core/components.d.ts.map +1 -0
- package/dist/core/components.js +78 -0
- package/dist/core/events.d.ts +23 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +77 -0
- package/dist/core/focus.d.ts +69 -0
- package/dist/core/focus.d.ts.map +1 -0
- package/dist/core/focus.js +336 -0
- package/dist/core/i18n.d.ts +27 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/i18n.js +88 -0
- package/dist/core/keybindings.d.ts +53 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +163 -0
- package/dist/core/layers.d.ts +28 -0
- package/dist/core/layers.d.ts.map +1 -0
- package/dist/core/layers.js +84 -0
- package/dist/core/manifest.d.ts +22 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +85 -0
- package/dist/core/navigation.d.ts +47 -0
- package/dist/core/navigation.d.ts.map +1 -0
- package/dist/core/navigation.js +110 -0
- package/dist/core/resources.d.ts +115 -0
- package/dist/core/resources.d.ts.map +1 -0
- package/dist/core/resources.js +321 -0
- package/dist/core/services.d.ts +21 -0
- package/dist/core/services.d.ts.map +1 -0
- package/dist/core/services.js +63 -0
- package/dist/core/store.d.ts +60 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +593 -0
- package/dist/core/surfaces.d.ts +81 -0
- package/dist/core/surfaces.d.ts.map +1 -0
- package/dist/core/surfaces.js +237 -0
- package/dist/core/syntax.d.ts +49 -0
- package/dist/core/syntax.d.ts.map +1 -0
- package/dist/core/syntax.js +172 -0
- package/dist/core/when.d.ts +15 -0
- package/dist/core/when.d.ts.map +1 -0
- package/dist/core/when.js +224 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/jsx/factory.d.ts +27 -0
- package/dist/jsx/factory.d.ts.map +1 -0
- package/dist/jsx/factory.js +102 -0
- package/dist/jsx/intrinsics.d.ts +96 -0
- package/dist/jsx/intrinsics.d.ts.map +1 -0
- package/dist/jsx/intrinsics.js +1 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
- package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-dev-runtime.js +12 -0
- package/dist/jsx/jsx-runtime.d.ts +42 -0
- package/dist/jsx/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime.js +16 -0
- package/dist/render/buffer.d.ts +77 -0
- package/dist/render/buffer.d.ts.map +1 -0
- package/dist/render/buffer.js +275 -0
- package/dist/render/color.d.ts +35 -0
- package/dist/render/color.d.ts.map +1 -0
- package/dist/render/color.js +160 -0
- package/dist/render/diff.d.ts +37 -0
- package/dist/render/diff.d.ts.map +1 -0
- package/dist/render/diff.js +66 -0
- package/dist/render/layout.d.ts +72 -0
- package/dist/render/layout.d.ts.map +1 -0
- package/dist/render/layout.js +633 -0
- package/dist/render/static.d.ts +63 -0
- package/dist/render/static.d.ts.map +1 -0
- package/dist/render/static.js +209 -0
- package/dist/runtime/bindings.d.ts +46 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +116 -0
- package/dist/runtime/hooks.d.ts +284 -0
- package/dist/runtime/hooks.d.ts.map +1 -0
- package/dist/runtime/hooks.js +846 -0
- package/dist/runtime/instance.d.ts +106 -0
- package/dist/runtime/instance.d.ts.map +1 -0
- package/dist/runtime/instance.js +181 -0
- package/dist/runtime/paint.d.ts +27 -0
- package/dist/runtime/paint.d.ts.map +1 -0
- package/dist/runtime/paint.js +567 -0
- package/dist/runtime/reconcile.d.ts +23 -0
- package/dist/runtime/reconcile.d.ts.map +1 -0
- package/dist/runtime/reconcile.js +260 -0
- package/dist/runtime/runtime.d.ts +42 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/style.d.ts +55 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/style.js +143 -0
- package/dist/themes/borders.d.ts +10 -0
- package/dist/themes/borders.d.ts.map +1 -0
- package/dist/themes/borders.js +88 -0
- package/dist/themes/builtin.d.ts +31 -0
- package/dist/themes/builtin.d.ts.map +1 -0
- package/dist/themes/builtin.js +291 -0
- package/dist/themes/glyphs.d.ts +15 -0
- package/dist/themes/glyphs.d.ts.map +1 -0
- package/dist/themes/glyphs.js +97 -0
- package/dist/themes/index.d.ts +5 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +4 -0
- package/dist/themes/registry.d.ts +20 -0
- package/dist/themes/registry.d.ts.map +1 -0
- package/dist/themes/registry.js +204 -0
- package/dist/types/adapter.d.ts +38 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +1 -0
- package/dist/types/animation.d.ts +31 -0
- package/dist/types/animation.d.ts.map +1 -0
- package/dist/types/animation.js +1 -0
- package/dist/types/app.d.ts +148 -0
- package/dist/types/app.d.ts.map +1 -0
- package/dist/types/app.js +1 -0
- package/dist/types/async.d.ts +30 -0
- package/dist/types/async.d.ts.map +1 -0
- package/dist/types/async.js +1 -0
- package/dist/types/capabilities.d.ts +44 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +33 -0
- package/dist/types/cells.d.ts +72 -0
- package/dist/types/cells.d.ts.map +1 -0
- package/dist/types/cells.js +10 -0
- package/dist/types/command.d.ts +127 -0
- package/dist/types/command.d.ts.map +1 -0
- package/dist/types/command.js +1 -0
- package/dist/types/component-registry.d.ts +77 -0
- package/dist/types/component-registry.d.ts.map +1 -0
- package/dist/types/component-registry.js +1 -0
- package/dist/types/disposable.d.ts +8 -0
- package/dist/types/disposable.d.ts.map +1 -0
- package/dist/types/disposable.js +1 -0
- package/dist/types/focus.d.ts +72 -0
- package/dist/types/focus.d.ts.map +1 -0
- package/dist/types/focus.js +1 -0
- package/dist/types/geometry.d.ts +28 -0
- package/dist/types/geometry.d.ts.map +1 -0
- package/dist/types/geometry.js +14 -0
- package/dist/types/graph.d.ts +129 -0
- package/dist/types/graph.d.ts.map +1 -0
- package/dist/types/graph.js +31 -0
- package/dist/types/i18n.d.ts +23 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/i18n.js +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +31 -0
- package/dist/types/input.d.ts +65 -0
- package/dist/types/input.d.ts.map +1 -0
- package/dist/types/input.js +1 -0
- package/dist/types/keybinding.d.ts +49 -0
- package/dist/types/keybinding.d.ts.map +1 -0
- package/dist/types/keybinding.js +1 -0
- package/dist/types/layer.d.ts +54 -0
- package/dist/types/layer.d.ts.map +1 -0
- package/dist/types/layer.js +1 -0
- package/dist/types/manifest.d.ts +114 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/markdown.d.ts +58 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/markdown.js +1 -0
- package/dist/types/navigation.d.ts +40 -0
- package/dist/types/navigation.d.ts.map +1 -0
- package/dist/types/navigation.js +1 -0
- package/dist/types/render.d.ts +72 -0
- package/dist/types/render.d.ts.map +1 -0
- package/dist/types/render.js +1 -0
- package/dist/types/resource.d.ts +170 -0
- package/dist/types/resource.d.ts.map +1 -0
- package/dist/types/resource.js +1 -0
- package/dist/types/services.d.ts +24 -0
- package/dist/types/services.d.ts.map +1 -0
- package/dist/types/services.js +3 -0
- package/dist/types/shell.d.ts +32 -0
- package/dist/types/shell.d.ts.map +1 -0
- package/dist/types/shell.js +1 -0
- package/dist/types/store.d.ts +124 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/store.js +1 -0
- package/dist/types/stream.d.ts +18 -0
- package/dist/types/stream.d.ts.map +1 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/style.d.ts +166 -0
- package/dist/types/style.d.ts.map +1 -0
- package/dist/types/style.js +1 -0
- package/dist/types/surface.d.ts +97 -0
- package/dist/types/surface.d.ts.map +1 -0
- package/dist/types/surface.js +1 -0
- package/dist/types/syntax.d.ts +68 -0
- package/dist/types/syntax.d.ts.map +1 -0
- package/dist/types/syntax.js +5 -0
- package/dist/types/terminal.d.ts +52 -0
- package/dist/types/terminal.d.ts.map +1 -0
- package/dist/types/terminal.js +1 -0
- package/dist/types/theme.d.ts +117 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/dist/types/theme.js +1 -0
- package/dist/types/when.d.ts +19 -0
- package/dist/types/when.d.ts.map +1 -0
- package/dist/types/when.js +1 -0
- package/dist/ui/primitives.d.ts +56 -0
- package/dist/ui/primitives.d.ts.map +1 -0
- package/dist/ui/primitives.js +108 -0
- package/dist/ui/screen.d.ts +23 -0
- package/dist/ui/screen.d.ts.map +1 -0
- package/dist/ui/screen.js +18 -0
- package/dist/util/disposable.d.ts +6 -0
- package/dist/util/disposable.d.ts.map +1 -0
- package/dist/util/disposable.js +49 -0
- package/dist/util/markdown.d.ts +24 -0
- package/dist/util/markdown.d.ts.map +1 -0
- package/dist/util/markdown.js +220 -0
- package/dist/util/paths.d.ts +47 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +134 -0
- package/dist/util/stream.d.ts +32 -0
- package/dist/util/stream.d.ts.map +1 -0
- package/dist/util/stream.js +206 -0
- package/dist/util/text.d.ts +65 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +419 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +11 -0
- package/src/app/app.ts +1096 -0
- package/src/core/animation.ts +144 -0
- package/src/core/clipboard.ts +40 -0
- package/src/core/commands.ts +169 -0
- package/src/core/components.ts +91 -0
- package/src/core/events.ts +95 -0
- package/src/core/focus.ts +358 -0
- package/src/core/i18n.ts +107 -0
- package/src/core/keybindings.ts +184 -0
- package/src/core/layers.ts +94 -0
- package/src/core/manifest.ts +84 -0
- package/src/core/navigation.ts +135 -0
- package/src/core/resources.ts +362 -0
- package/src/core/services.ts +69 -0
- package/src/core/store.ts +640 -0
- package/src/core/surfaces.ts +292 -0
- package/src/core/syntax.ts +200 -0
- package/src/core/when.ts +238 -0
- package/src/index.ts +76 -0
- package/src/jsx/factory.ts +124 -0
- package/src/jsx/intrinsics.ts +99 -0
- package/src/jsx/jsx-dev-runtime.ts +23 -0
- package/src/jsx/jsx-runtime.ts +76 -0
- package/src/render/buffer.ts +318 -0
- package/src/render/color.ts +180 -0
- package/src/render/diff.ts +99 -0
- package/src/render/layout.ts +764 -0
- package/src/render/static.ts +290 -0
- package/src/runtime/bindings.ts +150 -0
- package/src/runtime/hooks.ts +1057 -0
- package/src/runtime/instance.ts +284 -0
- package/src/runtime/paint.ts +737 -0
- package/src/runtime/reconcile.ts +335 -0
- package/src/runtime/runtime.ts +47 -0
- package/src/runtime/style.ts +205 -0
- package/src/themes/borders.ts +95 -0
- package/src/themes/builtin.ts +301 -0
- package/src/themes/glyphs.ts +101 -0
- package/src/themes/index.ts +4 -0
- package/src/themes/registry.ts +224 -0
- package/src/types/adapter.ts +41 -0
- package/src/types/animation.ts +34 -0
- package/src/types/app.ts +149 -0
- package/src/types/async.ts +35 -0
- package/src/types/capabilities.ts +79 -0
- package/src/types/cells.ts +79 -0
- package/src/types/command.ts +139 -0
- package/src/types/component-registry.ts +82 -0
- package/src/types/disposable.ts +8 -0
- package/src/types/focus.ts +77 -0
- package/src/types/geometry.ts +50 -0
- package/src/types/graph.ts +174 -0
- package/src/types/i18n.ts +25 -0
- package/src/types/index.ts +31 -0
- package/src/types/input.ts +81 -0
- package/src/types/keybinding.ts +51 -0
- package/src/types/layer.ts +46 -0
- package/src/types/manifest.ts +101 -0
- package/src/types/markdown.ts +47 -0
- package/src/types/navigation.ts +42 -0
- package/src/types/render.ts +86 -0
- package/src/types/resource.ts +185 -0
- package/src/types/services.ts +28 -0
- package/src/types/shell.ts +30 -0
- package/src/types/store.ts +148 -0
- package/src/types/stream.ts +24 -0
- package/src/types/style.ts +205 -0
- package/src/types/surface.ts +119 -0
- package/src/types/syntax.ts +93 -0
- package/src/types/terminal.ts +58 -0
- package/src/types/theme.ts +121 -0
- package/src/types/when.ts +21 -0
- package/src/ui/primitives.ts +118 -0
- package/src/ui/screen.ts +41 -0
- package/src/util/disposable.ts +49 -0
- package/src/util/markdown.ts +225 -0
- package/src/util/paths.ts +138 -0
- package/src/util/stream.ts +213 -0
- package/src/util/text.ts +428 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import type { BindingPath, ComponentNode, ErrorFallback, RenderError } from '../types/graph.js';
|
|
2
|
+
import type { Runtime } from './runtime.js';
|
|
3
|
+
import type { Instance } from './instance.js';
|
|
4
|
+
import {
|
|
5
|
+
createInstance, expandTemplate, findFunction, markDirty, normalizeChildren,
|
|
6
|
+
unmountInstance, updateProps, walkInstances,
|
|
7
|
+
} from './instance.js';
|
|
8
|
+
import { isTemplateChildren } from './bindings.js';
|
|
9
|
+
import { setCurrentInstance } from './hooks.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The render pass: a node graph in, a retained instance tree out.
|
|
13
|
+
*
|
|
14
|
+
* Two things make this cheap enough to run per frame. Instances are matched by
|
|
15
|
+
* key and component name, so an unchanged subtree keeps its hooks and its
|
|
16
|
+
* layout. And an instance is only re-rendered when something marked it dirty -
|
|
17
|
+
* its own state, a store path it read, or its parent handing it new props.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface RenderOptions {
|
|
21
|
+
/** Re-render everything, ignoring dirty flags. After a theme change. */
|
|
22
|
+
force?: boolean;
|
|
23
|
+
/** Record why each instance rendered. */
|
|
24
|
+
diagnostics?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_DEPTH = 200;
|
|
28
|
+
|
|
29
|
+
export function renderTree(
|
|
30
|
+
runtime: Runtime,
|
|
31
|
+
root: Instance | null,
|
|
32
|
+
node: ComponentNode,
|
|
33
|
+
options: RenderOptions = {},
|
|
34
|
+
): Instance {
|
|
35
|
+
const instance = reconcile(runtime, null, root ?? undefined, node, options, 0, undefined);
|
|
36
|
+
return instance;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Match by component name and key; anything else is a fresh mount. */
|
|
40
|
+
function canReuse(existing: Instance | undefined, node: ComponentNode): existing is Instance {
|
|
41
|
+
if (!existing) return false;
|
|
42
|
+
if (existing.component !== node.component) return false;
|
|
43
|
+
return existing.key === node.key;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function reconcile(
|
|
47
|
+
runtime: Runtime,
|
|
48
|
+
parent: Instance | null,
|
|
49
|
+
existing: Instance | undefined,
|
|
50
|
+
node: ComponentNode,
|
|
51
|
+
options: RenderOptions,
|
|
52
|
+
depth: number,
|
|
53
|
+
dataContext: BindingPath | undefined,
|
|
54
|
+
): Instance {
|
|
55
|
+
if (depth > MAX_DEPTH) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`[textui] component tree exceeded ${MAX_DEPTH} levels at <${node.component}> - ` +
|
|
58
|
+
'a component is almost certainly rendering itself',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let instance: Instance;
|
|
63
|
+
if (canReuse(existing, node)) {
|
|
64
|
+
instance = existing;
|
|
65
|
+
instance.parent = parent;
|
|
66
|
+
} else {
|
|
67
|
+
if (existing) unmountInstance(existing);
|
|
68
|
+
instance = createInstance(runtime, node, parent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The data context has to be in place before props resolve, or a relative
|
|
72
|
+
// path in a templated row has nothing to resolve against.
|
|
73
|
+
instance.dataContext = dataContext ?? parent?.dataContext;
|
|
74
|
+
|
|
75
|
+
const resolved = findFunction(runtime, node);
|
|
76
|
+
instance.kind = resolved.kind;
|
|
77
|
+
instance.fn = resolved.fn;
|
|
78
|
+
instance.host = resolved.host;
|
|
79
|
+
instance.definition = resolved.definition;
|
|
80
|
+
|
|
81
|
+
const previousProps = instance.props;
|
|
82
|
+
updateProps(instance, node);
|
|
83
|
+
|
|
84
|
+
const propsChanged = !shallowEqual(previousProps, instance.props);
|
|
85
|
+
const mustRender = options.force || instance.dirty || !instance.mounted || propsChanged;
|
|
86
|
+
|
|
87
|
+
if (!mustRender) {
|
|
88
|
+
// Clean itself, but something below changed: reconcile the children it
|
|
89
|
+
// produced last time rather than re-running this component.
|
|
90
|
+
if (instance.childDirty) {
|
|
91
|
+
instance.childDirty = false;
|
|
92
|
+
reconcileChildren(runtime, instance, instance.lastChildren, options, depth);
|
|
93
|
+
}
|
|
94
|
+
return instance;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (options.diagnostics && !instance.renderReason) {
|
|
98
|
+
instance.renderReason = !instance.mounted ? 'mount' : propsChanged ? 'props' : 'dirty';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
instance.dirty = false;
|
|
102
|
+
instance.childDirty = false;
|
|
103
|
+
instance.mounted = true;
|
|
104
|
+
instance.error = undefined;
|
|
105
|
+
|
|
106
|
+
subscribeToReads(instance);
|
|
107
|
+
|
|
108
|
+
const childNodes = renderToNodes(runtime, instance, options);
|
|
109
|
+
instance.lastChildren = childNodes;
|
|
110
|
+
reconcileChildren(runtime, instance, childNodes, options, depth);
|
|
111
|
+
|
|
112
|
+
if (!options.diagnostics) instance.renderReason = undefined;
|
|
113
|
+
return instance;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Produce this instance's child nodes, whatever kind it is. */
|
|
117
|
+
function renderToNodes(
|
|
118
|
+
runtime: Runtime,
|
|
119
|
+
instance: Instance,
|
|
120
|
+
options: RenderOptions,
|
|
121
|
+
): { node: ComponentNode; dataContext?: BindingPath }[] {
|
|
122
|
+
const childrenProp = instance.props.children;
|
|
123
|
+
|
|
124
|
+
const wrap = (nodes: ComponentNode[]): { node: ComponentNode }[] =>
|
|
125
|
+
nodes.map((node) => ({ node }));
|
|
126
|
+
|
|
127
|
+
switch (instance.kind) {
|
|
128
|
+
case 'function': {
|
|
129
|
+
const fn = instance.fn;
|
|
130
|
+
if (!fn) return [];
|
|
131
|
+
setCurrentInstance(instance);
|
|
132
|
+
try {
|
|
133
|
+
const output = fn(instance.props);
|
|
134
|
+
return wrap(normalizeChildren(output));
|
|
135
|
+
} catch (err) {
|
|
136
|
+
instance.error = err;
|
|
137
|
+
runtime.onError(err, `render of <${instance.component}>`);
|
|
138
|
+
return wrap(normalizeChildren(fallbackFor(instance, err)));
|
|
139
|
+
} finally {
|
|
140
|
+
setCurrentInstance(null);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'template': {
|
|
145
|
+
const template = instance.definition?.renderer.kind === 'template'
|
|
146
|
+
? instance.definition.renderer.template
|
|
147
|
+
: null;
|
|
148
|
+
if (!template) return [];
|
|
149
|
+
// The template receives this node's props as its data context.
|
|
150
|
+
return wrap([{ ...template, ...stripStructural(instance.props) }]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'host': {
|
|
154
|
+
if (isTemplateChildren(childrenProp)) {
|
|
155
|
+
const expanded = expandTemplate(runtime, childrenProp, instance.dataContext);
|
|
156
|
+
return expanded.nodes.map((node, i) => ({
|
|
157
|
+
node,
|
|
158
|
+
dataContext: expanded.contexts[i],
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
return wrap(normalizeChildren(childrenProp));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case 'missing': {
|
|
165
|
+
resolveLazy(runtime, instance);
|
|
166
|
+
return wrap(normalizeChildren(missingNode(instance, options)));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stripStructural(props: Record<string, unknown>): Record<string, unknown> {
|
|
172
|
+
const { children: _children, ...rest } = props;
|
|
173
|
+
return rest;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A lazily-registered component resolves in the background and re-renders. */
|
|
177
|
+
function resolveLazy(runtime: Runtime, instance: Instance): void {
|
|
178
|
+
const def = instance.definition;
|
|
179
|
+
if (!def || def.renderer.kind !== 'lazy') return;
|
|
180
|
+
void runtime.components
|
|
181
|
+
.resolve(instance.component)
|
|
182
|
+
.then(() => {
|
|
183
|
+
markDirty(instance, 'lazy component resolved');
|
|
184
|
+
runtime.requestRender();
|
|
185
|
+
})
|
|
186
|
+
.catch((err: unknown) => runtime.onError(err, `resolving <${instance.component}>`));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A name that was never registered is a runtime miss, not a compile error -
|
|
191
|
+
* that is the price of the graph being data, so the miss has to be visible
|
|
192
|
+
* rather than silent.
|
|
193
|
+
*/
|
|
194
|
+
function missingNode(instance: Instance, options: RenderOptions): ComponentNode {
|
|
195
|
+
if (instance.definition?.renderer.kind === 'lazy') {
|
|
196
|
+
return { component: 'text', content: '', dim: true };
|
|
197
|
+
}
|
|
198
|
+
const fallback = instance.node.$meta?.fallback;
|
|
199
|
+
if (fallback) {
|
|
200
|
+
// A miss is not a throw, but it is the same question - what does this
|
|
201
|
+
// render instead - so a fallback answers both.
|
|
202
|
+
const message = `no component registered as "${instance.component}"`;
|
|
203
|
+
return applyFallback(fallback, {
|
|
204
|
+
error: new Error(message), message, component: instance.component,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
component: 'text',
|
|
209
|
+
content: options.diagnostics
|
|
210
|
+
? `<${instance.component}?>`
|
|
211
|
+
: `<${instance.component}>`,
|
|
212
|
+
fg: 'danger',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Render a declared fallback.
|
|
218
|
+
*
|
|
219
|
+
* A function fallback is handed the failure. A node fallback is handed it as
|
|
220
|
+
* the `error` and `errorMessage` props, so the graph stays data and a
|
|
221
|
+
* registered component can render the failure without the registry holding a
|
|
222
|
+
* function. Props the fallback declares itself win.
|
|
223
|
+
*/
|
|
224
|
+
function applyFallback(fallback: ErrorFallback, failure: RenderError): ComponentNode {
|
|
225
|
+
if (typeof fallback === 'function') return fallback(failure);
|
|
226
|
+
return { error: failure.error, errorMessage: failure.message, ...fallback };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function fallbackFor(instance: Instance, err: unknown): ComponentNode {
|
|
230
|
+
const fallback = instance.node.$meta?.fallback ?? instance.definition?.fallback;
|
|
231
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
232
|
+
if (fallback) {
|
|
233
|
+
return applyFallback(fallback, { error: err, message, component: instance.component });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
component: 'text',
|
|
238
|
+
content: `${instance.component}: ${message}`,
|
|
239
|
+
fg: 'danger',
|
|
240
|
+
wrap: 'word',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Subscribe to exactly the store paths this instance's props read. Nothing
|
|
246
|
+
* else re-renders it, which is what keeps a hundred-row list from re-rendering
|
|
247
|
+
* because an unrelated counter moved.
|
|
248
|
+
*/
|
|
249
|
+
function subscribeToReads(instance: Instance): void {
|
|
250
|
+
for (const sub of instance.subscriptions) sub.dispose();
|
|
251
|
+
instance.subscriptions = [];
|
|
252
|
+
|
|
253
|
+
if (instance.reads.size === 0) return;
|
|
254
|
+
|
|
255
|
+
for (const path of instance.reads) {
|
|
256
|
+
instance.subscriptions.push(
|
|
257
|
+
instance.runtime.store.subscribe(path as BindingPath, () => {
|
|
258
|
+
markDirty(instance, `store ${path}`);
|
|
259
|
+
instance.runtime.requestRender();
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function reconcileChildren(
|
|
266
|
+
runtime: Runtime,
|
|
267
|
+
instance: Instance,
|
|
268
|
+
childNodes: { node: ComponentNode; dataContext?: BindingPath }[],
|
|
269
|
+
options: RenderOptions,
|
|
270
|
+
depth: number,
|
|
271
|
+
): void {
|
|
272
|
+
const previous = instance.children;
|
|
273
|
+
const byKey = new Map<string | number, Instance>();
|
|
274
|
+
for (let i = 0; i < previous.length; i++) {
|
|
275
|
+
const child = previous[i] as Instance;
|
|
276
|
+
byKey.set(child.key ?? `__index_${i}`, child);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const next: Instance[] = [];
|
|
280
|
+
const used = new Set<Instance>();
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
283
|
+
const entry = childNodes[i] as { node: ComponentNode; dataContext?: BindingPath };
|
|
284
|
+
const key = entry.node.key ?? `__index_${i}`;
|
|
285
|
+
const candidate = byKey.get(key);
|
|
286
|
+
const reusable = candidate && !used.has(candidate) ? candidate : undefined;
|
|
287
|
+
|
|
288
|
+
const child = reconcile(
|
|
289
|
+
runtime, instance, reusable, entry.node, options, depth + 1,
|
|
290
|
+
entry.dataContext ?? instance.dataContext,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (reusable) used.add(reusable);
|
|
294
|
+
next.push(child);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/*
|
|
298
|
+
* `next` as a set, because `includes` inside a loop over `previous` is
|
|
299
|
+
* quadratic - fine for a row of buttons, not for a list with a thousand
|
|
300
|
+
* children, which is exactly where a reconciler has to stay cheap.
|
|
301
|
+
*
|
|
302
|
+
* Built only here, where the answer is needed, rather than alongside `used`.
|
|
303
|
+
*/
|
|
304
|
+
const kept = new Set(next);
|
|
305
|
+
for (const child of previous) {
|
|
306
|
+
if (!used.has(child) && !kept.has(child)) unmountInstance(child);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
instance.children = next;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {
|
|
313
|
+
const aKeys = Object.keys(a);
|
|
314
|
+
const bKeys = Object.keys(b);
|
|
315
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
316
|
+
for (const key of aKeys) {
|
|
317
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Every effect queued during the last render, parents last. */
|
|
323
|
+
export function collectEffects(root: Instance): (() => void)[] {
|
|
324
|
+
const effects: (() => void)[] = [];
|
|
325
|
+
walkInstances(root, (instance) => {
|
|
326
|
+
if (instance.pendingEffects.length === 0) return;
|
|
327
|
+
effects.push(...instance.pendingEffects);
|
|
328
|
+
instance.pendingEffects = [];
|
|
329
|
+
});
|
|
330
|
+
return effects;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function disposeTree(root: Instance): void {
|
|
334
|
+
unmountInstance(root);
|
|
335
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactiveStore, EventBus } from '../types/store.js';
|
|
2
|
+
import type { ComponentRegistry } from '../types/component-registry.js';
|
|
3
|
+
import type { ResolvedTheme } from '../types/theme.js';
|
|
4
|
+
import type { TerminalCapabilities } from '../types/capabilities.js';
|
|
5
|
+
import type { Size } from '../types/geometry.js';
|
|
6
|
+
import type { ServiceContainer } from '../types/services.js';
|
|
7
|
+
import type { FocusManager } from '../types/focus.js';
|
|
8
|
+
import type { LayerManager } from '../types/layer.js';
|
|
9
|
+
import type { AnimationDriver } from '../types/animation.js';
|
|
10
|
+
import type { I18n } from '../types/i18n.js';
|
|
11
|
+
import type { WhenEngine } from '../types/when.js';
|
|
12
|
+
import type { TextUIApp } from '../types/app.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the reconciler and the hooks are allowed to reach.
|
|
16
|
+
*
|
|
17
|
+
* Narrower than the app on purpose: the render tree should depend on the few
|
|
18
|
+
* things it actually uses, so a component can be rendered by the test harness
|
|
19
|
+
* or the static renderer without an application, a terminal or a frame loop.
|
|
20
|
+
*/
|
|
21
|
+
export interface Runtime {
|
|
22
|
+
store: ReactiveStore;
|
|
23
|
+
events: EventBus;
|
|
24
|
+
when: WhenEngine;
|
|
25
|
+
components: ComponentRegistry;
|
|
26
|
+
services: ServiceContainer;
|
|
27
|
+
focus: FocusManager;
|
|
28
|
+
layers: LayerManager;
|
|
29
|
+
animation: AnimationDriver;
|
|
30
|
+
i18n: I18n;
|
|
31
|
+
|
|
32
|
+
theme(): ResolvedTheme;
|
|
33
|
+
capabilities(): TerminalCapabilities;
|
|
34
|
+
size(): Size;
|
|
35
|
+
|
|
36
|
+
execute(id: string, args?: Record<string, unknown>): unknown;
|
|
37
|
+
emit(path: string, payload?: unknown): void;
|
|
38
|
+
|
|
39
|
+
/** Ask for another frame. Coalesced by the scheduler. */
|
|
40
|
+
requestRender(): void;
|
|
41
|
+
|
|
42
|
+
/** The application, when there is one. Null under the static renderer. */
|
|
43
|
+
app(): TextUIApp | null;
|
|
44
|
+
|
|
45
|
+
/** Where render-time errors go. */
|
|
46
|
+
onError(err: unknown, context: string): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Style, StatefulStyle, StyleInput, BorderSpec, BorderStyle, StyleColor } from '../types/style.js';
|
|
2
|
+
import type { ResolvedTheme } from '../types/theme.js';
|
|
3
|
+
import type { Edges } from '../types/geometry.js';
|
|
4
|
+
import type { Color } from '../types/cells.js';
|
|
5
|
+
import type { BorderChars } from '../types/style.js';
|
|
6
|
+
import {
|
|
7
|
+
ATTR_BLINK, ATTR_BOLD, ATTR_DIM, ATTR_INVERSE, ATTR_ITALIC,
|
|
8
|
+
ATTR_STRIKE, ATTR_UNDERLINE,
|
|
9
|
+
} from '../types/cells.js';
|
|
10
|
+
import { packColor, type PackedColor } from '../render/color.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Style resolution.
|
|
14
|
+
*
|
|
15
|
+
* Five sources, merged in one fixed order so the answer to "why is this blue"
|
|
16
|
+
* is always the same walk: the theme's entry for this component, the
|
|
17
|
+
* component's own default, convenience props written inline, the `style` prop,
|
|
18
|
+
* and finally the state overlay for focus, hover, active, selected, disabled.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const STYLE_KEYS = new Set<string>([
|
|
22
|
+
'display', 'direction', 'gap', 'columnGap', 'rowGap', 'flexWrap', 'padding', 'margin',
|
|
23
|
+
'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight',
|
|
24
|
+
'flex', 'shrink', 'basis', 'align', 'alignSelf', 'justify',
|
|
25
|
+
'position', 'top', 'right', 'bottom', 'left', 'zIndex',
|
|
26
|
+
'overflow', 'overflowX', 'overflowY',
|
|
27
|
+
'fg', 'bg', 'bold', 'dim', 'italic', 'underline', 'inverse', 'strike', 'blink',
|
|
28
|
+
'border', 'wrap', 'textAlign', 'fill', 'scrim', 'scrimStrength',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export interface InteractionState {
|
|
32
|
+
focused: boolean;
|
|
33
|
+
hovered: boolean;
|
|
34
|
+
active: boolean;
|
|
35
|
+
selected: boolean;
|
|
36
|
+
disabled: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const NO_INTERACTION: InteractionState = {
|
|
40
|
+
focused: false, hovered: false, active: false, selected: false, disabled: false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function isStateful(value: Style | StatefulStyle): value is StatefulStyle {
|
|
44
|
+
return (
|
|
45
|
+
'base' in value || 'focus' in value || 'hover' in value ||
|
|
46
|
+
'active' in value || 'selected' in value || 'disabled' in value
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mergeStyles(...styles: (Style | undefined)[]): Style {
|
|
51
|
+
const out: Style = {};
|
|
52
|
+
for (const style of styles) {
|
|
53
|
+
if (!style) continue;
|
|
54
|
+
Object.assign(out, style);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Flatten a `style` prop, applying state overlays in a fixed order. */
|
|
60
|
+
export function flattenStyleInput(input: StyleInput | undefined, state: InteractionState): Style {
|
|
61
|
+
if (!input) return {};
|
|
62
|
+
|
|
63
|
+
if (Array.isArray(input)) {
|
|
64
|
+
return mergeStyles(...input.map((item) => (item ? flattenStyleInput(item, state) : undefined)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!isStateful(input)) return input;
|
|
68
|
+
|
|
69
|
+
// Order matters: selected loses to active, active loses to focus, and
|
|
70
|
+
// disabled wins over everything - a disabled control is not focusable.
|
|
71
|
+
return mergeStyles(
|
|
72
|
+
input.base,
|
|
73
|
+
state.selected ? input.selected : undefined,
|
|
74
|
+
state.hovered ? input.hover : undefined,
|
|
75
|
+
state.active ? input.active : undefined,
|
|
76
|
+
state.focused ? input.focus : undefined,
|
|
77
|
+
state.disabled ? input.disabled : undefined,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Pick the style keys written inline as convenience props. */
|
|
82
|
+
export function styleFromProps(props: Record<string, unknown>): Style {
|
|
83
|
+
const out: Record<string, unknown> = {};
|
|
84
|
+
for (const key of Object.keys(props)) {
|
|
85
|
+
if (STYLE_KEYS.has(key) && props[key] !== undefined) out[key] = props[key];
|
|
86
|
+
}
|
|
87
|
+
return out as Style;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveStyle(
|
|
91
|
+
props: Record<string, unknown>,
|
|
92
|
+
theme: ResolvedTheme,
|
|
93
|
+
component: string,
|
|
94
|
+
defaultStyle: Style | undefined,
|
|
95
|
+
state: InteractionState,
|
|
96
|
+
): Style {
|
|
97
|
+
const variants: string[] = [];
|
|
98
|
+
if (typeof props.variant === 'string') variants.push(props.variant);
|
|
99
|
+
if (typeof props.tone === 'string') variants.push(props.tone);
|
|
100
|
+
if (typeof props.size === 'string') variants.push(props.size);
|
|
101
|
+
|
|
102
|
+
return mergeStyles(
|
|
103
|
+
theme.styleFor(component, variants),
|
|
104
|
+
defaultStyle,
|
|
105
|
+
styleFromProps(props),
|
|
106
|
+
flattenStyleInput(props.style as StyleInput | undefined, state),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ------------------------------------------------------------------ colour
|
|
111
|
+
|
|
112
|
+
/** A token name, a literal colour, or nothing. */
|
|
113
|
+
export function resolveColor(
|
|
114
|
+
value: StyleColor | undefined,
|
|
115
|
+
theme: ResolvedTheme,
|
|
116
|
+
fallback: Color = 'default',
|
|
117
|
+
): Color {
|
|
118
|
+
if (value === undefined) return fallback;
|
|
119
|
+
return theme.color(value as string);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function packStyleColor(
|
|
123
|
+
value: StyleColor | undefined,
|
|
124
|
+
theme: ResolvedTheme,
|
|
125
|
+
fallback: Color = 'default',
|
|
126
|
+
): PackedColor {
|
|
127
|
+
return packColor(resolveColor(value, theme, fallback));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function attrsFromStyle(style: Style): number {
|
|
131
|
+
let attrs = 0;
|
|
132
|
+
if (style.bold) attrs |= ATTR_BOLD;
|
|
133
|
+
if (style.dim) attrs |= ATTR_DIM;
|
|
134
|
+
if (style.italic) attrs |= ATTR_ITALIC;
|
|
135
|
+
if (style.underline) attrs |= ATTR_UNDERLINE;
|
|
136
|
+
if (style.inverse) attrs |= ATTR_INVERSE;
|
|
137
|
+
if (style.strike) attrs |= ATTR_STRIKE;
|
|
138
|
+
if (style.blink) attrs |= ATTR_BLINK;
|
|
139
|
+
return attrs;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ------------------------------------------------------------------ border
|
|
143
|
+
|
|
144
|
+
export interface ResolvedBorder {
|
|
145
|
+
style: BorderStyle;
|
|
146
|
+
chars: BorderChars;
|
|
147
|
+
color: StyleColor | undefined;
|
|
148
|
+
/** Per-edge overrides. Undefined here means "use `color`". */
|
|
149
|
+
colors: { top?: StyleColor; right?: StyleColor; bottom?: StyleColor; left?: StyleColor };
|
|
150
|
+
dim: boolean;
|
|
151
|
+
sides: { top: boolean; right: boolean; bottom: boolean; left: boolean };
|
|
152
|
+
edges: Edges;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const NO_BORDER: ResolvedBorder = {
|
|
156
|
+
style: 'none',
|
|
157
|
+
chars: {
|
|
158
|
+
topLeft: ' ', top: ' ', topRight: ' ', right: ' ', bottomRight: ' ',
|
|
159
|
+
bottom: ' ', bottomLeft: ' ', left: ' ', cross: ' ',
|
|
160
|
+
teeTop: ' ', teeBottom: ' ', teeLeft: ' ', teeRight: ' ',
|
|
161
|
+
},
|
|
162
|
+
color: undefined,
|
|
163
|
+
colors: {},
|
|
164
|
+
dim: false,
|
|
165
|
+
sides: { top: false, right: false, bottom: false, left: false },
|
|
166
|
+
edges: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export function resolveBorder(spec: BorderSpec | undefined, theme: ResolvedTheme): ResolvedBorder {
|
|
170
|
+
if (spec === undefined) return NO_BORDER;
|
|
171
|
+
|
|
172
|
+
const style = typeof spec === 'string' ? spec : spec.style ?? theme.border;
|
|
173
|
+
if (style === 'none') return NO_BORDER;
|
|
174
|
+
|
|
175
|
+
// Naming any side means naming all of them. `sides: { left: true }` has to
|
|
176
|
+
// mean a left rule and nothing else - if unspecified sides defaulted to
|
|
177
|
+
// true, every panel divider in every shell would draw a full box instead.
|
|
178
|
+
const sidesSpec = typeof spec === 'string' ? undefined : spec.sides;
|
|
179
|
+
const sides = sidesSpec
|
|
180
|
+
? {
|
|
181
|
+
top: sidesSpec.top === true,
|
|
182
|
+
right: sidesSpec.right === true,
|
|
183
|
+
bottom: sidesSpec.bottom === true,
|
|
184
|
+
left: sidesSpec.left === true,
|
|
185
|
+
}
|
|
186
|
+
: { top: true, right: true, bottom: true, left: true };
|
|
187
|
+
|
|
188
|
+
const base = theme.borderChars(style);
|
|
189
|
+
const chars = typeof spec === 'string' || !spec.chars ? base : { ...base, ...spec.chars };
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
style,
|
|
193
|
+
chars,
|
|
194
|
+
color: typeof spec === 'string' ? undefined : spec.color,
|
|
195
|
+
colors: typeof spec === 'string' ? {} : spec.colors ?? {},
|
|
196
|
+
dim: typeof spec !== 'string' && spec.dim === true,
|
|
197
|
+
sides,
|
|
198
|
+
edges: {
|
|
199
|
+
top: sides.top ? 1 : 0,
|
|
200
|
+
right: sides.right ? 1 : 0,
|
|
201
|
+
bottom: sides.bottom ? 1 : 0,
|
|
202
|
+
left: sides.left ? 1 : 0,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { BorderChars, BorderStyle } from '../types/style.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Border glyph sets.
|
|
5
|
+
*
|
|
6
|
+
* `ascii` is not a fallback nobody sees - it is what an `unicode: 'ascii'`
|
|
7
|
+
* terminal actually gets, so it has to look deliberate rather than broken.
|
|
8
|
+
*/
|
|
9
|
+
export const BORDER_SETS: Record<BorderStyle, BorderChars> = {
|
|
10
|
+
none: {
|
|
11
|
+
topLeft: ' ', top: ' ', topRight: ' ',
|
|
12
|
+
right: ' ', bottomRight: ' ', bottom: ' ',
|
|
13
|
+
bottomLeft: ' ', left: ' ',
|
|
14
|
+
cross: ' ', teeTop: ' ', teeBottom: ' ', teeLeft: ' ', teeRight: ' ',
|
|
15
|
+
},
|
|
16
|
+
single: {
|
|
17
|
+
topLeft: '┌', top: '─', topRight: '┐',
|
|
18
|
+
right: '│', bottomRight: '┘', bottom: '─',
|
|
19
|
+
bottomLeft: '└', left: '│',
|
|
20
|
+
cross: '┼', teeTop: '┬', teeBottom: '┴', teeLeft: '┤', teeRight: '├',
|
|
21
|
+
},
|
|
22
|
+
round: {
|
|
23
|
+
topLeft: '╭', top: '─', topRight: '╮',
|
|
24
|
+
right: '│', bottomRight: '╯', bottom: '─',
|
|
25
|
+
bottomLeft: '╰', left: '│',
|
|
26
|
+
cross: '┼', teeTop: '┬', teeBottom: '┴', teeLeft: '┤', teeRight: '├',
|
|
27
|
+
},
|
|
28
|
+
double: {
|
|
29
|
+
topLeft: '╔', top: '═', topRight: '╗',
|
|
30
|
+
right: '║', bottomRight: '╝', bottom: '═',
|
|
31
|
+
bottomLeft: '╚', left: '║',
|
|
32
|
+
cross: '╬', teeTop: '╦', teeBottom: '╩', teeLeft: '╣', teeRight: '╠',
|
|
33
|
+
},
|
|
34
|
+
bold: {
|
|
35
|
+
topLeft: '┏', top: '━', topRight: '┓',
|
|
36
|
+
right: '┃', bottomRight: '┛', bottom: '━',
|
|
37
|
+
bottomLeft: '┗', left: '┃',
|
|
38
|
+
cross: '╋', teeTop: '┳', teeBottom: '┻', teeLeft: '┫', teeRight: '┣',
|
|
39
|
+
},
|
|
40
|
+
thick: {
|
|
41
|
+
topLeft: '▛', top: '▀', topRight: '▜',
|
|
42
|
+
right: '▐', bottomRight: '▟', bottom: '▄',
|
|
43
|
+
bottomLeft: '▙', left: '▌',
|
|
44
|
+
cross: '┼', teeTop: '┬', teeBottom: '┴', teeLeft: '┤', teeRight: '├',
|
|
45
|
+
},
|
|
46
|
+
half: {
|
|
47
|
+
topLeft: '▗', top: '▄', topRight: '▖',
|
|
48
|
+
right: '▌', bottomRight: '▘', bottom: '▀',
|
|
49
|
+
bottomLeft: '▝', left: '▐',
|
|
50
|
+
cross: '┼', teeTop: '┬', teeBottom: '┴', teeLeft: '┤', teeRight: '├',
|
|
51
|
+
},
|
|
52
|
+
dashed: {
|
|
53
|
+
topLeft: '┌', top: '╌', topRight: '┐',
|
|
54
|
+
right: '╎', bottomRight: '┘', bottom: '╌',
|
|
55
|
+
bottomLeft: '└', left: '╎',
|
|
56
|
+
cross: '┼', teeTop: '┬', teeBottom: '┴', teeLeft: '┤', teeRight: '├',
|
|
57
|
+
},
|
|
58
|
+
ascii: {
|
|
59
|
+
topLeft: '+', top: '-', topRight: '+',
|
|
60
|
+
right: '|', bottomRight: '+', bottom: '-',
|
|
61
|
+
bottomLeft: '+', left: '|',
|
|
62
|
+
cross: '+', teeTop: '+', teeBottom: '+', teeLeft: '+', teeRight: '+',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** What each style degrades to when the terminal cannot draw it. */
|
|
67
|
+
const ASCII_FALLBACK: Record<BorderStyle, BorderStyle> = {
|
|
68
|
+
none: 'none',
|
|
69
|
+
single: 'ascii',
|
|
70
|
+
round: 'ascii',
|
|
71
|
+
double: 'ascii',
|
|
72
|
+
bold: 'ascii',
|
|
73
|
+
thick: 'ascii',
|
|
74
|
+
half: 'ascii',
|
|
75
|
+
dashed: 'ascii',
|
|
76
|
+
ascii: 'ascii',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Block-drawing styles need more than the BMP box-drawing range. */
|
|
80
|
+
const BMP_FALLBACK: Partial<Record<BorderStyle, BorderStyle>> = {
|
|
81
|
+
thick: 'single',
|
|
82
|
+
half: 'single',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function borderCharsFor(
|
|
86
|
+
style: BorderStyle,
|
|
87
|
+
unicode: 'ascii' | 'bmp' | 'full',
|
|
88
|
+
): BorderChars {
|
|
89
|
+
if (unicode === 'ascii') return BORDER_SETS[ASCII_FALLBACK[style]] as BorderChars;
|
|
90
|
+
if (unicode === 'bmp') {
|
|
91
|
+
const fallback = BMP_FALLBACK[style];
|
|
92
|
+
return BORDER_SETS[fallback ?? style] as BorderChars;
|
|
93
|
+
}
|
|
94
|
+
return BORDER_SETS[style] as BorderChars;
|
|
95
|
+
}
|