termux-dev 1.9.7 → 1.9.9
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/README.md +5 -1
- package/assets/banner.svg +1 -1
- package/assets/preview.svg +1 -1
- package/dist/cli/index.js +14 -5
- package/dist/cli/keys.js +43 -4
- package/dist/cli/prompt.js +3 -3
- package/dist/cli/typeahead.js +40 -14
- package/dist/cli/update-screen.js +2 -3
- package/dist/cli/updater.js +18 -1
- package/dist/cli/width.js +32 -0
- package/dist/core/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ Built specifically for **Android Termux**, **Windows**, **macOS**, and **Linux**
|
|
|
22
22
|
|
|
23
23
|
<br/><br/>
|
|
24
24
|
|
|
25
|
+
<a href="assets/demo.mp4"><img src="assets/demo.gif" alt="devx demo: plan, build, test, preview with a QR code" width="880" /></a>
|
|
26
|
+
|
|
27
|
+
<br/>
|
|
28
|
+
|
|
25
29
|
<img src="assets/preview.svg" alt="The devx terminal interface" width="880" />
|
|
26
30
|
|
|
27
31
|
</div>
|
|
@@ -235,7 +239,7 @@ Type `/` in the prompt to open the autocomplete command palette:
|
|
|
235
239
|
- **`@filename`** — Fast fuzzy file autocomplete and full file embedding.
|
|
236
240
|
- **`Backspace`** — Atomically deletes entire badges `[Pasted text #1]` and `[1.png 203kb]` in one press.
|
|
237
241
|
- **`Ctrl+Backspace`** / **`Alt+Backspace`** — Erase the text word by word (also while the agent works).
|
|
238
|
-
- **`Ctrl+Enter`** — While typing: start a new line (a new paragraph) instead of sending. `Alt+Enter` and `Shift+Enter` do the same where the terminal reports them; on a terminal that cannot tell them from `Enter` (a phone keyboard, for one) type `\` before `Enter`.
|
|
242
|
+
- **`Ctrl+Enter`** — While typing: start a new line (a new paragraph) instead of sending. `Alt+Enter` and `Shift+Enter` do the same where the terminal reports them; on a terminal that cannot tell them from `Enter` (a phone keyboard, for one) type `\` before `Enter`. devx asks the terminal to tell them apart only where that is known to work (kitty, Ghostty, WezTerm, iTerm2, foot, Alacritty); Termux and other terminals that read that request as a cursor or style code never get it. `DEVX_EXTENDED_KEYS=1` (or `0`) forces it on (or off).
|
|
239
243
|
- **`Ctrl+P`** / **`Ctrl+O`** — Unfold or fold the reasoning of the model (`Ctrl+P`) and the commands with their output (`Ctrl+O`) in the whole chat, also what is already above. Both are folded by default; works while the agent works and at the prompt.
|
|
240
244
|
- **`Esc`** — Stop the current answer and whatever it started (a running command); what you typed waits in the next prompt. Esc also closes every list and card.
|
|
241
245
|
- **`Ctrl+C`** — Stop current generation immediately or exit.
|
package/assets/banner.svg
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<rect width="1000" height="280" fill="url(#bg)"/>
|
|
17
17
|
</g>
|
|
18
18
|
<rect x="1.5" y="1.5" width="997" height="277" rx="18" fill="none" stroke="#23252c" stroke-width="1.5"/>
|
|
19
|
-
<text x="500.0" y="70" text-anchor="middle" font-family="'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" font-size="13" font-weight="600" letter-spacing="5" fill="#7d8896">THE TERMINAL-NATIVE AI CODING AGENT<tspan dx="16" fill="#2fe3ee" font-weight="700">v1.9.
|
|
19
|
+
<text x="500.0" y="70" text-anchor="middle" font-family="'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" font-size="13" font-weight="600" letter-spacing="5" fill="#7d8896">THE TERMINAL-NATIVE AI CODING AGENT<tspan dx="16" fill="#2fe3ee" font-weight="700">v1.9.9</tspan></text>
|
|
20
20
|
<g>
|
|
21
21
|
<text x="500.0" y="178" text-anchor="middle" font-family="'JetBrains Mono','Fira Code','SF Mono',ui-monospace,Menlo,Consolas,'DejaVu Sans Mono',monospace" font-size="112" font-weight="800" letter-spacing="14" fill="#2fe3ee">DEVX</text>
|
|
22
22
|
</g>
|
package/assets/preview.svg
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<rect x="595.2" y="574.0" width="243.2" height="19" fill="#000000"/>
|
|
33
33
|
<text x="44.8" y="62.0" class="t" fill="#b794f6">▌</text>
|
|
34
34
|
<text x="61.6" y="62.0" class="t b" fill="#f4f7fb">devx</text>
|
|
35
|
-
<text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.9.
|
|
35
|
+
<text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.9.9</text>
|
|
36
36
|
<text x="61.6" y="106.0" class="t" fill="#5f6b7a">model</text>
|
|
37
37
|
<text x="128.8" y="106.0" class="t" fill="#e5eaf1">deepseek-v4.1-flash · think high</text>
|
|
38
38
|
<text x="406.0" y="106.0" class="t" fill="#5f6b7a">/model</text>
|
package/dist/cli/index.js
CHANGED
|
@@ -69,7 +69,7 @@ import { modelAcceptsImages } from '../core/vision.js';
|
|
|
69
69
|
import { contextBreakdown } from '../core/context-stats.js';
|
|
70
70
|
import { runUsagePanel } from './usage-panel.js';
|
|
71
71
|
import { TurnComposer } from './typeahead.js';
|
|
72
|
-
import {
|
|
72
|
+
import { extendedKeysOff, normalizeKey, splitKeys } from './keys.js';
|
|
73
73
|
import { LANGUAGES, getLanguage, setLanguage, t } from '../core/i18n.js';
|
|
74
74
|
import { anthropicOnlyModels, formatFromBaseUrl, refreshFormats } from '../core/formats.js';
|
|
75
75
|
import { providerFormat } from '../providers/index.js';
|
|
@@ -594,7 +594,7 @@ process.on('exit', () => {
|
|
|
594
594
|
process.stdin.setRawMode(false);
|
|
595
595
|
}
|
|
596
596
|
if (process.stdout.isTTY)
|
|
597
|
-
process.stdout.write(
|
|
597
|
+
process.stdout.write(extendedKeysOff() + '\x1B[?1004l' + RESTORE_TERMINAL_TITLE + '\x1B[?25h');
|
|
598
598
|
resetTerminalTheme();
|
|
599
599
|
});
|
|
600
600
|
process.on('SIGINT', () => {
|
|
@@ -606,11 +606,18 @@ process.on('SIGINT', () => {
|
|
|
606
606
|
process.stdin.setRawMode(false);
|
|
607
607
|
process.stdin.pause();
|
|
608
608
|
}
|
|
609
|
-
process.stdout.write(
|
|
609
|
+
process.stdout.write(extendedKeysOff() + '\x1B[?1004l' + RESTORE_TERMINAL_TITLE + '\x1B[?25h'); // restore cursor
|
|
610
610
|
resetTerminalTheme();
|
|
611
611
|
process.exit(0);
|
|
612
612
|
}
|
|
613
613
|
});
|
|
614
|
+
/**
|
|
615
|
+
* The new version has been started (it owns the terminal now) and this process ends when it ends.
|
|
616
|
+
* Nothing else may run here: a second prompt on the same terminal takes keys from the first.
|
|
617
|
+
*/
|
|
618
|
+
function waitForRestartedDevx() {
|
|
619
|
+
return new Promise(() => { });
|
|
620
|
+
}
|
|
614
621
|
function clearTerminalScreen() {
|
|
615
622
|
if (process.stdout.isTTY) {
|
|
616
623
|
// \x1b[2J: clear screen, \x1b[3J: clear scrollback buffer (crucial for Termux/xterm), \x1b[H: cursor to home
|
|
@@ -933,7 +940,8 @@ export async function main() {
|
|
|
933
940
|
MemoryManager.enabled = config.enableMemory !== false;
|
|
934
941
|
const sessionManager = new SessionManager(config.model, planMode);
|
|
935
942
|
drawLogo(config.banner || 'full');
|
|
936
|
-
await runStartupUpdateCheck(config, { save: saveConfig })
|
|
943
|
+
if ((await runStartupUpdateCheck(config, { save: saveConfig })) === 'updated')
|
|
944
|
+
await waitForRestartedDevx();
|
|
937
945
|
history.addMessage({ role: 'system', content: await buildSystemPrompt(planMode) });
|
|
938
946
|
// The servers start while the prompt is already there (starting them takes seconds: `npx` and the
|
|
939
947
|
// network). What needs them waits for `mcpReady`: the first request, and /mcp. When they are up the
|
|
@@ -1281,7 +1289,8 @@ export async function main() {
|
|
|
1281
1289
|
continue;
|
|
1282
1290
|
}
|
|
1283
1291
|
if (cmd === '/update') {
|
|
1284
|
-
await runManualUpdate()
|
|
1292
|
+
if ((await runManualUpdate()) === 'updated')
|
|
1293
|
+
await waitForRestartedDevx();
|
|
1285
1294
|
continue;
|
|
1286
1295
|
}
|
|
1287
1296
|
if (cmd === '/help') {
|
package/dist/cli/keys.js
CHANGED
|
@@ -28,11 +28,50 @@ export function isNewlineKey(key) {
|
|
|
28
28
|
* Asks the terminal to tell Ctrl+Enter and Shift+Enter apart from Enter (a plain terminal sends
|
|
29
29
|
* the same byte for all of them). Two protocols are requested, a terminal answers the one it knows:
|
|
30
30
|
* the kitty keyboard protocol (kitty, foot, ghostty, wezterm, alacritty, ...) and xterm's
|
|
31
|
-
* modifyOtherKeys.
|
|
32
|
-
*
|
|
31
|
+
* modifyOtherKeys. While on, the keys arrive as `CSI … u`: run them through `normalizeKey`.
|
|
32
|
+
*
|
|
33
|
+
* Only sent to terminals that are known to understand them (`extendedKeysSupported`): a terminal
|
|
34
|
+
* that does not parses `CSI > 1 u` as "restore the cursor" and `CSI > 4 ; 1 m` as "underline, bold",
|
|
35
|
+
* which throws the prompt to the top of the screen and draws lines under every row (Termux, UserLAnd).
|
|
36
|
+
*/
|
|
37
|
+
const EXTENDED_KEYS_ON_SEQ = '\x1b[>1u\x1b[>4;1m';
|
|
38
|
+
const EXTENDED_KEYS_OFF_SEQ = '\x1b[<u\x1b[>4;0m';
|
|
39
|
+
const KEYBOARD_PROTOCOL_PROGRAMS = new Set(['ghostty', 'wezterm', 'iterm.app', 'kitty', 'foot', 'alacritty', 'rio', 'contour']);
|
|
40
|
+
/**
|
|
41
|
+
* Does this terminal understand the extended keyboard modes? Decided from the environment, and
|
|
42
|
+
* only for terminals known to: a wrong guess in the other direction costs Ctrl+Enter (Alt+Enter
|
|
43
|
+
* and a backslash before Enter still break the line), a wrong guess in this one wrecks the screen.
|
|
44
|
+
* `DEVX_EXTENDED_KEYS=1` forces it on, `DEVX_EXTENDED_KEYS=0` off.
|
|
33
45
|
*/
|
|
34
|
-
export
|
|
35
|
-
|
|
46
|
+
export function extendedKeysSupported(env = process.env, platform = process.platform) {
|
|
47
|
+
const forced = (env.DEVX_EXTENDED_KEYS ?? '').trim().toLowerCase();
|
|
48
|
+
if (['1', 'on', 'true', 'yes'].includes(forced))
|
|
49
|
+
return true;
|
|
50
|
+
if (['0', 'off', 'false', 'no'].includes(forced))
|
|
51
|
+
return false;
|
|
52
|
+
// Android: Termux, UserLAnd and other terminals built on the same emulator.
|
|
53
|
+
if (platform === 'android' || env.TERMUX_VERSION || env.PREFIX?.includes('com.termux') || env.ANDROID_ROOT || env.ANDROID_DATA)
|
|
54
|
+
return false;
|
|
55
|
+
// Multiplexers sit between devx and the real terminal: they decide for themselves.
|
|
56
|
+
if (env.TMUX || env.STY)
|
|
57
|
+
return false;
|
|
58
|
+
const term = (env.TERM ?? '').toLowerCase();
|
|
59
|
+
if (term.startsWith('screen') || term.startsWith('tmux') || term === 'linux' || term === 'dumb')
|
|
60
|
+
return false;
|
|
61
|
+
if (env.KITTY_WINDOW_ID || env.GHOSTTY_RESOURCES_DIR || env.WEZTERM_EXECUTABLE)
|
|
62
|
+
return true;
|
|
63
|
+
if (/kitty|ghostty|foot|alacritty|wezterm/.test(term))
|
|
64
|
+
return true;
|
|
65
|
+
return KEYBOARD_PROTOCOL_PROGRAMS.has((env.TERM_PROGRAM ?? '').toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
/** What to write to turn the extended keyboard modes on (nothing on a terminal that does not know them). */
|
|
68
|
+
export function extendedKeysOn() {
|
|
69
|
+
return extendedKeysSupported() ? EXTENDED_KEYS_ON_SEQ : '';
|
|
70
|
+
}
|
|
71
|
+
/** What to write to turn them off again. */
|
|
72
|
+
export function extendedKeysOff() {
|
|
73
|
+
return extendedKeysSupported() ? EXTENDED_KEYS_OFF_SEQ : '';
|
|
74
|
+
}
|
|
36
75
|
/**
|
|
37
76
|
* Turns the extended encodings back into the keys the rest of the code knows:
|
|
38
77
|
* `CSI 99;5u` and `CSI 27;5;99~` become Ctrl+C (0x03), `CSI 27u` becomes Esc, and every way
|
package/dist/cli/prompt.js
CHANGED
|
@@ -8,7 +8,7 @@ import { filterCommandPalette, estimatePromptTokens, formatPromptTokenBadge } fr
|
|
|
8
8
|
import { PromptHistoryNavigator } from './history_search.js';
|
|
9
9
|
import { buildPromptFrame, moveCursorRow, palette, renderUserEcho, resolveMode } from './ui.js';
|
|
10
10
|
import { displayWidth, padEndWidth, rowsToTop, truncateWidth } from './width.js';
|
|
11
|
-
import {
|
|
11
|
+
import { extendedKeysOff, extendedKeysOn, isNewlineKey, normalizeKey } from './keys.js';
|
|
12
12
|
export const SLASH_COMMANDS = [
|
|
13
13
|
{ cmd: '/new', desc: 'Start a new clean chat session' },
|
|
14
14
|
{ cmd: '/resume', desc: 'Resume a previous chat session' },
|
|
@@ -99,7 +99,7 @@ export function askPrompt(opts = {}) {
|
|
|
99
99
|
process.stdin.setRawMode(true);
|
|
100
100
|
process.stdout.write('\x1b[?2004h'); // Enable bracketed paste mode
|
|
101
101
|
// Lets the terminal tell Ctrl+Enter and Shift+Enter (a new paragraph) from Enter (send).
|
|
102
|
-
process.stdout.write(
|
|
102
|
+
process.stdout.write(extendedKeysOn());
|
|
103
103
|
}
|
|
104
104
|
// ── framed prompt (clean layout) ──
|
|
105
105
|
// The frame is a block of rows; the cursor rests on the input row. Redrawing moves up
|
|
@@ -552,7 +552,7 @@ export function askPrompt(opts = {}) {
|
|
|
552
552
|
disposed = true;
|
|
553
553
|
unsubscribeOwner?.();
|
|
554
554
|
if (process.stdout.isTTY) {
|
|
555
|
-
process.stdout.write('\x1b[?2004l' +
|
|
555
|
+
process.stdout.write('\x1b[?2004l' + extendedKeysOff());
|
|
556
556
|
}
|
|
557
557
|
process.stdout.removeListener('resize', onResize);
|
|
558
558
|
process.stdin.removeListener('data', onData);
|
package/dist/cli/typeahead.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { extendedKeysOff, extendedKeysOn, isNewlineKey, normalizeKey, splitKeys } from './keys.js';
|
|
5
5
|
import { buildPromptFrame, palette } from './ui.js';
|
|
6
|
-
import { displayWidth, truncateWidth } from './width.js';
|
|
6
|
+
import { columnAfter, displayWidth, truncateWidth } from './width.js';
|
|
7
7
|
import { t } from '../core/i18n.js';
|
|
8
8
|
/** The down arrow in every form a terminal may send it: `ESC [ B`, `ESC O B` (application mode), `ESC [ 1 ; mods B` (with modifier fields). */
|
|
9
9
|
const DOWN_ARROW = /^\x1b(?:\[|O)(?:1;[\d:]+)?B$/;
|
|
@@ -153,6 +153,8 @@ export class TurnComposer {
|
|
|
153
153
|
state = { text: '', cursor: 0 };
|
|
154
154
|
queue = [];
|
|
155
155
|
lines = [];
|
|
156
|
+
/** The column the output cursor is in: the frame is drawn under it and the cursor comes back to this column. */
|
|
157
|
+
col = 0;
|
|
156
158
|
original = null;
|
|
157
159
|
timer = null;
|
|
158
160
|
/** Called on ctrl+p (thinking) and ctrl+o (tool output). */
|
|
@@ -207,9 +209,10 @@ export class TurnComposer {
|
|
|
207
209
|
this.active = true;
|
|
208
210
|
this.suspended = false;
|
|
209
211
|
this.shown = false;
|
|
212
|
+
this.col = 0;
|
|
210
213
|
this.original = this.out.write.bind(this.out);
|
|
211
214
|
this.out.write = ((chunk, encoding, cb) => this.through(chunk, encoding, cb));
|
|
212
|
-
this.write(
|
|
215
|
+
this.write(extendedKeysOn() + this.drawSeq());
|
|
213
216
|
// The working line animates: its time and counter are redrawn while the output is quiet.
|
|
214
217
|
if (this.status) {
|
|
215
218
|
this.timer = setInterval(() => this.tick(), 60);
|
|
@@ -225,7 +228,7 @@ export class TurnComposer {
|
|
|
225
228
|
this.timer = null;
|
|
226
229
|
}
|
|
227
230
|
if (this.active) {
|
|
228
|
-
this.write(this.eraseSeq() +
|
|
231
|
+
this.write(this.eraseSeq() + extendedKeysOff() + '\x1b[?25h');
|
|
229
232
|
if (this.original)
|
|
230
233
|
this.out.write = this.original;
|
|
231
234
|
this.original = null;
|
|
@@ -241,14 +244,14 @@ export class TurnComposer {
|
|
|
241
244
|
if (!this.active || this.suspended)
|
|
242
245
|
return;
|
|
243
246
|
// The card or question that takes over reads plain keys: the extended encodings go off for it.
|
|
244
|
-
this.write(this.eraseSeq() +
|
|
247
|
+
this.write(this.eraseSeq() + extendedKeysOff());
|
|
245
248
|
this.suspended = true;
|
|
246
249
|
}
|
|
247
250
|
resume() {
|
|
248
251
|
if (!this.active || !this.suspended)
|
|
249
252
|
return;
|
|
250
253
|
this.suspended = false;
|
|
251
|
-
this.write(
|
|
254
|
+
this.write(extendedKeysOn() + this.drawSeq());
|
|
252
255
|
}
|
|
253
256
|
/** Builds the frame again (the context percentage or the cost changed) and draws it. */
|
|
254
257
|
refresh() {
|
|
@@ -298,17 +301,27 @@ export class TurnComposer {
|
|
|
298
301
|
/** The write that replaces the terminal's own for the length of the turn. */
|
|
299
302
|
through(chunk, encoding, cb) {
|
|
300
303
|
const raw = this.original;
|
|
301
|
-
|
|
304
|
+
const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
305
|
+
if (!this.active || this.suspended) {
|
|
306
|
+
this.advanceColumn(text);
|
|
302
307
|
return raw(chunk, encoding, cb);
|
|
308
|
+
}
|
|
309
|
+
// The old frame is wiped from the column the cursor is in now; the new one comes back to the column the text ends in.
|
|
310
|
+
const erase = this.eraseSeq();
|
|
311
|
+
this.advanceColumn(text);
|
|
303
312
|
if (typeof chunk === 'string') {
|
|
304
313
|
// The callback form `write(chunk, cb)` has the callback in the second place.
|
|
305
|
-
return raw(
|
|
314
|
+
return raw(erase + chunk + this.drawSeq(), encoding, cb);
|
|
306
315
|
}
|
|
307
|
-
raw(
|
|
316
|
+
raw(erase);
|
|
308
317
|
const result = raw(chunk, encoding, cb);
|
|
309
318
|
raw(this.drawSeq());
|
|
310
319
|
return result;
|
|
311
320
|
}
|
|
321
|
+
/** Where the cursor is after this text (also while a card has the screen, so that the frame comes back to the right column). */
|
|
322
|
+
advanceColumn(text) {
|
|
323
|
+
this.col = columnAfter(this.col, text, this.out.columns ?? 80);
|
|
324
|
+
}
|
|
312
325
|
write(text) {
|
|
313
326
|
if (text)
|
|
314
327
|
(this.original ?? this.out.write.bind(this.out))(text);
|
|
@@ -339,12 +352,25 @@ export class TurnComposer {
|
|
|
339
352
|
oneRow(line) {
|
|
340
353
|
return line.replace(/[\r\n\x00-\x08\x0b-\x1a\x1c-\x1f]+/g, ' ');
|
|
341
354
|
}
|
|
342
|
-
/**
|
|
355
|
+
/** Puts the cursor back in the column of the output, on the row it is on. */
|
|
356
|
+
backToColumn() {
|
|
357
|
+
const cols = Math.max(1, this.out.columns ?? 80);
|
|
358
|
+
return `\x1b[${Math.min(this.col, cols - 1) + 1}G`;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Wipes the frame: everything under the cursor row. The cursor stays on its text.
|
|
362
|
+
*
|
|
363
|
+
* Only relative moves are used, no saved cursor (ESC 7 / ESC 8): the saved position is a screen row,
|
|
364
|
+
* and when the window changes size in the middle of a write (the keyboard of a phone comes and goes)
|
|
365
|
+
* some terminals do not move it with the text, and the cursor comes back to the wrong row.
|
|
366
|
+
*/
|
|
343
367
|
eraseSeq() {
|
|
344
368
|
if (!this.shown)
|
|
345
369
|
return '';
|
|
346
370
|
this.shown = false;
|
|
347
|
-
|
|
371
|
+
// The erase starts from plain attributes: a style left on by the text above (an underline, a
|
|
372
|
+
// background) would be painted into every row it wipes (stripes across the screen).
|
|
373
|
+
return '\x1b[0m\x1b[1B\r\x1b[J\x1b[1A' + this.backToColumn();
|
|
348
374
|
}
|
|
349
375
|
/** Draws the frame under the cursor row and puts the cursor back where the text goes on. */
|
|
350
376
|
drawSeq() {
|
|
@@ -356,9 +382,9 @@ export class TurnComposer {
|
|
|
356
382
|
// The real cursor only marks where the output goes on. Whatever showed it in the meantime
|
|
357
383
|
// (an approval card, a spinner that stopped) must not leave it blinking under the output.
|
|
358
384
|
// ESC D moves down (scrolling at the bottom edge) and keeps the column; then back up.
|
|
359
|
-
let seq = '\x1b[?25l' + '\x1bD'.repeat(n) + `\x1b[${n}A
|
|
385
|
+
let seq = '\x1b[?25l' + '\x1bD'.repeat(n) + `\x1b[${n}A`;
|
|
360
386
|
for (const line of lines)
|
|
361
|
-
seq += `\x1b[1B\r\x1b[2K${line}\x1b[0m`;
|
|
362
|
-
return seq +
|
|
387
|
+
seq += `\x1b[0m\x1b[1B\r\x1b[2K${line}\x1b[0m`;
|
|
388
|
+
return seq + `\x1b[${n}A` + this.backToColumn();
|
|
363
389
|
}
|
|
364
390
|
}
|
|
@@ -350,14 +350,13 @@ export async function runStartupUpdateCheck(config, deps = {}) {
|
|
|
350
350
|
if (result.updateAvailable) {
|
|
351
351
|
if (config.skippedUpdate === result.latestVersion)
|
|
352
352
|
return;
|
|
353
|
-
|
|
353
|
+
return presentUpdate(result, {
|
|
354
354
|
offerSkip: true,
|
|
355
355
|
save: async (version) => {
|
|
356
356
|
config.skippedUpdate = version;
|
|
357
357
|
await deps.save?.(config);
|
|
358
358
|
},
|
|
359
359
|
}, deps);
|
|
360
|
-
return;
|
|
361
360
|
}
|
|
362
361
|
if (result.error)
|
|
363
362
|
return; // offline at start-up is not worth a word
|
|
@@ -375,7 +374,7 @@ export async function runManualUpdate(deps = {}) {
|
|
|
375
374
|
const result = await withCheckingLine(out, current, () => (deps.check ?? checkForUpdates)(10000));
|
|
376
375
|
const cols = columnsOf(out);
|
|
377
376
|
if (result.updateAvailable) {
|
|
378
|
-
|
|
377
|
+
return presentUpdate(result, { offerSkip: false }, deps);
|
|
379
378
|
}
|
|
380
379
|
else if (result.error) {
|
|
381
380
|
out.write(renderNotice('warn', t('update.offline', { reason: result.error }), cols) + '\n');
|
package/dist/cli/updater.js
CHANGED
|
@@ -280,9 +280,26 @@ export async function runUpdateSteps(steps, events = {}, now = Date.now) {
|
|
|
280
280
|
}
|
|
281
281
|
return { ok: true };
|
|
282
282
|
}
|
|
283
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Starts devx again, with the same arguments, and ends this process when the new one ends.
|
|
285
|
+
*
|
|
286
|
+
* From here on this process only waits: the terminal belongs to the new one. It stops reading the
|
|
287
|
+
* keyboard (two programs reading one terminal take the keys from each other, and a typed line
|
|
288
|
+
* can end up in the shell) and no longer reacts to Ctrl+C itself (the new devx gets it).
|
|
289
|
+
*/
|
|
284
290
|
export function restartDevx() {
|
|
291
|
+
try {
|
|
292
|
+
process.stdin.pause();
|
|
293
|
+
}
|
|
294
|
+
catch { }
|
|
285
295
|
const child = spawn(process.argv[0], process.argv.slice(1), { stdio: 'inherit' });
|
|
296
|
+
process.removeAllListeners('SIGINT');
|
|
297
|
+
process.on('SIGINT', () => { });
|
|
298
|
+
process.removeAllListeners('SIGTERM');
|
|
299
|
+
process.on('SIGTERM', () => { try {
|
|
300
|
+
child.kill('SIGTERM');
|
|
301
|
+
}
|
|
302
|
+
catch { } });
|
|
286
303
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
287
304
|
child.on('error', () => process.exit(0));
|
|
288
305
|
}
|
package/dist/cli/width.js
CHANGED
|
@@ -289,3 +289,35 @@ export function rowsToTop(lineWidths, cursorRow, cursorCol, cols) {
|
|
|
289
289
|
rows += Math.max(1, Math.ceil(lineWidths[i] / width));
|
|
290
290
|
return rows + Math.floor(Math.max(0, cursorCol) / width);
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* The column (0-based, in cells) the cursor is in after `text` is written starting at column `col`
|
|
294
|
+
* of a terminal `cols` wide. Escape sequences take no room; a line break and a carriage return go
|
|
295
|
+
* to column 0; a character that does not fit wraps to the next row. A cursor that has just filled
|
|
296
|
+
* the last column is reported as `cols` (the terminal holds it there until the next character).
|
|
297
|
+
*/
|
|
298
|
+
export function columnAfter(col, text, cols) {
|
|
299
|
+
const width = Math.max(1, cols);
|
|
300
|
+
let at = Math.max(0, Math.min(col, width));
|
|
301
|
+
const plain = text.replace(ANSI, '').replace(/\x1b[0-9A-Za-z=>]/g, '');
|
|
302
|
+
for (const ch of plain) {
|
|
303
|
+
if (ch === '\n' || ch === '\r') {
|
|
304
|
+
at = 0;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (ch === '\b') {
|
|
308
|
+
at = Math.max(0, at - 1);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (ch === '\t') {
|
|
312
|
+
at = Math.min(width - 1, (Math.floor(at / 8) + 1) * 8);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const w = codePointWidth(ch.codePointAt(0));
|
|
316
|
+
if (w === 0)
|
|
317
|
+
continue;
|
|
318
|
+
if (at + w > width)
|
|
319
|
+
at = 0;
|
|
320
|
+
at += w;
|
|
321
|
+
}
|
|
322
|
+
return at;
|
|
323
|
+
}
|
package/dist/core/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const DEVX_VERSION = '1.9.
|
|
1
|
+
export const DEVX_VERSION = '1.9.9';
|
package/package.json
CHANGED