phewsh 0.15.55 → 0.15.57
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/bin/phewsh.js +6 -0
- package/lib/md.js +41 -4
- package/lib/ui.js +68 -0
- package/package.json +1 -1
package/bin/phewsh.js
CHANGED
|
@@ -14,6 +14,12 @@ if (command === 'shim-preflight') {
|
|
|
14
14
|
process.exit(0);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Smart word-wrap for all prose the CLI prints — wraps at word boundaries
|
|
18
|
+
// instead of the terminal's mid-word hard wrap. TTY-only, idempotent, and it
|
|
19
|
+
// leaves process.stdout.write paths (spinner, streamed AI) alone. Installed
|
|
20
|
+
// after the shim-preflight fast path so that stays instant and untouched.
|
|
21
|
+
try { require('../lib/ui').installSmartWrap(); } catch { /* never block the CLI on cosmetics */ }
|
|
22
|
+
|
|
17
23
|
// ── ANSI helpers (no chalk dependency)
|
|
18
24
|
const b = (s) => `\x1b[1m${s}\x1b[0m`; // bold
|
|
19
25
|
const d = (s) => `\x1b[2m${s}\x1b[0m`; // dim
|
package/lib/md.js
CHANGED
|
@@ -33,6 +33,43 @@ function inlineMd(t, base = '') {
|
|
|
33
33
|
(_, x, u) => `${A.uline}${A.teal}${x}${A.reset}${base} ${A.dim}${A.slate}(${u})${A.reset}${base}`);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Soft-wrap a rendered line at word boundaries to the terminal width — this is
|
|
37
|
+
// what replaces the terminal's mid-word hard wrap (CSS `text-wrap: pretty`, in
|
|
38
|
+
// spirit). We never break inside a word. `base` is the block's colour SGR,
|
|
39
|
+
// re-emitted at the start of each continuation line so colour survives the
|
|
40
|
+
// break; `hang` is the continuation indent (hanging indent under list/quote
|
|
41
|
+
// markers). No-op when width is unknown (piped output) so redirected text and
|
|
42
|
+
// tests stay raw.
|
|
43
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
44
|
+
const vis = (s) => s.replace(ANSI, '').length;
|
|
45
|
+
|
|
46
|
+
function softWrap(s, base = '', hang) {
|
|
47
|
+
const width = process.stdout.columns;
|
|
48
|
+
if (!width || width < 24 || vis(s) <= width) return s;
|
|
49
|
+
const lead = s.match(/^ */)[0].length;
|
|
50
|
+
const indent = hang == null ? lead : hang;
|
|
51
|
+
const words = s.slice(lead).split(/ +/);
|
|
52
|
+
const pad = ' '.repeat(indent);
|
|
53
|
+
const out = [];
|
|
54
|
+
let cur = ' '.repeat(lead);
|
|
55
|
+
let len = lead;
|
|
56
|
+
let started = false;
|
|
57
|
+
for (const word of words) {
|
|
58
|
+
const wl = vis(word);
|
|
59
|
+
if (started && len + 1 + wl > width) {
|
|
60
|
+
out.push(cur + A.reset);
|
|
61
|
+
cur = base + pad + word;
|
|
62
|
+
len = indent + wl;
|
|
63
|
+
} else {
|
|
64
|
+
cur += (started ? ' ' : '') + word;
|
|
65
|
+
len += (started ? 1 : 0) + wl;
|
|
66
|
+
started = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
out.push(cur);
|
|
70
|
+
return out.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
36
73
|
// Render one complete line, mutating `state` for block-level context.
|
|
37
74
|
// Body text keeps the default terminal foreground (bright) — only structural
|
|
38
75
|
// elements get colour, so a reply reads as a reply, not as dimmed UI chrome.
|
|
@@ -47,12 +84,12 @@ function renderLine(line, state = {}) {
|
|
|
47
84
|
}
|
|
48
85
|
if (/^#{1,2}\s/.test(line)) return `\n${A.bold}${A.teal}${inlineMd(line.replace(/^#+\s*/, ''), A.teal)}${A.reset}`;
|
|
49
86
|
if (/^#{3,}\s/.test(line)) return `${A.cream}${inlineMd(line.replace(/^#+\s*/, ''), A.cream)}${A.reset}`;
|
|
50
|
-
if (/^\s*[-*]\s/.test(line)) return ` ${A.teal}·${A.reset} ${A.sage}${inlineMd(line.replace(/^\s*[-*]\s*/, ''), A.sage)}${A.reset}
|
|
51
|
-
if (/^\s*\d+\.\s/.test(line)) return ` ${A.sage}${inlineMd(line.trim(), A.sage)}${A.reset}
|
|
52
|
-
if (/^\s*>\s?/.test(line)) return ` ${A.slate}${A.ital}${inlineMd(line.replace(/^\s*>\s?/, ''), A.slate)}${A.italOff}${A.reset}
|
|
87
|
+
if (/^\s*[-*]\s/.test(line)) return softWrap(` ${A.teal}·${A.reset} ${A.sage}${inlineMd(line.replace(/^\s*[-*]\s*/, ''), A.sage)}${A.reset}`, A.sage, 4);
|
|
88
|
+
if (/^\s*\d+\.\s/.test(line)) return softWrap(` ${A.sage}${inlineMd(line.trim(), A.sage)}${A.reset}`, A.sage, 5);
|
|
89
|
+
if (/^\s*>\s?/.test(line)) return softWrap(` ${A.slate}${A.ital}${inlineMd(line.replace(/^\s*>\s?/, ''), A.slate)}${A.italOff}${A.reset}`, A.slate, 4);
|
|
53
90
|
if (/^---+\s*$/.test(line)) return ` ${A.slate}${'─'.repeat(40)}${A.reset}`;
|
|
54
91
|
if (line.trim() === '') return '';
|
|
55
|
-
return inlineMd(line, ''); // body: default foreground, inline styling only
|
|
92
|
+
return softWrap(inlineMd(line, ''), '', 0); // body: default foreground, inline styling only
|
|
56
93
|
}
|
|
57
94
|
|
|
58
95
|
// Line-buffered streaming renderer. Feed raw token text via push(); each time a
|
package/lib/ui.js
CHANGED
|
@@ -111,6 +111,72 @@ async function brandReveal(fast = false) {
|
|
|
111
111
|
console.log('');
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// ── Smart line wrap ──────────────────────────────────────
|
|
115
|
+
// The terminal's default hard wrap breaks mid-word at the window edge. This
|
|
116
|
+
// wraps at word boundaries instead (CSS `text-wrap: pretty`, in spirit),
|
|
117
|
+
// ANSI-aware: colour codes carry zero width, and the colour active at a break
|
|
118
|
+
// is re-opened on the continuation line so nothing loses its tint. A word
|
|
119
|
+
// longer than the line is left whole — we never split inside a word, which is
|
|
120
|
+
// the whole point. Per physical line; only over-long lines are touched.
|
|
121
|
+
const SGR = /\x1b\[[0-9;]*m/g;
|
|
122
|
+
const visW = (s) => s.replace(SGR, '').length;
|
|
123
|
+
|
|
124
|
+
// Active SGR state after consuming `s`, starting from `prev`. A reset clears it;
|
|
125
|
+
// any other colour code accumulates. Good enough to re-open colour at a break.
|
|
126
|
+
function sgrAfter(prev, s) {
|
|
127
|
+
let active = prev;
|
|
128
|
+
for (const code of s.match(SGR) || []) {
|
|
129
|
+
active = (code === '\x1b[0m' || code === '\x1b[m') ? '' : active + code;
|
|
130
|
+
}
|
|
131
|
+
return active;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function wrapAnsi(s, width = process.stdout.columns) {
|
|
135
|
+
if (!width || width < 24) return s;
|
|
136
|
+
return s.split('\n').map((line) => {
|
|
137
|
+
if (visW(line) <= width) return line;
|
|
138
|
+
const lead = line.match(/^ */)[0].length;
|
|
139
|
+
const pad = ' '.repeat(lead);
|
|
140
|
+
const words = line.slice(lead).split(/ +/);
|
|
141
|
+
const out = [];
|
|
142
|
+
let cur = pad, len = lead, started = false, active = '';
|
|
143
|
+
for (const word of words) {
|
|
144
|
+
const wl = visW(word);
|
|
145
|
+
if (started && len + 1 + wl > width) {
|
|
146
|
+
out.push(cur + '\x1b[0m'); // close colour cleanly at the break
|
|
147
|
+
cur = active + pad + word; // re-open it on the continuation line
|
|
148
|
+
len = lead + wl;
|
|
149
|
+
} else {
|
|
150
|
+
cur += (started ? ' ' : '') + word;
|
|
151
|
+
len += (started ? 1 : 0) + wl;
|
|
152
|
+
started = true;
|
|
153
|
+
}
|
|
154
|
+
active = sgrAfter(active, word);
|
|
155
|
+
}
|
|
156
|
+
out.push(cur);
|
|
157
|
+
return out.join('\n');
|
|
158
|
+
}).join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Monkeypatch console.log so every prose line the CLI prints wraps at word
|
|
162
|
+
// boundaries. TTY-only (piped/redirected output stays raw for tooling), and
|
|
163
|
+
// only the simple all-string case is transformed — object args pass through
|
|
164
|
+
// untouched. Idempotent. process.stdout.write paths (spinner, typewriter,
|
|
165
|
+
// streamed AI render) are intentionally left alone; they handle their own.
|
|
166
|
+
function installSmartWrap() {
|
|
167
|
+
if (!process.stdout.isTTY || console.log.__phewshWrapped) return;
|
|
168
|
+
const orig = console.log.bind(console);
|
|
169
|
+
const wrapped = (...args) => {
|
|
170
|
+
if (args.length && args.every((a) => typeof a === 'string')) {
|
|
171
|
+
orig(wrapAnsi(args.join(' ')));
|
|
172
|
+
} else {
|
|
173
|
+
orig(...args);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
wrapped.__phewshWrapped = true;
|
|
177
|
+
console.log = wrapped;
|
|
178
|
+
}
|
|
179
|
+
|
|
114
180
|
// ── Status panel ─────────────────────────────────────────
|
|
115
181
|
function statusPanel(title, rows) {
|
|
116
182
|
const maxLabel = Math.max(...rows.map(r => r[0].length));
|
|
@@ -267,6 +333,8 @@ module.exports = {
|
|
|
267
333
|
// Components
|
|
268
334
|
spinner, brandReveal, statusPanel, interopLine, divider, typewrite,
|
|
269
335
|
randomTip, TOUR_PAGES,
|
|
336
|
+
// Smart wrap
|
|
337
|
+
wrapAnsi, installSmartWrap,
|
|
270
338
|
// ANSI helpers
|
|
271
339
|
hide, show, up, clearLine, sleep,
|
|
272
340
|
};
|