blyuecode 0.8.0 → 0.8.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/core/deps.js +17 -12
- package/dist/core/media.js +40 -66
- package/dist/core/ytdlp.js +59 -1
- package/dist/ui/Chat.js +78 -4
- package/dist/ui/splash.js +2 -2
- package/package.json +1 -1
package/dist/core/deps.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
1
|
+
import { exec, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
const execAsync = promisify(exec);
|
|
@@ -77,18 +77,23 @@ export async function installMediaDeps(onOutput) {
|
|
|
77
77
|
return { ok: false, output: 'no install command for this platform' };
|
|
78
78
|
}
|
|
79
79
|
return new Promise((resolve) => {
|
|
80
|
-
const proc =
|
|
81
|
-
|
|
82
|
-
proc.stdout?.on('data', (c) => {
|
|
83
|
-
const s = c.toString();
|
|
84
|
-
buf += s;
|
|
85
|
-
onOutput?.(s);
|
|
86
|
-
});
|
|
87
|
-
proc.stderr?.on('data', (c) => {
|
|
88
|
-
const s = c.toString();
|
|
89
|
-
buf += s;
|
|
90
|
-
onOutput?.(s);
|
|
80
|
+
const proc = spawn('sh', ['-c', cmd], {
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
91
82
|
});
|
|
83
|
+
let buf = '';
|
|
84
|
+
const forward = (chunk) => {
|
|
85
|
+
const text = chunk.toString('utf8');
|
|
86
|
+
buf += text;
|
|
87
|
+
if (onOutput) {
|
|
88
|
+
for (const line of text.split(/[\r\n]+/)) {
|
|
89
|
+
const t = line.trim();
|
|
90
|
+
if (t)
|
|
91
|
+
onOutput(t);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
proc.stdout?.on('data', forward);
|
|
96
|
+
proc.stderr?.on('data', forward);
|
|
92
97
|
proc.on('exit', (code) => {
|
|
93
98
|
resolve({ ok: code === 0, output: buf.slice(-2000) });
|
|
94
99
|
});
|
package/dist/core/media.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { exec } from 'child_process';
|
|
2
|
-
import { runYtdlp } from './ytdlp.js';
|
|
3
2
|
import { promisify } from 'util';
|
|
4
3
|
import fs from 'fs';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import os from 'os';
|
|
6
|
+
import { runYtdlpStream } from './ytdlp.js';
|
|
7
7
|
const execAsync = promisify(exec);
|
|
8
8
|
const MUSIC_DIR = path.join(os.homedir(), '.blyue', 'music');
|
|
9
9
|
const VIDEO_DIR = path.join(os.homedir(), '.blyue', 'video');
|
|
@@ -35,16 +35,15 @@ export async function checkDeps() {
|
|
|
35
35
|
return { ytdlp, ffmpeg, mpv, ok: missing.length === 0, missing };
|
|
36
36
|
}
|
|
37
37
|
export async function searchYouTubeN(query, n = 5) {
|
|
38
|
-
const r = await
|
|
38
|
+
const r = await runYtdlpStream([
|
|
39
39
|
'ytsearch' + n + ':' + query,
|
|
40
40
|
'--dump-json',
|
|
41
41
|
'--no-warnings',
|
|
42
42
|
'--skip-download',
|
|
43
43
|
'--default-search', 'ytsearch' + n,
|
|
44
44
|
]);
|
|
45
|
-
if (!r.ok)
|
|
45
|
+
if (!r.ok)
|
|
46
46
|
throw new Error(r.error?.userMessage || 'yt-dlp search failed');
|
|
47
|
-
}
|
|
48
47
|
const lines = r.stdout.trim().split('\n').filter(Boolean);
|
|
49
48
|
const out = [];
|
|
50
49
|
for (const line of lines) {
|
|
@@ -65,52 +64,37 @@ export async function searchYouTubeN(query, n = 5) {
|
|
|
65
64
|
return out;
|
|
66
65
|
}
|
|
67
66
|
export async function searchYouTube(query) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
'--dump-json',
|
|
71
|
-
'--no-warnings',
|
|
72
|
-
'--skip-download',
|
|
73
|
-
'--default-search', 'ytsearch1',
|
|
74
|
-
]);
|
|
75
|
-
if (!r.ok) {
|
|
76
|
-
throw new Error(r.error?.userMessage || 'yt-dlp search failed');
|
|
77
|
-
}
|
|
78
|
-
const first = r.stdout.trim().split('\n')[0];
|
|
79
|
-
if (!first)
|
|
67
|
+
const results = await searchYouTubeN(query, 1);
|
|
68
|
+
if (results.length === 0)
|
|
80
69
|
throw new Error('no results');
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
info = JSON.parse(first);
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
throw new Error('could not parse yt-dlp output: ' + err.message);
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
id: String(info.id || ''),
|
|
90
|
-
title: String(info.title || 'unknown'),
|
|
91
|
-
duration: Number(info.duration) || 0,
|
|
92
|
-
uploader: String(info.uploader || info.channel || ''),
|
|
93
|
-
kind: 'audio',
|
|
94
|
-
};
|
|
70
|
+
return results[0];
|
|
95
71
|
}
|
|
96
72
|
export async function downloadAudio(track, onProgress) {
|
|
97
73
|
fs.mkdirSync(MUSIC_DIR, { recursive: true });
|
|
98
74
|
const outTemplate = path.join(MUSIC_DIR, '%(id)s.%(ext)s');
|
|
99
75
|
onProgress?.({ phase: 'downloading' });
|
|
100
|
-
const
|
|
76
|
+
const onLine = (line) => {
|
|
77
|
+
if (line.includes('[download]') || line.includes('ETA')) {
|
|
78
|
+
onProgress?.({ phase: 'downloading', line });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const r = await runYtdlpStream([
|
|
101
82
|
'-f', 'bestaudio[ext=m4a]/bestaudio[ext=webm]/bestaudio/best',
|
|
102
83
|
'--no-part',
|
|
103
84
|
'--no-mtime',
|
|
104
85
|
'--no-cache-dir',
|
|
105
|
-
'--socket-timeout', '
|
|
106
|
-
'--concurrent-fragments', '
|
|
86
|
+
'--socket-timeout', '15',
|
|
87
|
+
'--concurrent-fragments', '8',
|
|
107
88
|
'--buffer-size', '16K',
|
|
108
|
-
'--http-chunk-size', '
|
|
89
|
+
'--http-chunk-size', '5M',
|
|
90
|
+
'--retries', '5',
|
|
91
|
+
'--fragment-retries', '5',
|
|
92
|
+
'--extractor-args', 'youtube:player_client=android,web_safari',
|
|
109
93
|
'-o', outTemplate,
|
|
110
94
|
'--no-playlist',
|
|
111
95
|
'--no-warnings',
|
|
112
96
|
'https://www.youtube.com/watch?v=' + track.id,
|
|
113
|
-
]);
|
|
97
|
+
], onLine);
|
|
114
98
|
if (!r.ok)
|
|
115
99
|
throw new Error(r.error?.userMessage || 'yt-dlp download failed');
|
|
116
100
|
const candidates = fs
|
|
@@ -122,18 +106,6 @@ export async function downloadAudio(track, onProgress) {
|
|
|
122
106
|
return { ...track, file: candidates[0], kind: 'audio' };
|
|
123
107
|
}
|
|
124
108
|
export const VIDEO_QUALITIES = ['360p', '480p', '720p', '1080p', 'best'];
|
|
125
|
-
export function estimateSizeMb(durationSec, quality) {
|
|
126
|
-
if (!durationSec || durationSec <= 0)
|
|
127
|
-
return 0;
|
|
128
|
-
const bitrateMbps = {
|
|
129
|
-
'360p': 0.6,
|
|
130
|
-
'480p': 1.1,
|
|
131
|
-
'720p': 2.5,
|
|
132
|
-
'1080p': 5.0,
|
|
133
|
-
best: 8.0,
|
|
134
|
-
};
|
|
135
|
-
return Math.round((bitrateMbps[quality] * durationSec) / 8);
|
|
136
|
-
}
|
|
137
109
|
export function parseQuality(s) {
|
|
138
110
|
if (!s)
|
|
139
111
|
return '720p';
|
|
@@ -150,6 +122,18 @@ export function parseQuality(s) {
|
|
|
150
122
|
return '360p';
|
|
151
123
|
return '720p';
|
|
152
124
|
}
|
|
125
|
+
export function estimateSizeMb(durationSec, quality) {
|
|
126
|
+
if (!durationSec || durationSec <= 0)
|
|
127
|
+
return 0;
|
|
128
|
+
const bitrateMbps = {
|
|
129
|
+
'360p': 0.6,
|
|
130
|
+
'480p': 1.1,
|
|
131
|
+
'720p': 2.5,
|
|
132
|
+
'1080p': 5.0,
|
|
133
|
+
best: 8.0,
|
|
134
|
+
};
|
|
135
|
+
return Math.round((bitrateMbps[quality] * durationSec) / 8);
|
|
136
|
+
}
|
|
153
137
|
function formatSelector(q) {
|
|
154
138
|
if (q === 'best')
|
|
155
139
|
return 'bestvideo+bestaudio/best';
|
|
@@ -160,32 +144,22 @@ export async function downloadVideo(track, quality, onProgress) {
|
|
|
160
144
|
fs.mkdirSync(VIDEO_DIR, { recursive: true });
|
|
161
145
|
const outTemplate = path.join(VIDEO_DIR, '%(id)s.%(ext)s');
|
|
162
146
|
onProgress?.({ phase: 'downloading' });
|
|
147
|
+
const onLine = (line) => {
|
|
148
|
+
if (line.includes('[download]') || line.includes('ETA')) {
|
|
149
|
+
onProgress?.({ phase: 'downloading', line });
|
|
150
|
+
}
|
|
151
|
+
};
|
|
163
152
|
const formatSel = quality === '360p'
|
|
164
153
|
? '18/best[height<=360][ext=mp4]/best[height<=360]'
|
|
165
154
|
: quality === '480p'
|
|
166
155
|
? '22/best[height<=480][ext=mp4]/best[height<=480]'
|
|
167
156
|
: formatSelector(quality);
|
|
168
|
-
const args = [
|
|
169
|
-
'-f', formatSel,
|
|
170
|
-
'--extractor-args', 'youtube:player_client=android,web_safari',
|
|
171
|
-
'--no-part',
|
|
172
|
-
'--no-mtime',
|
|
173
|
-
'--no-cache-dir',
|
|
174
|
-
'--socket-timeout', '15',
|
|
175
|
-
'--concurrent-fragments', '8',
|
|
176
|
-
'--buffer-size', '16K',
|
|
177
|
-
'--http-chunk-size', '5M',
|
|
178
|
-
'--retries', '5',
|
|
179
|
-
'--fragment-retries', '5',
|
|
180
|
-
'-o', outTemplate,
|
|
181
|
-
'--no-playlist',
|
|
182
|
-
'--no-warnings',
|
|
183
|
-
'https://www.youtube.com/watch?v=' + track.id,
|
|
184
|
-
];
|
|
157
|
+
const args = ['-f', formatSel];
|
|
185
158
|
if (quality !== '360p' && quality !== '480p') {
|
|
186
|
-
args.
|
|
159
|
+
args.push('--merge-output-format', 'mp4');
|
|
187
160
|
}
|
|
188
|
-
|
|
161
|
+
args.push('--extractor-args', 'youtube:player_client=android,web_safari', '--no-part', '--no-mtime', '--no-cache-dir', '--socket-timeout', '15', '--concurrent-fragments', '8', '--buffer-size', '16K', '--http-chunk-size', '5M', '--retries', '5', '--fragment-retries', '5', '-o', outTemplate, '--no-playlist', '--no-warnings', 'https://www.youtube.com/watch?v=' + track.id);
|
|
162
|
+
const r = await runYtdlpStream(args, onLine);
|
|
189
163
|
if (!r.ok)
|
|
190
164
|
throw new Error(r.error?.userMessage || 'yt-dlp download failed');
|
|
191
165
|
const candidates = fs.readdirSync(VIDEO_DIR)
|
package/dist/core/ytdlp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
1
|
+
import { exec, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
@@ -110,6 +110,64 @@ export async function runYtdlp(args, opts = {}) {
|
|
|
110
110
|
return { ok: false, stdout: err.stdout || '', stderr, error: classified };
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
export async function runYtdlpStream(args, onProgress, opts = {}) {
|
|
114
|
+
const fullArgs = [...args, '--newline', '--progress'];
|
|
115
|
+
const cmd = 'yt-dlp ' + fullArgs.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(' ');
|
|
116
|
+
const started = Date.now();
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const proc = spawn('yt-dlp', fullArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
119
|
+
let stdout = '';
|
|
120
|
+
let stderr = '';
|
|
121
|
+
const onData = (chunk, toStdout) => {
|
|
122
|
+
const text = chunk.toString('utf8');
|
|
123
|
+
if (toStdout)
|
|
124
|
+
stdout += text;
|
|
125
|
+
else
|
|
126
|
+
stderr += text;
|
|
127
|
+
if (onProgress) {
|
|
128
|
+
for (const line of text.split(/[\r\n]+/)) {
|
|
129
|
+
const t = line.trim();
|
|
130
|
+
if (t)
|
|
131
|
+
onProgress(t);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
proc.stdout?.on('data', (c) => onData(c, true));
|
|
136
|
+
proc.stderr?.on('data', (c) => onData(c, false));
|
|
137
|
+
const timeout = opts.timeoutMs ?? 30 * 60 * 1000;
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
try {
|
|
140
|
+
proc.kill('SIGTERM');
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
log(`TIMEOUT ${Date.now() - started}ms ${cmd.slice(0, 200)}`);
|
|
144
|
+
resolve({
|
|
145
|
+
ok: false, stdout, stderr,
|
|
146
|
+
error: { kind: 'unknown', userMessage: 'download timed out', detail: '', autoFixable: false },
|
|
147
|
+
});
|
|
148
|
+
}, timeout);
|
|
149
|
+
proc.on('exit', (code) => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
if (code === 0) {
|
|
152
|
+
log(`OK ${Date.now() - started}ms ${cmd.slice(0, 200)}`);
|
|
153
|
+
resolve({ ok: true, stdout, stderr });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const combined = stderr || stdout;
|
|
157
|
+
const classified = classify(combined, code ?? 1);
|
|
158
|
+
log(`ERR ${Date.now() - started}ms kind=${classified.kind} code=${code} detail=${classified.detail}`);
|
|
159
|
+
resolve({ ok: false, stdout, stderr, error: classified });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
proc.on('error', (err) => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
resolve({
|
|
165
|
+
ok: false, stdout, stderr,
|
|
166
|
+
error: { kind: 'unknown', userMessage: err.message, detail: String(err), autoFixable: false },
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
113
171
|
export function updateCmd() {
|
|
114
172
|
const isTermux = !!process.env.TERMUX_VERSION || !!process.env.PREFIX?.includes('com.termux');
|
|
115
173
|
if (isTermux)
|
package/dist/ui/Chat.js
CHANGED
|
@@ -40,6 +40,48 @@ const fmtTime = (s) => {
|
|
|
40
40
|
const sec = Math.floor(s % 60);
|
|
41
41
|
return h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0') : m + ':' + String(sec).padStart(2, '0');
|
|
42
42
|
};
|
|
43
|
+
const parseYtdlpProgress = (line) => {
|
|
44
|
+
const m = line.match(/\[download\]\s+([\d.]+)%\s+of\s+~?\s*([\d.]+\s*\w+)(?:\s+at\s+([\d.]+\s*\w+\/s))?(?:\s+ETA\s+([\d:]+))?/);
|
|
45
|
+
if (!m)
|
|
46
|
+
return null;
|
|
47
|
+
const [, pct, size, speed, eta] = m;
|
|
48
|
+
const pctNum = Math.min(100, Math.max(0, parseFloat(pct)));
|
|
49
|
+
const bars = 20;
|
|
50
|
+
const filled = Math.round((pctNum / 100) * bars);
|
|
51
|
+
const bar = '━'.repeat(filled) + '─'.repeat(bars - filled);
|
|
52
|
+
const parts = [pct + '%', size.trim()];
|
|
53
|
+
if (speed)
|
|
54
|
+
parts.push(speed.trim());
|
|
55
|
+
if (eta)
|
|
56
|
+
parts.push('ETA ' + eta);
|
|
57
|
+
return bar + ' ' + parts.join(' · ');
|
|
58
|
+
};
|
|
59
|
+
const parsePkgProgress = (line) => {
|
|
60
|
+
let m = line.match(/(\d+)%\s*\[/);
|
|
61
|
+
if (m) {
|
|
62
|
+
const pct = Math.min(100, Math.max(0, parseInt(m[1], 10)));
|
|
63
|
+
const bars = 20;
|
|
64
|
+
const filled = Math.round((pct / 100) * bars);
|
|
65
|
+
return '━'.repeat(filled) + '─'.repeat(bars - filled) + ' ' + pct + '%';
|
|
66
|
+
}
|
|
67
|
+
m = line.match(/^Get:\d+\s+\S+\s+\[([\d.,]+\s*\w+)\]/);
|
|
68
|
+
if (m)
|
|
69
|
+
return '· fetching ' + m[1];
|
|
70
|
+
m = line.match(/^Setting up ([^ ]+)/);
|
|
71
|
+
if (m)
|
|
72
|
+
return '· unpacking ' + m[1];
|
|
73
|
+
m = line.match(/^Unpacking ([^ ]+)/);
|
|
74
|
+
if (m)
|
|
75
|
+
return '· unpacking ' + m[1];
|
|
76
|
+
m = line.match(/^(Selecting|Preparing) ([^ ]+)/);
|
|
77
|
+
if (m)
|
|
78
|
+
return '· ' + m[2];
|
|
79
|
+
if (line.startsWith('Reading package') || line.startsWith('Building dependency'))
|
|
80
|
+
return '· ' + line.slice(0, 60);
|
|
81
|
+
if (line.includes('Setting up') || line.includes('unpack'))
|
|
82
|
+
return '· ' + line.slice(0, 60);
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
43
85
|
const HistoryLine = ({ item }) => {
|
|
44
86
|
if (item.kind === 'user') {
|
|
45
87
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: pastelAt(0.08), bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }));
|
|
@@ -216,7 +258,14 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
216
258
|
if (pm.kind === 'audio') {
|
|
217
259
|
setMediaStatus('downloading: ' + pm.track.title + '…');
|
|
218
260
|
try {
|
|
219
|
-
const d = await downloadAudio(pm.track)
|
|
261
|
+
const d = await downloadAudio(pm.track, (p) => {
|
|
262
|
+
const line = p.line;
|
|
263
|
+
if (line) {
|
|
264
|
+
const pretty = parseYtdlpProgress(line);
|
|
265
|
+
if (pretty)
|
|
266
|
+
setMediaStatus('♪ ' + pretty);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
220
269
|
setMediaStatus('loading into player…');
|
|
221
270
|
await player.play(d);
|
|
222
271
|
setMediaStatus(null);
|
|
@@ -240,7 +289,14 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
240
289
|
else {
|
|
241
290
|
setMediaStatus('downloading ' + pm.quality + ': ' + pm.track.title + '…');
|
|
242
291
|
try {
|
|
243
|
-
const v = await downloadVideo(pm.track, pm.quality)
|
|
292
|
+
const v = await downloadVideo(pm.track, pm.quality, (p) => {
|
|
293
|
+
const line = p.line;
|
|
294
|
+
if (line) {
|
|
295
|
+
const pretty = parseYtdlpProgress(line);
|
|
296
|
+
if (pretty)
|
|
297
|
+
setMediaStatus('♪ ' + pretty);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
244
300
|
setMediaStatus('opening player…');
|
|
245
301
|
const { cleanup, notice } = await playVideo(v);
|
|
246
302
|
setMediaStatus(null);
|
|
@@ -433,9 +489,27 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
433
489
|
...h,
|
|
434
490
|
mkItem('tool', 'will run: ' + plat.installCmd),
|
|
435
491
|
mkItem('tool', '~' + plat.approximateSizeMb + ' MB via ' + plat.updateHint),
|
|
436
|
-
mkItem('tool', '
|
|
492
|
+
mkItem('tool', 'starting install…'),
|
|
437
493
|
]);
|
|
438
|
-
|
|
494
|
+
setLoading(true);
|
|
495
|
+
let lastLine = '';
|
|
496
|
+
const r = await installMediaDeps((line) => {
|
|
497
|
+
const pretty = parsePkgProgress(line);
|
|
498
|
+
if (pretty && pretty !== lastLine) {
|
|
499
|
+
lastLine = pretty;
|
|
500
|
+
setHistory((h) => [...h, mkItem('tool', ' ' + pretty)]);
|
|
501
|
+
}
|
|
502
|
+
else if (!pretty && line.length < 100) {
|
|
503
|
+
setHistory((h) => [...h, mkItem('tool', ' ' + line.slice(0, 100))]);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
setLoading(false);
|
|
507
|
+
if (r.ok) {
|
|
508
|
+
setHistory((h) => [...h, mkItem('tool', '✓ media tools installed')]);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
setHistory((h) => [...h, mkItem('error', 'install failed: ' + r.output.slice(-400))]);
|
|
512
|
+
}
|
|
439
513
|
return;
|
|
440
514
|
}
|
|
441
515
|
if (trimmed.startsWith('/image ')) {
|
package/dist/ui/splash.js
CHANGED
|
@@ -106,7 +106,7 @@ function sleep(ms) {
|
|
|
106
106
|
}
|
|
107
107
|
export async function playSplash(version) {
|
|
108
108
|
const cols = process.stdout.columns || 80;
|
|
109
|
-
process.stdout.write(HIDE_CURSOR);
|
|
109
|
+
process.stdout.write('\x1b[?1049h' + HIDE_CURSOR);
|
|
110
110
|
try {
|
|
111
111
|
for (let frame = 0; frame <= T_HOLD_END; frame++) {
|
|
112
112
|
process.stdout.write(HOME_CLEAR + buildFrame(frame, cols, version));
|
|
@@ -116,6 +116,6 @@ export async function playSplash(version) {
|
|
|
116
116
|
process.stdout.write(HOME_CLEAR);
|
|
117
117
|
}
|
|
118
118
|
finally {
|
|
119
|
-
process.stdout.write(SHOW_CURSOR);
|
|
119
|
+
process.stdout.write(SHOW_CURSOR + '\x1b[?1049l');
|
|
120
120
|
}
|
|
121
121
|
}
|