@stevezhou/sisu 0.1.5 → 0.1.7

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/dist/logo.js CHANGED
@@ -4,6 +4,8 @@ exports.sisuTreeLines = exports.sisuTreeArt = exports.sisuMarkLines = exports.si
4
4
  exports.sisuMobiusArt = sisuMobiusArt;
5
5
  exports.sisuWordmark = sisuWordmark;
6
6
  exports.displayWidth = displayWidth;
7
+ exports.sisuSplashFrame = sisuSplashFrame;
8
+ exports.sisuSplashHeight = sisuSplashHeight;
7
9
  exports.sisuSplash = sisuSplash;
8
10
  exports.sisuWelcomeCopy = sisuWelcomeCopy;
9
11
  exports.sisuBanner = sisuBanner;
@@ -16,19 +18,10 @@ Object.defineProperty(exports, "sisuTreeArt", { enumerable: true, get: function
16
18
  Object.defineProperty(exports, "sisuTreeLines", { enumerable: true, get: function () { return tree_1.sisuTreeLines; } });
17
19
  const mark_2 = require("./mark");
18
20
  const mobius_1 = require("./mobius");
19
- const tree_2 = require("./tree");
20
21
  const RESET = '\x1b[0m';
21
- const TERRACOTTA = '\x1b[38;2;184;90;58m';
22
22
  const INK = '\x1b[38;2;220;214;204m';
23
23
  const INK_DIM = '\x1b[38;2;140;132;120m';
24
24
  const HEADLINE = '思有所溯';
25
- const TAGLINE = '为科研而生的工作台——记录退到思考之后,不打扰任何一念。';
26
- const COLOPHON = [
27
- '对话、文献与数据,落笔成档',
28
- '每个结论,都能沿枝回到它的根',
29
- '众人的知识彼此嫁接,长成一片林',
30
- ];
31
- const NUMERALS = ['一', '二', '三'];
32
25
  function sisuMobiusArt(columns = 80, phase = 0, color = false) {
33
26
  return (0, mobius_1.renderMobiusFrame)({
34
27
  cols: (0, mobius_1.mobiusFrameWidth)(columns),
@@ -76,17 +69,6 @@ function lockup(color) {
76
69
  const latin = paint('SiSu', INK_DIM, color);
77
70
  return [`${inf} ${name}`, ` ${latin}`];
78
71
  }
79
- function colophon(columns, color) {
80
- const lines = [];
81
- for (let i = 0; i < COLOPHON.length; i += 1) {
82
- const num = paint(NUMERALS[i], TERRACOTTA, color);
83
- const body = paint(COLOPHON[i], INK_DIM, color);
84
- const line = `${num} ${body}`;
85
- if (displayWidth(line) + 2 <= columns)
86
- lines.push(line);
87
- }
88
- return lines;
89
- }
90
72
  function padLeft(text, n) {
91
73
  return `${' '.repeat(Math.max(0, n))}${text}`;
92
74
  }
@@ -95,61 +77,36 @@ function center(text, width) {
95
77
  const pad = Math.max(0, Math.floor((width - vis) / 2));
96
78
  return padLeft(text, pad);
97
79
  }
98
- function copyColumn(columns, color, compact) {
99
- const indent = 2;
100
- const lines = [
101
- ...lockup(color).map((line) => padLeft(line, indent)),
102
- '',
103
- padLeft(paint(HEADLINE, INK, color), indent),
104
- ];
105
- if (!compact && displayWidth(TAGLINE) + indent <= columns) {
106
- lines.push(padLeft(paint(TAGLINE, INK_DIM, color), indent));
107
- }
108
- if (!compact) {
109
- const notes = colophon(columns, color);
110
- if (notes.length) {
111
- lines.push('');
112
- for (const note of notes)
113
- lines.push(padLeft(note, indent));
114
- }
115
- }
116
- return lines;
117
- }
118
- function treeColumn(columns, color) {
119
- const raw = (0, tree_2.sisuTreeLines)(columns);
120
- const painted = (0, tree_2.sisuTreeArt)(columns, color).split('\n');
121
- const treeWidth = raw.reduce((max, line) => Math.max(max, line.length), 0);
122
- const pad = Math.max(0, Math.floor((columns - treeWidth) / 2));
123
- return painted.map((line, i) => {
124
- const vis = raw[i]?.length ?? displayWidth(line);
125
- return padLeft(`${line}${' '.repeat(Math.max(0, treeWidth - vis))}`, pad);
126
- });
80
+ function centerBlock(lines, width) {
81
+ return lines.map((line) => center(line, width));
127
82
  }
83
+ const STILL_PHASE = 0.85;
128
84
  /**
129
- * Login-page splash: lockup + 思有所溯 + 题跋 over the 溯源之树.
130
- * Side-by-side on a wide terminal; stacked when the sheet is narrow.
85
+ * Möbius splash. `phase` slides the half-twist around the single face.
86
+ * Line count is stable for a given width so the intro can rewrite in place.
131
87
  */
132
- function sisuSplash(columns = 80, color = true) {
88
+ function sisuSplashFrame(columns = 80, color = true, phase = STILL_PHASE, _grow = 1) {
133
89
  const width = Math.max(20, Math.floor(columns));
134
- const compact = width < 52;
135
- const copy = copyColumn(width, color, compact);
136
- const tree = treeColumn(width, color);
137
- if (width >= 100) {
138
- const gap = 3;
139
- const leftW = Math.max(...copy.map((line) => displayWidth(line)));
140
- const rightW = width - leftW - gap;
141
- const right = treeColumn(rightW, color);
142
- const rows = Math.max(copy.length, right.length);
143
- const merged = [''];
144
- for (let i = 0; i < rows; i += 1) {
145
- const left = copy[i] ?? '';
146
- const pad = Math.max(0, leftW + gap - displayWidth(left));
147
- merged.push(`${left}${' '.repeat(pad)}${right[i] ?? ''}`);
148
- }
149
- merged.push('');
150
- return merged.join('\n');
151
- }
152
- return ['', ...copy, '', ...tree, ''].join('\n');
90
+ const ringW = (0, mobius_1.mobiusFrameWidth)(width);
91
+ const ring = (0, mobius_1.renderMobiusFrame)({
92
+ cols: ringW,
93
+ rows: (0, mobius_1.mobiusFrameHeight)(width),
94
+ phase,
95
+ color,
96
+ }).split('\n');
97
+ const pad = Math.max(0, Math.floor((width - ringW) / 2));
98
+ const art = ring.map((line) => padLeft(line, pad));
99
+ const caption = centerBlock([
100
+ `${infinityMark(color)} ${paint('思溯', INK, color)} ${paint('SiSu', INK_DIM, color)}`,
101
+ paint(HEADLINE, INK, color),
102
+ ], width);
103
+ return [...art, '', ...caption].join('\n');
104
+ }
105
+ function sisuSplashHeight(columns = 80) {
106
+ return sisuSplashFrame(columns, false).split('\n').length;
107
+ }
108
+ function sisuSplash(columns = 80, color = true) {
109
+ return sisuSplashFrame(columns, color, STILL_PHASE, 1);
153
110
  }
154
111
  function sisuWelcomeCopy(guest, color = true) {
155
112
  const lines = [
@@ -165,8 +122,8 @@ function sisuWelcomeCopy(guest, color = true) {
165
122
  }
166
123
  return lines;
167
124
  }
168
- function sisuBanner(columns = 80, _phase = 0, color = false) {
169
- return sisuSplash(columns, color);
125
+ function sisuBanner(columns = 80, phase = STILL_PHASE, color = false) {
126
+ return sisuSplashFrame(columns, color, phase, 1);
170
127
  }
171
128
  function stripAnsi(text) {
172
129
  return text.replace(/\x1b\[[0-9;]*m/g, '');
package/dist/main.js CHANGED
@@ -28,7 +28,7 @@ Usage:
28
28
  sisu history
29
29
  sisu thread <conversation-id>
30
30
  sisu training --on|--off
31
- sisu interactive TUI (welcome tree)
31
+ sisu interactive TUI (Möbius splash)
32
32
  sisu help
33
33
 
34
34
  Auth and workspaces live in $SISU_HOME (default ~/.sisu), shared with Desktop.
package/dist/mobius.js CHANGED
@@ -1,22 +1,23 @@
1
1
  "use strict";
2
- /** Volumetric Möbius ring: half-twist ribbon with thickness, perspective, and lighting. */
2
+ /** Volumetric Möbius: lemniscate centerline, half-twist ribbon, traveling glint. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.mobiusRgb = mobiusRgb;
5
5
  exports.mobiusPoint = mobiusPoint;
6
+ exports.logoPoint = logoPoint;
6
7
  exports.renderMobiusFrame = renderMobiusFrame;
7
8
  exports.mobiusFrameHeight = mobiusFrameHeight;
8
9
  exports.mobiusFrameWidth = mobiusFrameWidth;
9
10
  const RESET = '\x1b[0m';
10
11
  const SHADE_RAMP = ' .:-=+*#%@';
11
- const RADIUS = 1.88;
12
- const HALF_WIDTH = 0.34;
13
- const THICKNESS = 0.08;
12
+ const SCALE = 1.72;
13
+ const HALF_WIDTH = 0.3;
14
+ const THICKNESS = 0.045;
14
15
  const CELL_ASPECT = 0.5;
15
- const CAM = 8.6;
16
- const FOCAL = 5.1;
17
- const KEY = norm([-0.58, 0.78, 0.24]);
18
- const FILL = norm([0.48, 0.12, 0.86]);
19
- const VIEW = [0, 0.12, 1];
16
+ const CAM = 6.4;
17
+ const FOCAL = 5.2;
18
+ const KEY = norm([-0.55, 0.7, 0.45]);
19
+ const FILL = norm([0.52, 0.1, 0.85]);
20
+ const VIEW = [0, 0.06, 1];
20
21
  function clamp(value, lo, hi) {
21
22
  return Math.max(lo, Math.min(hi, value));
22
23
  }
@@ -37,7 +38,7 @@ function cross(a, b) {
37
38
  function dot(a, b) {
38
39
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
39
40
  }
40
- /** Brand gradient along the band: blue → purple → gold, matching sisu-mark. */
41
+ /** Brand gradient along the band: blue → purple → gold. */
41
42
  function mobiusRgb(t) {
42
43
  const u = ((t / (Math.PI * 2)) % 1 + 1) % 1;
43
44
  if (u < 0.5) {
@@ -68,24 +69,10 @@ function rotateY(x, z, angle) {
68
69
  const s = Math.sin(angle);
69
70
  return [x * c + z * s, -x * s + z * c];
70
71
  }
71
- function rotateZ(x, y, angle) {
72
- const c = Math.cos(angle);
73
- const s = Math.sin(angle);
74
- return [x * c - y * s, x * s + y * c];
75
- }
76
- function transform(x, y, z, tilt, spin) {
77
- // Spin around the ring axis so the half-twist travels; then a fixed 3/4 view.
78
- ;
79
- [x, y] = rotateZ(x, y, spin);
80
- [y, z] = rotateX(y, z, tilt);
81
- [x, z] = rotateY(x, z, 0.32);
82
- return [x, y, z];
83
- }
84
72
  /**
85
- * Standard Möbius strip: a ring of radius R with a half-twist in the width.
86
- * After one full trip around θ, the normal flips — one-sided surface.
73
+ * Circular Möbius (unit tests): after one full trip around θ the width flips.
87
74
  */
88
- function mobiusPoint(theta, width, radius = RADIUS, halfWidth = HALF_WIDTH) {
75
+ function mobiusPoint(theta, width, radius = 1.88, halfWidth = 0.34) {
89
76
  const twist = theta / 2;
90
77
  const ring = radius + width * halfWidth * Math.cos(twist);
91
78
  return [
@@ -94,24 +81,45 @@ function mobiusPoint(theta, width, radius = RADIUS, halfWidth = HALF_WIDTH) {
94
81
  width * halfWidth * Math.sin(twist),
95
82
  ];
96
83
  }
97
- function mobiusNormal(theta, width, radius = RADIUS, halfWidth = HALF_WIDTH) {
98
- const twist = theta / 2;
99
- const c = Math.cos(theta);
100
- const s = Math.sin(theta);
101
- const c2 = Math.cos(twist);
102
- const s2 = Math.sin(twist);
103
- const ring = radius + width * halfWidth * c2;
104
- const dTheta = [
105
- -ring * s - width * halfWidth * 0.5 * s2 * c,
106
- ring * c - width * halfWidth * 0.5 * s2 * s,
107
- width * halfWidth * 0.5 * c2,
108
- ];
109
- const dWidth = [
110
- halfWidth * c2 * c,
111
- halfWidth * c2 * s,
112
- halfWidth * s2,
113
- ];
114
- return norm(cross(dTheta, dWidth));
84
+ /** Gerono lemniscate the of the SiSu mark. */
85
+ function lemniscate(t) {
86
+ return [SCALE * Math.sin(t), SCALE * Math.sin(t) * Math.cos(t), 0];
87
+ }
88
+ function lemniscateTangent(t) {
89
+ return norm([Math.cos(t), Math.cos(2 * t), 0]);
90
+ }
91
+ /**
92
+ * Ribbon around the ∞: width offset in a frame that half-twists once per lap.
93
+ * phase slides the twist so the single face travels without a seam.
94
+ */
95
+ function logoPoint(t, width, phase = 0) {
96
+ const [cx, cy, cz] = lemniscate(t);
97
+ const tangent = lemniscateTangent(t);
98
+ const binormal = [0, 0, 1];
99
+ const normal = norm(cross(binormal, tangent));
100
+ const twist = (t + phase) / 2;
101
+ const ox = normal[0] * Math.cos(twist) + binormal[0] * Math.sin(twist);
102
+ const oy = normal[1] * Math.cos(twist) + binormal[1] * Math.sin(twist);
103
+ const oz = normal[2] * Math.cos(twist) + binormal[2] * Math.sin(twist);
104
+ const w = width * HALF_WIDTH;
105
+ return [cx + ox * w, cy + oy * w, cz + oz * w];
106
+ }
107
+ function logoNormal(t, phase) {
108
+ const tangent = lemniscateTangent(t);
109
+ const binormal = [0, 0, 1];
110
+ const normal = norm(cross(binormal, tangent));
111
+ const twist = (t + phase) / 2;
112
+ return norm([
113
+ normal[0] * Math.cos(twist) + binormal[0] * Math.sin(twist),
114
+ normal[1] * Math.cos(twist) + binormal[1] * Math.sin(twist),
115
+ normal[2] * Math.cos(twist) + binormal[2] * Math.sin(twist),
116
+ ]);
117
+ }
118
+ function transform(x, y, z, tilt, yaw) {
119
+ ;
120
+ [y, z] = rotateX(y, z, tilt);
121
+ [x, z] = rotateY(x, z, yaw);
122
+ return [x, y, z];
115
123
  }
116
124
  function shadeChar(shade) {
117
125
  return SHADE_RAMP[Math.round(clamp(shade, 0, 1) * (SHADE_RAMP.length - 1))] || '@';
@@ -122,125 +130,93 @@ function renderMobiusFrame(options = {}) {
122
130
  const phase = options.phase ?? 0;
123
131
  const color = options.color !== false;
124
132
  const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
125
- const tilt = 0.88;
126
- const samples = [];
127
- const thetaSteps = 580;
133
+ const tilt = 0.42;
134
+ const yaw = 0.18 * Math.sin(phase * 0.5);
135
+ const thetaSteps = 520;
136
+ const scale = Math.min((cols - 2) / 4.15, (rows - 2) / (2.15 * CELL_ASPECT));
128
137
  for (let i = 0; i < thetaSteps; i += 1) {
129
138
  const theta = (i / thetaSteps) * Math.PI * 2;
130
139
  for (let j = -4; j <= 4; j += 1) {
131
140
  const width = j / 4;
132
- const base = mobiusPoint(theta, width);
133
- const n0 = mobiusNormal(theta, width);
134
- // Faces of the thin solid, plus the mid-surface; skip the interior fill.
141
+ const base = logoPoint(theta, width, phase);
142
+ const n0 = logoNormal(theta, phase);
135
143
  const thicks = j === -4 || j === 4 ? [-1, 0, 1] : [-1, 1];
136
- for (const k of thicks) {
137
- const thick = k;
144
+ for (const thick of thicks) {
138
145
  let x = base[0] + n0[0] * thick * THICKNESS;
139
146
  let y = base[1] + n0[1] * thick * THICKNESS;
140
147
  let z = base[2] + n0[2] * thick * THICKNESS;
141
- let nx = n0[0];
142
- let ny = n0[1];
143
- let nz = n0[2];
144
- if (thick < 0) {
145
- nx = -nx;
146
- ny = -ny;
147
- nz = -nz;
148
- }
149
- ;
150
- [x, y, z] = transform(x, y, z, tilt, phase);
151
- [nx, ny, nz] = transform(nx, ny, nz, tilt, phase);
148
+ let nx = thick < 0 ? -n0[0] : n0[0];
149
+ let ny = thick < 0 ? -n0[1] : n0[1];
150
+ let nz = thick < 0 ? -n0[2] : n0[2];
151
+ [x, y, z] = transform(x, y, z, tilt, yaw);
152
+ [nx, ny, nz] = transform(nx, ny, nz, tilt, yaw);
153
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
154
+ continue;
152
155
  let normal = [nx, ny, nz];
153
156
  const facing = dot(normal, VIEW);
154
157
  const back = facing < 0;
155
158
  if (back)
156
159
  normal = [-normal[0], -normal[1], -normal[2]];
157
160
  const depth = CAM - z;
158
- if (depth < 0.35)
161
+ if (depth < 0.4 || !Number.isFinite(depth))
159
162
  continue;
160
163
  const px = (x * FOCAL) / depth;
161
164
  const py = (y * FOCAL) / depth;
165
+ if (!Number.isFinite(px) || !Number.isFinite(py))
166
+ continue;
162
167
  const lambert = Math.max(0, dot(normal, KEY));
163
168
  const fill = Math.max(0, dot(normal, FILL)) * 0.2;
164
- const camFill = Math.max(0, dot(normal, VIEW)) * 0.22;
169
+ const camFill = Math.max(0, dot(normal, VIEW)) * 0.2;
165
170
  const half = norm([KEY[0] + VIEW[0], KEY[1] + VIEW[1], KEY[2] + VIEW[2]]);
166
- const spec = Math.pow(Math.max(0, dot(normal, half)), 28);
167
- const edge = Math.max(Math.abs(width), Math.abs(thick));
168
- const rim = Math.pow(1 - Math.abs(dot(normal, VIEW)), 2.2) * (0.1 + 0.5 * Math.pow(edge, 2));
169
- const ambient = back ? 0.09 : 0.18;
170
- const lit = ambient + lambert * (back ? 0.3 : 0.74) + fill + camFill + spec * 0.5 + rim;
171
- const shade = clamp(Math.pow(lit, 1.2), 0.07, 1);
172
- samples.push({
173
- px,
174
- py,
175
- depth,
176
- shade,
177
- spec,
178
- t: theta + (back ? Math.PI : 0),
179
- });
171
+ const spec = Math.pow(Math.max(0, dot(normal, half)), 26);
172
+ const edge = Math.abs(width);
173
+ const rim = Math.pow(1 - Math.abs(dot(normal, VIEW)), 2.2) * (0.12 + 0.45 * edge * edge);
174
+ const rider = Math.pow(Math.max(0, Math.cos(theta - phase)), 18);
175
+ const ambient = back ? 0.1 : 0.2;
176
+ const lit = ambient + lambert * (back ? 0.32 : 0.7) + fill + camFill + spec * 0.45 + rim + rider * 0.55;
177
+ const shade = clamp(Math.pow(lit, 1.12), 0.07, 1);
178
+ const fog = 0.5 + 0.5 * clamp(1 - (depth - 5.2) / 3.4, 0, 1);
179
+ const col = Math.round(px * scale + (cols - 1) / 2);
180
+ const row = Math.round(-py * scale * CELL_ASPECT + (rows - 1) / 2);
181
+ if (col < 0 || col >= cols || row < 0 || row >= rows)
182
+ continue;
183
+ const prev = grid[row][col];
184
+ if (!prev || depth < prev.z) {
185
+ grid[row][col] = {
186
+ z: depth,
187
+ shade: clamp(shade * fog, 0.06, 1),
188
+ t: theta + phase,
189
+ spec: (spec + rider) * fog,
190
+ };
191
+ }
180
192
  }
181
193
  }
182
194
  }
183
- let minX = Infinity;
184
- let maxX = -Infinity;
185
- let minY = Infinity;
186
- let maxY = -Infinity;
187
- let minD = Infinity;
188
- let maxD = -Infinity;
189
- for (const sample of samples) {
190
- const sx = sample.px;
191
- const sy = sample.py * CELL_ASPECT;
192
- if (sx < minX)
193
- minX = sx;
194
- if (sx > maxX)
195
- maxX = sx;
196
- if (sy < minY)
197
- minY = sy;
198
- if (sy > maxY)
199
- maxY = sy;
200
- if (sample.depth < minD)
201
- minD = sample.depth;
202
- if (sample.depth > maxD)
203
- maxD = sample.depth;
204
- }
205
- const spanX = Math.max(0.001, maxX - minX);
206
- const spanY = Math.max(0.001, maxY - minY);
207
- const scale = Math.min((cols - 4) / spanX, (rows - 2) / spanY);
208
- const midX = (minX + maxX) / 2;
209
- const midY = (minY + maxY) / 2;
210
- const depthSpan = Math.max(0.001, maxD - minD);
211
- for (const sample of samples) {
212
- const fog = 0.4 + 0.6 * (1 - (sample.depth - minD) / depthSpan);
213
- const col = Math.round((sample.px - midX) * scale + (cols - 1) / 2);
214
- const row = Math.round(-((sample.py * CELL_ASPECT) - midY) * scale + (rows - 1) / 2);
215
- if (col < 0 || col >= cols || row < 0 || row >= rows)
216
- continue;
217
- const prev = grid[row][col];
218
- if (!prev || sample.depth < prev.z) {
219
- grid[row][col] = {
220
- z: sample.depth,
221
- shade: clamp(sample.shade * fog, 0.06, 1),
222
- t: sample.t,
223
- spec: sample.spec * fog,
224
- };
225
- }
226
- }
227
- return grid.map((line) => line.map((cell) => {
195
+ return grid
196
+ .map((line) => line
197
+ .map((cell) => {
228
198
  if (!cell)
229
199
  return ' ';
230
200
  const ch = shadeChar(cell.shade);
231
201
  if (!color)
232
202
  return ch;
233
203
  let [r, g, b] = mobiusRgb(cell.t);
234
- const lift = 0.18 + cell.shade * 0.95 + cell.spec * 0.7;
204
+ const lift = 0.2 + cell.shade * 0.92 + cell.spec * 0.55;
235
205
  r = clamp(Number.isFinite(r * lift) ? Math.round(r * lift) : 180, 0, 255);
236
206
  g = clamp(Number.isFinite(g * lift) ? Math.round(g * lift) : 180, 0, 255);
237
207
  b = clamp(Number.isFinite(b * lift) ? Math.round(b * lift) : 180, 0, 255);
238
208
  return `${ansiRgb(r, g, b)}${ch}${RESET}`;
239
- }).join('')).join('\n');
209
+ })
210
+ .join(''))
211
+ .join('\n');
240
212
  }
241
213
  function mobiusFrameHeight(columns = 80) {
242
- return columns < 56 ? 13 : 20;
214
+ return columns < 52 ? 11 : columns < 72 ? 15 : 18;
243
215
  }
244
216
  function mobiusFrameWidth(columns = 80) {
245
- return columns < 56 ? 44 : Math.min(76, Math.max(56, columns - 4));
217
+ if (columns < 52)
218
+ return Math.max(32, columns);
219
+ if (columns < 72)
220
+ return Math.min(60, columns);
221
+ return Math.min(72, columns);
246
222
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.stripAnsi = void 0;
4
4
  exports.renderPager = renderPager;
5
5
  const logo_1 = require("../logo");
6
+ const mobius_1 = require("../mobius");
6
7
  const model_1 = require("./model");
7
8
  const theme_1 = require("./theme");
8
9
  Object.defineProperty(exports, "stripAnsi", { enumerable: true, get: function () { return theme_1.stripAnsi; } });
@@ -107,16 +108,12 @@ function center(text, width, vis = (0, logo_1.displayWidth)(text)) {
107
108
  }
108
109
  function welcomeLines(guest, width, height, _theme) {
109
110
  const copy = (0, logo_1.sisuWelcomeCopy)(guest, true).map((line) => center(line, width));
110
- const treeCols = height >= 18 && width >= 48 ? Math.min(width, 72) : width >= 36 ? 44 : 28;
111
- const raw = (0, logo_1.sisuTreeLines)(treeCols);
112
- const painted = (0, logo_1.sisuTreeArt)(treeCols, true).split('\n');
113
- const treeWidth = raw.reduce((max, line) => Math.max(max, line.length), 0);
114
- const treePad = Math.max(0, Math.floor((width - treeWidth) / 2));
115
- const tree = painted.map((line, i) => {
116
- const vis = raw[i]?.length ?? (0, logo_1.displayWidth)(line);
117
- return `${' '.repeat(treePad)}${line}${' '.repeat(Math.max(0, treeWidth - vis))}`;
118
- });
119
- return [...copy, '', ...tree];
111
+ const ringW = height >= 16 && width >= 48 ? (0, mobius_1.mobiusFrameWidth)(Math.min(width, 72)) : Math.min(width, 44);
112
+ const ringH = Math.min((0, mobius_1.mobiusFrameHeight)(ringW), Math.max(8, height - 7));
113
+ const painted = (0, logo_1.sisuMobiusArt)(ringW, 0.85, true).split('\n').slice(0, ringH);
114
+ const pad = Math.max(0, Math.floor((width - ringW) / 2));
115
+ const ring = painted.map((line) => `${' '.repeat(pad)}${line}`);
116
+ return [...copy, '', ...ring];
120
117
  }
121
118
  function slashMenuLines(state, theme) {
122
119
  if (!state.slashOpen)
package/dist/tree.js CHANGED
@@ -1,116 +1,331 @@
1
1
  "use strict";
2
- /** 溯源之树 hand-inked ASCII of the login welcome painting. */
2
+ /** Volumetric 溯源之树: 3D L-system, z-buffer, lighting, grow + orbit. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.buildTree3d = buildTree3d;
5
+ exports.treeFrameHeight = treeFrameHeight;
6
+ exports.treeFrameWidth = treeFrameWidth;
7
+ exports.renderTreeFrame = renderTreeFrame;
4
8
  exports.sisuTreeLines = sisuTreeLines;
5
9
  exports.sisuTreeArt = sisuTreeArt;
6
10
  exports.sisuTreeWidth = sisuTreeWidth;
7
11
  exports.sisuTreeHeight = sisuTreeHeight;
8
12
  const RESET = '\x1b[0m';
9
- const INK = [196, 186, 170];
10
- const INK_DIM = [130, 120, 108];
11
- const FRUIT = [184, 90, 58];
12
- const HILL = [90, 82, 70];
13
- /**
14
- * Desktop login tree. Trunk sits near center; crown opens into an airy
15
- * fan with terracotta tips — the same 图景 as BrandCanvas, not a Möbius.
16
- */
17
- const TREE_WIDE = [
18
- ' • • • • • • •',
19
- ' • • • • • • • • • •',
20
- ' • • • • • • • • • • • •',
21
- ' • • ╲ • ╲ • ╱ • ╲ • • •',
22
- ' • ╲ ╱ ╲ ╲ ╱ ╲ ╱ ╲ ╲ ╲ ╱',
23
- ' • ╲ ╱ ╲ ╲ ╱ ╲ ╱ ╲ ╲ ╲╱',
24
- ' ╲ ╲╱ ╲ ╲╱ ╲ ╱ ╲ ╲ ╱',
25
- ' ╲ ╱ ╲ ╱ Y ╲ ╲ ╱',
26
- ' ╲ ╱ ╲ ╱ ╱ ╲ ╲╱',
27
- ' ╲ ╱ Y ╱ ╲ ╱',
28
- ' ╲ ╱ ╱ ╲ ╱ ╲╱',
29
- ' Y ╱ ╲ ╱ ╱',
30
- ' │ ╱ ╲ ╱',
31
- ' │ ╱ Y',
32
- ' │ ╱',
33
- ' │',
34
- '▁▁▁▂▂▁▁▁▁▁▁▁▁▁│▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁',
35
- ];
36
- const TREE_MID = [
37
- ' • • • • • • •',
38
- ' • • • • • • • • •',
39
- ' • • ╲ • ╲ • ╱ • ╲ • •',
40
- ' • ╲ ╱ ╲ ╲ ╱ ╲ ╱ ╲ ╲ ╱',
41
- ' ╲ ╲ ╱ ╲ ╲ ╱ ╲ ╱ ╲ Y',
42
- ' ╲ Y ╲ Y ╲╱ ╲ ╱',
43
- ' ╲ ╱ ╲ ╱ ╱ Y',
44
- ' ╲╱ Y ╱ ╱',
45
- ' │ ╱ ╲ ╱',
46
- ' │ ╱ ╲ ╱',
47
- ' │ ╱ ╲╱',
48
- ' │',
49
- '▁▁▂▂▁▁▁▁▁▁▁│▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁',
50
- ];
51
- const TREE_NARROW = [
52
- ' • • • • •',
53
- ' • ╲ • ╱ • ╲ •',
54
- ' ╲ ╱ ╲ ╱ ╲ ╱ ╲╱',
55
- ' ╲ ╱ Y ╲╱ ╱',
56
- ' Y ╱ ╲ ╱',
57
- ' │ ╱ ╲╱',
58
- ' │',
59
- '▁▁▁▁▁▁▁▁│▁▁▁▁▁▁▁▁▁▁▁▁▁▁',
60
- ];
13
+ const SHADE = ' .:-=+*#%@';
14
+ const CELL_ASPECT = 0.52;
15
+ const CAM = 5.35;
16
+ const FOCAL = 4.7;
17
+ const LOOK_Y = 0.72;
18
+ const KEY = norm([-0.62, 0.72, 0.32]);
19
+ const FILL = norm([0.55, 0.18, 0.82]);
20
+ const VIEW = [0, 0.08, 1];
21
+ const INK = [198, 186, 168];
22
+ const FRUIT_RGB = [184, 90, 58];
23
+ const HILL_RGB = [92, 84, 72];
24
+ const TREE_SEED = 20260817;
25
+ const MAX_DEPTH = 5;
26
+ function clamp(value, lo, hi) {
27
+ return Math.max(lo, Math.min(hi, value));
28
+ }
61
29
  function lerp(a, b, t) {
62
30
  return a + (b - a) * t;
63
31
  }
64
- function paintRgb(ch, rgb) {
65
- return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${ch}`;
32
+ function norm(v) {
33
+ const length = Math.hypot(v[0], v[1], v[2]) || 1;
34
+ return [v[0] / length, v[1] / length, v[2] / length];
66
35
  }
67
- function inkAt(x, width) {
68
- const t = width <= 1 ? 0 : x / (width - 1);
36
+ function cross(a, b) {
69
37
  return [
70
- Math.round(lerp(INK[0], INK_DIM[0], t * 0.45)),
71
- Math.round(lerp(INK[1], INK_DIM[1], t * 0.45)),
72
- Math.round(lerp(INK[2], INK_DIM[2], t * 0.45)),
38
+ a[1] * b[2] - a[2] * b[1],
39
+ a[2] * b[0] - a[0] * b[2],
40
+ a[0] * b[1] - a[1] * b[2],
73
41
  ];
74
42
  }
75
- function paintLine(line, color) {
76
- if (!color)
77
- return line;
78
- let out = '';
79
- for (let i = 0; i < line.length; i += 1) {
80
- const ch = line[i];
81
- if (ch === ' ') {
82
- out += ' ';
83
- continue;
43
+ function dot(a, b) {
44
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
45
+ }
46
+ function add(a, b, s = 1) {
47
+ return [a[0] + b[0] * s, a[1] + b[1] * s, a[2] + b[2] * s];
48
+ }
49
+ function rotateY(x, z, angle) {
50
+ const c = Math.cos(angle);
51
+ const s = Math.sin(angle);
52
+ return [x * c + z * s, -x * s + z * c];
53
+ }
54
+ function rotateX(y, z, angle) {
55
+ const c = Math.cos(angle);
56
+ const s = Math.sin(angle);
57
+ return [y * c - z * s, y * s + z * c];
58
+ }
59
+ function mulberry32(seed) {
60
+ let a = seed >>> 0;
61
+ return () => {
62
+ a |= 0;
63
+ a = (a + 0x6d2b79f5) | 0;
64
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
65
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
66
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
67
+ };
68
+ }
69
+ function perpFrame(dir) {
70
+ const helper = Math.abs(dir[1]) < 0.86 ? [0, 1, 0] : [1, 0, 0];
71
+ const n1 = norm(cross(dir, helper));
72
+ const n2 = norm(cross(dir, n1));
73
+ return [n1, n2];
74
+ }
75
+ let cached = null;
76
+ function headingFrom(yaw, pitch) {
77
+ const sp = Math.sin(pitch);
78
+ return norm([sp * Math.sin(yaw), Math.cos(pitch), sp * Math.cos(yaw)]);
79
+ }
80
+ function buildTree3d(seed = TREE_SEED) {
81
+ if (cached && cached.seed === seed)
82
+ return cached.branches;
83
+ const rng = mulberry32(seed);
84
+ const branches = [];
85
+ const grow = (origin, yaw, pitch, len, radius, depth, t0) => {
86
+ if (depth > MAX_DEPTH || len < 0.08 || branches.length > 150)
87
+ return;
88
+ const heading = headingFrom(yaw, pitch);
89
+ const end = add(origin, heading, len);
90
+ const t1 = Math.min(1, t0 + len / 2.8);
91
+ const childCount = depth >= MAX_DEPTH ? 0 : rng() < 0.74 ? 2 : 3;
92
+ branches.push({
93
+ ax: origin[0],
94
+ ay: origin[1],
95
+ az: origin[2],
96
+ bx: end[0],
97
+ by: end[1],
98
+ bz: end[2],
99
+ radius,
100
+ depth,
101
+ t0,
102
+ t1,
103
+ tip: childCount === 0,
104
+ });
105
+ const baseYaw = yaw + (rng() - 0.5) * 0.4;
106
+ for (let i = 0; i < childCount; i += 1) {
107
+ const fork = ((i - (childCount - 1) / 2) * 2 * Math.PI) / childCount;
108
+ const childYaw = baseYaw + fork + (rng() - 0.5) * 0.35;
109
+ const childPitch = Math.min(0.98, pitch + (depth === 0 ? 0.4 : 0.26) + rng() * 0.22);
110
+ grow(end, childYaw, childPitch, len * (0.66 + rng() * 0.12), radius * 0.67, depth + 1, t1 + rng() * 0.02);
84
111
  }
85
- if (ch === '•') {
86
- out += paintRgb(ch, FRUIT);
87
- continue;
112
+ };
113
+ grow([0, 0, 0], 0.1, 0.03, 0.82, 0.095, 0, 0);
114
+ const tMax = branches.reduce((m, b) => Math.max(m, b.t1), 0) || 1;
115
+ for (const b of branches) {
116
+ b.t0 /= tMax;
117
+ b.t1 /= tMax;
118
+ }
119
+ cached = { seed, branches };
120
+ return branches;
121
+ }
122
+ function shadeChar(shade) {
123
+ return SHADE[Math.round(clamp(shade, 0, 1) * (SHADE.length - 1))] || '@';
124
+ }
125
+ function ansiRgb(r, g, b) {
126
+ return `\x1b[38;2;${r};${g};${b}m`;
127
+ }
128
+ function plot(grid, col, row, depth, shade, spec, kind) {
129
+ if (col < 0 || row < 0 || row >= grid.length || col >= grid[0].length)
130
+ return;
131
+ if (!Number.isFinite(col) || !Number.isFinite(row) || !Number.isFinite(depth))
132
+ return;
133
+ const prev = grid[row][col];
134
+ if (!prev || depth < prev.z) {
135
+ grid[row][col] = { z: depth, shade: clamp(shade, 0.05, 1), spec, kind };
136
+ }
137
+ }
138
+ function project(x, y, z, yaw, pitch, cols, rows) {
139
+ ;
140
+ [x, z] = rotateY(x, z, yaw);
141
+ [y, z] = rotateX(y - LOOK_Y, z, pitch);
142
+ const depth = CAM - z;
143
+ if (depth < 0.45 || !Number.isFinite(depth))
144
+ return null;
145
+ const px = (x * FOCAL) / depth;
146
+ const py = (y * FOCAL) / depth;
147
+ if (!Number.isFinite(px) || !Number.isFinite(py))
148
+ return null;
149
+ const scale = Math.min((cols * 0.9) / 2.05, (rows * 0.88) / (1.85 * CELL_ASPECT));
150
+ const col = Math.round(px * scale + (cols - 1) / 2);
151
+ const row = Math.round(-py * scale * CELL_ASPECT + rows * 0.54);
152
+ return { col, row, depth };
153
+ }
154
+ function light(normal, back = false) {
155
+ const facing = dot(normal, VIEW);
156
+ if (facing < 0) {
157
+ normal = [-normal[0], -normal[1], -normal[2]];
158
+ back = true;
159
+ }
160
+ const lambert = Math.max(0, dot(normal, KEY));
161
+ const fill = Math.max(0, dot(normal, FILL)) * 0.22;
162
+ const cam = Math.max(0, dot(normal, VIEW)) * 0.2;
163
+ const half = norm([KEY[0] + VIEW[0], KEY[1] + VIEW[1], KEY[2] + VIEW[2]]);
164
+ const spec = Math.pow(Math.max(0, dot(normal, half)), 22);
165
+ const rim = Math.pow(1 - Math.abs(dot(normal, VIEW)), 2.1) * 0.22;
166
+ const ambient = back ? 0.1 : 0.2;
167
+ const lit = ambient + lambert * (back ? 0.28 : 0.72) + fill + cam + spec * 0.45 + rim;
168
+ return { shade: clamp(Math.pow(lit, 1.15), 0.07, 1), spec };
169
+ }
170
+ function swayAt(x, y, z, phase) {
171
+ const h = clamp(y / 2.3, 0, 1);
172
+ const gust = Math.sin(phase * 1.7 + x * 1.4 + z * 0.8);
173
+ const lean = 0.11 * h * h * gust;
174
+ return [x + lean, y, z + lean * 0.35];
175
+ }
176
+ function sampleBranch(grid, branch, grow, phase, yaw, pitch, cols, rows) {
177
+ if (grow <= branch.t0)
178
+ return;
179
+ const visible = clamp((grow - branch.t0) / Math.max(0.02, branch.t1 - branch.t0), 0, 1);
180
+ const along = Math.max(4, Math.ceil((branch.depth <= 1 ? 24 : branch.depth <= 3 ? 12 : 6) * visible));
181
+ const rings = branch.depth <= 1 ? 9 : branch.depth <= 3 ? 4 : 2;
182
+ const dx = branch.bx - branch.ax;
183
+ const dy = branch.by - branch.ay;
184
+ const dz = branch.bz - branch.az;
185
+ const dir = norm([dx, dy, dz]);
186
+ const [n1, n2] = perpFrame(dir);
187
+ for (let i = 0; i <= along; i += 1) {
188
+ const t = (i / along) * visible;
189
+ const px = branch.ax + dx * t;
190
+ const py = branch.ay + dy * t;
191
+ const pz = branch.az + dz * t;
192
+ const radius = branch.radius * (1 - t * 0.28);
193
+ for (let r = 0; r < rings; r += 1) {
194
+ const a = (r / rings) * Math.PI * 2 + phase * 0.05;
195
+ const ox = n1[0] * Math.cos(a) + n2[0] * Math.sin(a);
196
+ const oy = n1[1] * Math.cos(a) + n2[1] * Math.sin(a);
197
+ const oz = n1[2] * Math.cos(a) + n2[2] * Math.sin(a);
198
+ const [sx, sy, sz] = swayAt(px + ox * radius, py + oy * radius, pz + oz * radius, phase);
199
+ let nx = ox;
200
+ let ny = oy;
201
+ let nz = oz;
202
+ [nx, nz] = rotateY(nx, nz, yaw);
203
+ [ny, nz] = rotateX(ny, nz, pitch);
204
+ const hit = project(sx, sy, sz, yaw, pitch, cols, rows);
205
+ if (!hit)
206
+ continue;
207
+ const lit = light(norm([nx, ny, nz]));
208
+ const fog = 0.45 + 0.55 * clamp(1 - (hit.depth - 5.2) / 4.2, 0, 1);
209
+ plot(grid, hit.col, hit.row, hit.depth, lit.shade * fog, lit.spec * fog, 'wood');
88
210
  }
89
- if (ch === '▁' || ch === '▂') {
90
- out += paintRgb(ch, HILL);
211
+ }
212
+ }
213
+ function sampleFruit(grid, branches, grow, phase, yaw, pitch, cols, rows) {
214
+ const placed = [];
215
+ const gap = 0.28;
216
+ let count = 0;
217
+ for (const branch of branches) {
218
+ if (!branch.tip || grow < branch.t1)
219
+ continue;
220
+ if (placed.some((p) => Math.hypot(p[0] - branch.bx, p[1] - branch.by, p[2] - branch.bz) < gap))
91
221
  continue;
222
+ placed.push([branch.bx, branch.by, branch.bz]);
223
+ const [bx, by, bz] = swayAt(branch.bx, branch.by, branch.bz, phase);
224
+ const hit = project(bx, by, bz, yaw, pitch, cols, rows);
225
+ if (!hit)
226
+ continue;
227
+ const twinkle = 0.88 + 0.12 * Math.sin(phase * 3.1 + branch.bx * 7);
228
+ plot(grid, hit.col, hit.row, hit.depth - 0.02, 0.92 * twinkle, 0.55, 'fruit');
229
+ count += 1;
230
+ if (count >= 22)
231
+ break;
232
+ }
233
+ }
234
+ function sampleGround(grid, phase, yaw, pitch, cols, rows) {
235
+ for (let ix = -16; ix <= 16; ix += 1) {
236
+ for (let iz = -14; iz <= 14; iz += 1) {
237
+ const x = ix * 0.12;
238
+ const z = iz * 0.12;
239
+ const rr = (x * x) / (1.85 * 1.85) + (z * z) / (1.45 * 1.45);
240
+ if (rr > 1)
241
+ continue;
242
+ if (rr < 0.88 && rr > 0.18 && (ix * 3 + iz * 5) % 4 !== 0)
243
+ continue;
244
+ const y = 0.05 * Math.sin(x * 1.6 + 0.5) * Math.cos(z * 1.25) +
245
+ 0.018 * Math.sin(x * 3.0 + z * 2.1 + phase * 0.15);
246
+ const hit = project(x, y, z, yaw, pitch, cols, rows);
247
+ if (!hit)
248
+ continue;
249
+ const nx = -0.08 * Math.cos(x * 1.6 + 0.5);
250
+ const nz = 0.06 * Math.sin(z * 1.25);
251
+ let nnx = nx;
252
+ let nny = 1;
253
+ let nnz = nz;
254
+ [nnx, nnz] = rotateY(nnx, nnz, yaw);
255
+ [nny, nnz] = rotateX(nny, nnz, pitch);
256
+ const lit = light(norm([nnx, nny, nnz]), true);
257
+ const fog = 0.22 + 0.35 * clamp(1 - rr, 0, 1) * clamp(1 - (hit.depth - 5.2) / 4.8, 0, 1);
258
+ plot(grid, hit.col, hit.row, hit.depth + 0.1, lit.shade * fog, 0, 'ground');
92
259
  }
93
- out += paintRgb(ch, inkAt(i, line.length));
94
260
  }
95
- return `${out}${RESET}`;
96
261
  }
97
- function padBlock(lines) {
98
- const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
99
- return lines.map((line) => line.padEnd(width, ' '));
262
+ function paintCell(cell, color) {
263
+ const ch = cell.kind === 'fruit' ? '\u2022' : shadeChar(cell.shade);
264
+ if (!color)
265
+ return ch;
266
+ const base = cell.kind === 'fruit' ? FRUIT_RGB : cell.kind === 'ground' ? HILL_RGB : INK;
267
+ const lift = 0.22 + cell.shade * 0.95 + cell.spec * 0.55;
268
+ const r = clamp(Math.round(base[0] * lift), 0, 255);
269
+ const g = clamp(Math.round(base[1] * lift), 0, 255);
270
+ const b = clamp(Math.round(base[2] * lift), 0, 255);
271
+ if (![r, g, b].every(Number.isFinite))
272
+ return ch;
273
+ return `${ansiRgb(r, g, b)}${ch}${RESET}`;
274
+ }
275
+ function treeFrameHeight(columns = 80) {
276
+ if (columns < 44)
277
+ return 10;
278
+ if (columns < 68)
279
+ return 14;
280
+ return 18;
281
+ }
282
+ function treeFrameWidth(columns = 80) {
283
+ if (columns < 44)
284
+ return Math.max(28, columns);
285
+ if (columns < 68)
286
+ return Math.min(52, columns);
287
+ return Math.min(72, Math.max(56, columns - 2));
288
+ }
289
+ function renderTreeFrame(options = {}) {
290
+ const cols = Math.max(24, Math.floor(options.cols ?? 64));
291
+ const rows = Math.max(8, Math.floor(options.rows ?? 16));
292
+ const phase = options.phase ?? 0.4;
293
+ const grow = clamp(options.grow ?? 1, 0, 1);
294
+ const color = options.color !== false;
295
+ const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
296
+ const yaw = 0.42 + 0.62 * Math.sin(phase);
297
+ const pitch = 0.3 + 0.07 * Math.cos(phase * 0.85);
298
+ const branches = buildTree3d();
299
+ sampleGround(grid, phase, yaw, pitch, cols, rows);
300
+ for (const branch of branches)
301
+ sampleBranch(grid, branch, grow, phase, yaw, pitch, cols, rows);
302
+ sampleFruit(grid, branches, grow, phase, yaw, pitch, cols, rows);
303
+ return grid
304
+ .map((line) => line.map((cell) => (cell ? paintCell(cell, color) : ' ')).join(''))
305
+ .join('\n');
100
306
  }
101
- function sisuTreeLines(columns = 80) {
102
- if (columns >= 68)
103
- return padBlock(TREE_WIDE);
104
- if (columns >= 44)
105
- return padBlock(TREE_MID);
106
- return padBlock(TREE_NARROW);
307
+ function sisuTreeLines(columns = 80, phase = 2.05, grow = 1) {
308
+ const art = renderTreeFrame({
309
+ cols: treeFrameWidth(columns),
310
+ rows: treeFrameHeight(columns),
311
+ phase,
312
+ grow,
313
+ color: false,
314
+ });
315
+ return art.split('\n');
107
316
  }
108
- function sisuTreeArt(columns = 80, color = true) {
109
- return sisuTreeLines(columns).map((line) => paintLine(line, color)).join('\n');
317
+ function sisuTreeArt(columns = 80, color = true, phase = 2.05, grow = 1) {
318
+ return renderTreeFrame({
319
+ cols: treeFrameWidth(columns),
320
+ rows: treeFrameHeight(columns),
321
+ phase,
322
+ grow,
323
+ color,
324
+ });
110
325
  }
111
326
  function sisuTreeWidth(columns = 80) {
112
- return sisuTreeLines(columns).reduce((max, line) => Math.max(max, line.length), 0);
327
+ return treeFrameWidth(columns);
113
328
  }
114
329
  function sisuTreeHeight(columns = 80) {
115
- return sisuTreeLines(columns).length;
330
+ return treeFrameHeight(columns);
116
331
  }
package/dist/tui.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.shouldAnimateSplash = shouldAnimateSplash;
7
7
  exports.playMobiusIntro = playMobiusIntro;
8
+ exports.playTreeIntro = playTreeIntro;
8
9
  exports.defaultTuiIo = defaultTuiIo;
9
10
  exports.tuiHelp = tuiHelp;
10
11
  exports.runTui = runTui;
@@ -48,6 +49,26 @@ async function playMobiusIntro(io, options = {}) {
48
49
  io.write('\x1b[?25h');
49
50
  io.write(`\n${(0, logo_1.sisuWordmark)()}\n\n`);
50
51
  }
52
+ /** Slide the half-twist around the ∞ so the single face loops. */
53
+ async function playTreeIntro(io, options = {}) {
54
+ const columns = options.columns ?? process.stdout.columns ?? 80;
55
+ const frames = Math.max(2, options.frames ?? 36);
56
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
57
+ const color = options.color ?? Boolean(process.stdout.isTTY);
58
+ const rows = (0, logo_1.sisuSplashHeight)(columns);
59
+ io.write('\x1b[?25l');
60
+ for (let i = 0; i < frames; i += 1) {
61
+ const u = i / (frames - 1);
62
+ const phase = u * Math.PI * 2;
63
+ const art = (0, logo_1.sisuSplashFrame)(columns, color, phase);
64
+ if (i === 0)
65
+ io.write(`${art}\n`);
66
+ else
67
+ io.write(`\x1b[${rows}A${art}\n`);
68
+ await sleep(42);
69
+ }
70
+ io.write('\x1b[?25h');
71
+ }
51
72
  function defaultTuiIo() {
52
73
  let rl;
53
74
  const ensureRl = () => {
@@ -174,9 +195,15 @@ async function runTui(io, deps = {}) {
174
195
  const columns = deps.columns ?? process.stdout.columns ?? 80;
175
196
  const animate = deps.animate ?? shouldAnimateSplash();
176
197
  try {
177
- io.write(`${(0, logo_1.sisuSplash)(columns, true)}\n`);
178
198
  if (animate) {
179
- await (deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(80);
199
+ await playTreeIntro(io, {
200
+ columns,
201
+ color: deps.color ?? true,
202
+ sleep: deps.sleep,
203
+ });
204
+ }
205
+ else {
206
+ io.write(`${(0, logo_1.sisuSplash)(columns, true)}\n`);
180
207
  }
181
208
  const account = auth();
182
209
  const startWebLogin = async (notify) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevezhou/sisu",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "SiSu CLI — one login, cloud quota, local workspace",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.sisu.chat",