@stevezhou/sisu 0.1.2 → 0.1.3
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/pager/render.js +57 -45
- package/dist/pager/theme.js +53 -5
- package/package.json +1 -1
package/dist/pager/render.js
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripAnsi = void 0;
|
|
3
4
|
exports.renderPager = renderPager;
|
|
4
5
|
const model_1 = require("./model");
|
|
5
6
|
const theme_1 = require("./theme");
|
|
7
|
+
Object.defineProperty(exports, "stripAnsi", { enumerable: true, get: function () { return theme_1.stripAnsi; } });
|
|
6
8
|
const PROMPT_PREFIX = '› ';
|
|
7
9
|
const PROMPT_BOX_ROWS = 2;
|
|
8
10
|
const STATUS_ROWS = 1;
|
|
9
11
|
const MARK_WIDTH = 2;
|
|
10
|
-
/** Pad (and clip reserved chrome) to exactly `cols` cells. */
|
|
11
|
-
function pad(line, cols) {
|
|
12
|
-
const width = Math.max(0, cols);
|
|
13
|
-
if (width === 0)
|
|
14
|
-
return '';
|
|
15
|
-
const plain = (0, theme_1.stripAnsi)(line);
|
|
16
|
-
if (plain.length === width)
|
|
17
|
-
return plain;
|
|
18
|
-
if (plain.length > width)
|
|
19
|
-
return plain.slice(0, width);
|
|
20
|
-
return plain.padEnd(width, ' ');
|
|
21
|
-
}
|
|
22
12
|
function wrapPlain(text, width) {
|
|
23
13
|
const max = Math.max(1, width);
|
|
24
14
|
if (!text)
|
|
@@ -40,6 +30,24 @@ function lineCount(text) {
|
|
|
40
30
|
return 0;
|
|
41
31
|
return text.split('\n').length;
|
|
42
32
|
}
|
|
33
|
+
function paintKind(kind, body, theme) {
|
|
34
|
+
if (!body)
|
|
35
|
+
return body;
|
|
36
|
+
if (kind === 'user')
|
|
37
|
+
return theme.user(body);
|
|
38
|
+
if (kind === 'tool')
|
|
39
|
+
return theme.tool(body);
|
|
40
|
+
if (kind === 'status')
|
|
41
|
+
return theme.dim(body);
|
|
42
|
+
return theme.text(body);
|
|
43
|
+
}
|
|
44
|
+
function entryPrefix(kind) {
|
|
45
|
+
if (kind === 'user')
|
|
46
|
+
return 'you ';
|
|
47
|
+
if (kind === 'tool')
|
|
48
|
+
return 'tool ';
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
43
51
|
function entryBodyLines(entry, wrapWidth) {
|
|
44
52
|
if (entry.folded) {
|
|
45
53
|
const n = lineCount(entry.text);
|
|
@@ -48,18 +56,19 @@ function entryBodyLines(entry, wrapWidth) {
|
|
|
48
56
|
}
|
|
49
57
|
if (!entry.text)
|
|
50
58
|
return [''];
|
|
51
|
-
return wrapPlain(entry.text
|
|
59
|
+
return wrapPlain(`${entryPrefix(entry.kind)}${entry.text}`, wrapWidth);
|
|
52
60
|
}
|
|
53
|
-
function layoutScrollback(state, cols) {
|
|
61
|
+
function layoutScrollback(state, cols, theme) {
|
|
54
62
|
const wrapWidth = Math.max(1, cols - MARK_WIDTH);
|
|
55
63
|
const lines = [];
|
|
56
64
|
const entryOf = [];
|
|
57
65
|
for (let i = 0; i < state.entries.length; i += 1) {
|
|
58
66
|
const entry = state.entries[i];
|
|
59
67
|
const body = entryBodyLines(entry, wrapWidth);
|
|
60
|
-
const
|
|
68
|
+
const selected = i === state.selected && state.entries.length > 0;
|
|
61
69
|
for (let j = 0; j < body.length; j += 1) {
|
|
62
|
-
|
|
70
|
+
const mark = selected && j === 0 ? theme.accent('▸ ') : ' ';
|
|
71
|
+
lines.push(`${mark}${paintKind(entry.kind, body[j], theme)}`);
|
|
63
72
|
entryOf.push(i);
|
|
64
73
|
}
|
|
65
74
|
}
|
|
@@ -91,40 +100,45 @@ function windowAroundSelected(lines, entryOf, selected, budget, lastEntry) {
|
|
|
91
100
|
function isIdleWelcome(state) {
|
|
92
101
|
return !state.conversationId && state.entries.length === 0;
|
|
93
102
|
}
|
|
94
|
-
function
|
|
103
|
+
function center(text, width) {
|
|
104
|
+
const padLeft = Math.max(0, Math.floor((width - text.length) / 2));
|
|
105
|
+
return `${' '.repeat(padLeft)}${text}`;
|
|
106
|
+
}
|
|
107
|
+
function welcomeLines(guest, width, theme) {
|
|
108
|
+
const title = theme.accent(center('SISU', width));
|
|
95
109
|
if (guest) {
|
|
96
110
|
return [
|
|
97
|
-
|
|
111
|
+
title,
|
|
112
|
+
'',
|
|
113
|
+
theme.text(center('Sign in to start a conversation.', width)),
|
|
98
114
|
'',
|
|
99
|
-
|
|
100
|
-
'/
|
|
101
|
-
'/help commands',
|
|
115
|
+
theme.dim(center('/login browser', width)),
|
|
116
|
+
theme.dim(center('/help commands', width)),
|
|
102
117
|
];
|
|
103
118
|
}
|
|
104
119
|
return [
|
|
105
|
-
|
|
120
|
+
title,
|
|
106
121
|
'',
|
|
107
|
-
'Ask anything, or type /help.',
|
|
122
|
+
theme.dim(center('Ask anything, or type /help.', width)),
|
|
108
123
|
];
|
|
109
124
|
}
|
|
110
|
-
function slashMenuLines(state) {
|
|
125
|
+
function slashMenuLines(state, theme) {
|
|
111
126
|
if (!state.slashOpen)
|
|
112
127
|
return [];
|
|
113
128
|
const items = (0, model_1.filterSlash)(state.draft);
|
|
114
129
|
return items.map((item, index) => {
|
|
115
|
-
const
|
|
116
|
-
|
|
130
|
+
const active = index === state.slashIndex;
|
|
131
|
+
const mark = active ? theme.accent('› ') : ' ';
|
|
132
|
+
const name = active ? theme.accent(item.name) : theme.text(item.name);
|
|
133
|
+
return `${mark}${name} ${theme.dim(item.hint)}`;
|
|
117
134
|
});
|
|
118
135
|
}
|
|
119
136
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* frame itself is plain so `line.length === cols` for the tty writer.
|
|
123
|
-
* Deterministic: no clock, no I/O.
|
|
137
|
+
* Fixed-grid frame: always `rows` lines, each `cols` visible cells.
|
|
138
|
+
* Colors are 24-bit SGR; measure width with stripAnsi / visibleWidth.
|
|
124
139
|
*/
|
|
125
|
-
function renderPager(state, cols, rows,
|
|
126
|
-
|
|
127
|
-
(0, theme_1.getTheme)(theme);
|
|
140
|
+
function renderPager(state, cols, rows, themeName = 'dark') {
|
|
141
|
+
const theme = (0, theme_1.getTheme)(themeName);
|
|
128
142
|
const height = Math.max(0, rows);
|
|
129
143
|
const width = Math.max(0, cols);
|
|
130
144
|
if (height === 0)
|
|
@@ -133,38 +147,36 @@ function renderPager(state, cols, rows, theme = 'dark') {
|
|
|
133
147
|
const statusRows = height > promptRows ? Math.min(STATUS_ROWS, height - promptRows) : 0;
|
|
134
148
|
const chrome = promptRows + statusRows;
|
|
135
149
|
const bodyBudget = Math.max(0, height - chrome);
|
|
136
|
-
const slash = slashMenuLines(state);
|
|
150
|
+
const slash = slashMenuLines(state, theme);
|
|
137
151
|
const slashTake = Math.min(slash.length, bodyBudget);
|
|
138
152
|
const guest = !(state.statusLine || '').trim() || (state.statusLine || '').includes('not signed in');
|
|
139
|
-
const welcome = isIdleWelcome(state) ? welcomeLines(guest
|
|
153
|
+
const welcome = isIdleWelcome(state) ? welcomeLines(guest, width, theme) : [];
|
|
140
154
|
const welcomeTake = Math.min(welcome.length, Math.max(0, bodyBudget - slashTake));
|
|
141
155
|
const scrollBudget = bodyBudget - slashTake - welcomeTake;
|
|
142
|
-
const laid = layoutScrollback(state, width);
|
|
156
|
+
const laid = layoutScrollback(state, width, theme);
|
|
143
157
|
const lastEntry = Math.max(0, state.entries.length - 1);
|
|
144
158
|
const visibleScroll = windowAroundSelected(laid.lines, laid.entryOf, state.selected, scrollBudget, lastEntry);
|
|
145
159
|
const body = [];
|
|
146
160
|
body.push(...welcome.slice(0, welcomeTake));
|
|
147
|
-
// Top-pad remaining scrollback so live status sits just above slash/prompt.
|
|
148
161
|
while (body.length + visibleScroll.length < welcomeTake + scrollBudget) {
|
|
149
162
|
body.push('');
|
|
150
163
|
}
|
|
151
164
|
body.push(...visibleScroll);
|
|
152
165
|
body.push(...slash.slice(0, slashTake));
|
|
153
|
-
const lines = body.map((line) =>
|
|
166
|
+
const lines = body.map((line) => (0, theme_1.padVisible)(line, width));
|
|
154
167
|
if (statusRows > 0) {
|
|
155
|
-
lines.push(
|
|
168
|
+
lines.push((0, theme_1.padVisible)(theme.dim(state.statusLine ?? ''), width));
|
|
156
169
|
}
|
|
157
170
|
if (promptRows >= 2) {
|
|
158
|
-
|
|
159
|
-
lines.push(
|
|
160
|
-
lines.push(pad(`${PROMPT_PREFIX}${state.draft}`, width));
|
|
171
|
+
lines.push((0, theme_1.padVisible)(theme.border('─'.repeat(width)), width));
|
|
172
|
+
lines.push((0, theme_1.padVisible)(`${theme.accent(PROMPT_PREFIX)}${theme.text(state.draft)}`, width));
|
|
161
173
|
}
|
|
162
174
|
else if (promptRows === 1) {
|
|
163
|
-
lines.push(
|
|
175
|
+
lines.push((0, theme_1.padVisible)(`${theme.accent(PROMPT_PREFIX)}${theme.text(state.draft)}`, width));
|
|
164
176
|
}
|
|
165
177
|
while (lines.length < height)
|
|
166
|
-
lines.push(
|
|
178
|
+
lines.push((0, theme_1.padVisible)('', width));
|
|
167
179
|
if (lines.length > height)
|
|
168
180
|
lines.length = height;
|
|
169
|
-
return lines.map((line) =>
|
|
181
|
+
return lines.map((line) => (0, theme_1.padVisible)(line, width)).join('\n');
|
|
170
182
|
}
|
package/dist/pager/theme.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/** SiSu pager palette: blue → purple → gold
|
|
2
|
+
/** SiSu pager palette: blue → purple → gold. */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.getTheme = getTheme;
|
|
5
5
|
exports.stripAnsi = stripAnsi;
|
|
6
|
+
exports.visibleWidth = visibleWidth;
|
|
7
|
+
exports.clipVisible = clipVisible;
|
|
8
|
+
exports.padVisible = padVisible;
|
|
6
9
|
const RESET = '\x1b[0m';
|
|
7
10
|
function ansiRgb(r, g, b) {
|
|
8
11
|
return `\x1b[38;2;${r};${g};${b}m`;
|
|
@@ -10,16 +13,17 @@ function ansiRgb(r, g, b) {
|
|
|
10
13
|
function paint(prefix) {
|
|
11
14
|
return (s) => (s ? `${prefix}${s}${RESET}` : s);
|
|
12
15
|
}
|
|
13
|
-
/** Brand anchors from mobiusRgb (blue, purple, gold). */
|
|
14
16
|
const BLUE = ansiRgb(37, 99, 235);
|
|
15
17
|
const PURPLE = ansiRgb(124, 58, 237);
|
|
16
18
|
const GOLD = ansiRgb(217, 119, 6);
|
|
17
19
|
const DARK = {
|
|
18
20
|
text: paint(ansiRgb(230, 232, 240)),
|
|
19
|
-
dim: paint(ansiRgb(
|
|
21
|
+
dim: paint(ansiRgb(118, 124, 148)),
|
|
20
22
|
accent: paint(GOLD),
|
|
21
23
|
error: paint(ansiRgb(239, 68, 68)),
|
|
22
|
-
border: paint(
|
|
24
|
+
border: paint(ansiRgb(72, 64, 96)),
|
|
25
|
+
user: paint(ansiRgb(156, 174, 214)),
|
|
26
|
+
tool: paint(PURPLE),
|
|
23
27
|
reset: RESET,
|
|
24
28
|
};
|
|
25
29
|
const LIGHT = {
|
|
@@ -28,12 +32,56 @@ const LIGHT = {
|
|
|
28
32
|
accent: paint(GOLD),
|
|
29
33
|
error: paint(ansiRgb(185, 28, 28)),
|
|
30
34
|
border: paint(BLUE),
|
|
35
|
+
user: paint(ansiRgb(37, 99, 180)),
|
|
36
|
+
tool: paint(PURPLE),
|
|
31
37
|
reset: RESET,
|
|
32
38
|
};
|
|
33
39
|
function getTheme(name = 'dark') {
|
|
34
40
|
return name === 'light' ? LIGHT : DARK;
|
|
35
41
|
}
|
|
36
|
-
/** Strip 24-bit / SGR sequences for visible-width measurement. */
|
|
37
42
|
function stripAnsi(s) {
|
|
38
43
|
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
39
44
|
}
|
|
45
|
+
function visibleWidth(line) {
|
|
46
|
+
return stripAnsi(line).length;
|
|
47
|
+
}
|
|
48
|
+
function clipVisible(line, width) {
|
|
49
|
+
if (width <= 0)
|
|
50
|
+
return '';
|
|
51
|
+
if (visibleWidth(line) <= width)
|
|
52
|
+
return line;
|
|
53
|
+
let seen = 0;
|
|
54
|
+
let out = '';
|
|
55
|
+
const re = /\x1b\[[0-9;]*m/g;
|
|
56
|
+
let last = 0;
|
|
57
|
+
let match = re.exec(line);
|
|
58
|
+
while (match) {
|
|
59
|
+
for (let i = last; i < match.index && seen < width; i += 1) {
|
|
60
|
+
out += line[i];
|
|
61
|
+
seen += 1;
|
|
62
|
+
}
|
|
63
|
+
if (seen >= width)
|
|
64
|
+
break;
|
|
65
|
+
out += match[0];
|
|
66
|
+
last = match.index + match[0].length;
|
|
67
|
+
match = re.exec(line);
|
|
68
|
+
}
|
|
69
|
+
for (let i = last; i < line.length && seen < width; i += 1) {
|
|
70
|
+
if (line[i] === '\x1b')
|
|
71
|
+
break;
|
|
72
|
+
out += line[i];
|
|
73
|
+
seen += 1;
|
|
74
|
+
}
|
|
75
|
+
return out + RESET;
|
|
76
|
+
}
|
|
77
|
+
function padVisible(line, cols) {
|
|
78
|
+
const width = Math.max(0, cols);
|
|
79
|
+
if (width === 0)
|
|
80
|
+
return '';
|
|
81
|
+
const vis = visibleWidth(line);
|
|
82
|
+
if (vis === width)
|
|
83
|
+
return line;
|
|
84
|
+
if (vis > width)
|
|
85
|
+
return clipVisible(line, width);
|
|
86
|
+
return `${line}${' '.repeat(width - vis)}`;
|
|
87
|
+
}
|