termux-dev 1.8.0 → 1.8.1

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/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.8.0</tspan></text>
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.8.1</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>
@@ -9,9 +9,9 @@
9
9
  .i { font-style: italic; }
10
10
  .cur { animation: blink 1.1s steps(1) infinite; }
11
11
  @keyframes blink { 0%,55% { opacity: 1 } 56%,100% { opacity: 0 } }
12
- .spin { animation: pulse 1.6s ease-in-out infinite; }
13
- @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
14
- @media (prefers-reduced-motion: reduce) { .cur, .spin { animation: none } }
12
+ .sp { opacity: 0; animation: spin .8s steps(1) infinite; }
13
+ @keyframes spin { 0% { opacity: 1 } 10% { opacity: 0 } 100% { opacity: 0 } }
14
+ @media (prefers-reduced-motion: reduce) { .cur { animation: none } .sp { animation: none } .sp.f0 { opacity: 1 } }
15
15
  </style>
16
16
  <rect x="8" y="8" width="924" height="642" rx="14" fill="#000000"/>
17
17
  <g clip-path="url(#clip)">
@@ -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.8.0</text>
35
+ <text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.8.1</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>
@@ -69,7 +69,16 @@
69
69
  <text x="70.0" y="458.0" class="t" fill="#5f6b7a">╰</text>
70
70
  <text x="86.8" y="458.0" class="t" fill="#9aa6b4">24 passed</text>
71
71
  <text x="86.8" y="480.0" class="t" fill="#5f6b7a">… +3 lines (ctrl+o to expand)</text>
72
- <text x="44.8" y="512.0" class="t spin" fill="#5ed3e0">⠹</text>
72
+ <text x="44.8" y="512.0" class="t sp f0" style="animation-delay:0.00s" fill="#5ed3e0">⠋</text>
73
+ <text x="44.8" y="512.0" class="t sp f1" style="animation-delay:0.08s" fill="#5ed3e0">⠙</text>
74
+ <text x="44.8" y="512.0" class="t sp f2" style="animation-delay:0.16s" fill="#5ed3e0">⠹</text>
75
+ <text x="44.8" y="512.0" class="t sp f3" style="animation-delay:0.24s" fill="#5ed3e0">⠸</text>
76
+ <text x="44.8" y="512.0" class="t sp f4" style="animation-delay:0.32s" fill="#5ed3e0">⠼</text>
77
+ <text x="44.8" y="512.0" class="t sp f5" style="animation-delay:0.40s" fill="#5ed3e0">⠴</text>
78
+ <text x="44.8" y="512.0" class="t sp f6" style="animation-delay:0.48s" fill="#5ed3e0">⠦</text>
79
+ <text x="44.8" y="512.0" class="t sp f7" style="animation-delay:0.56s" fill="#5ed3e0">⠧</text>
80
+ <text x="44.8" y="512.0" class="t sp f8" style="animation-delay:0.64s" fill="#5ed3e0">⠇</text>
81
+ <text x="44.8" y="512.0" class="t sp f9" style="animation-delay:0.72s" fill="#5ed3e0">⠏</text>
73
82
  <text x="61.6" y="512.0" class="t" fill="#e5eaf1">Writing</text>
74
83
  <text x="145.6" y="512.0" class="t" fill="#5f6b7a">(12s · ↓ 1.4k tokens · ctrl+c to stop)</text>
75
84
  <text x="44.8" y="544.0" class="t" fill="#5ed3e0">●</text>
@@ -1,5 +1,6 @@
1
1
  import pc from 'picocolors';
2
2
  import { getCurrentTheme } from './theme.js';
3
+ import { DEVX_LOGO } from './logo.js';
3
4
  import { DEVX_VERSION } from '../core/types.js';
4
5
  export function renderBannerLines(mode = 'full', version = DEVX_VERSION, cols = 80) {
5
6
  if (mode === 'off') {
@@ -18,9 +19,7 @@ export function renderBannerLines(mode = 'full', version = DEVX_VERSION, cols =
18
19
  if (cols < 68) {
19
20
  // Compact ASCII for narrow mobile screens (~26 chars)
20
21
  return [
21
- theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
22
- theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
23
- theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
22
+ ...DEVX_LOGO.map((l) => ' ' + theme.colorFn(l)),
24
23
  ' ' + theme.boldFn(`v${version}`)
25
24
  ];
26
25
  }
@@ -0,0 +1,241 @@
1
+ import { LiveBlock } from './liveblock.js';
2
+ import { WORD_BACKSPACE, normalizeKey, splitKeys } from './keys.js';
3
+ import { windowItems } from './picker.js';
4
+ import { palette } from './ui.js';
5
+ import { displayWidth, padEndWidth, tailWidth, truncateWidth, wrapWords } from './width.js';
6
+ import { t } from '../core/i18n.js';
7
+ /** Every word typed must be found in the label, the id or the keywords; matches at the start of the label come first. */
8
+ export function filterItems(items, query) {
9
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
10
+ const pinned = items.filter((i) => i.pinned);
11
+ const rest = items.filter((i) => !i.pinned);
12
+ if (words.length === 0)
13
+ return [...pinned, ...rest];
14
+ const scored = [];
15
+ for (const item of rest) {
16
+ const hay = `${item.label} ${item.keywords ?? ''}`.toLowerCase();
17
+ if (!words.every((w) => hay.includes(w)))
18
+ continue;
19
+ const label = item.label.toLowerCase();
20
+ scored.push({ item, score: label.startsWith(words[0]) ? 0 : label.includes(words[0]) ? 1 : 2 });
21
+ }
22
+ scored.sort((a, b) => a.score - b.score); // stable: the original order is kept inside a score
23
+ return [...pinned, ...scored.map((s) => s.item)];
24
+ }
25
+ function hintLine(width, typing) {
26
+ const variants = [
27
+ typing
28
+ ? [['type', t('finder.hint.filter')], ['↑↓', t('picker.hint.move')], ['enter', t('picker.hint.select')], ['esc', t('picker.hint.cancel')]]
29
+ : [['enter', t('picker.hint.confirm')], ['esc', t('picker.hint.cancel')]],
30
+ [['↑↓', ''], ['enter', ''], ['esc', '']],
31
+ [['enter', ''], ['esc', '']],
32
+ ];
33
+ for (const items of variants) {
34
+ const text = items.map(([k, label]) => palette.text(k) + (label ? ' ' + palette.faint(label) : '')).join(palette.faint(items[0][1] ? ' · ' : ' '));
35
+ if (displayWidth(text) <= width)
36
+ return text;
37
+ }
38
+ return '';
39
+ }
40
+ /** The card around some content rows; `rows` are already padded to the inner width by the caller's `box`. */
41
+ function frame(title, subtitle, cols, body, footer) {
42
+ if (cols < 60) {
43
+ const width = Math.max(20, cols - 1);
44
+ const head = ' ' + palette.bold(palette.accent(title)) + (subtitle ? palette.faint(' · ' + truncateWidth(subtitle, Math.max(6, width - displayWidth(title) - 6))) : '');
45
+ const plain = (c, shaded = false) => (shaded ? palette.shade(padEndWidth(' ' + c, width)) : ' ' + c);
46
+ return [head, ...body(width - 1, plain), ' ' + footer];
47
+ }
48
+ const W = Math.min(cols - 1, 76);
49
+ const iw = W - 4;
50
+ const rule = palette.rule;
51
+ const box = (content, shaded = false) => rule('│') + (shaded ? palette.shade(' ' + padEndWidth(content, iw) + ' ') : ' ' + padEndWidth(content, iw) + ' ') + rule('│');
52
+ const head = ' ' + palette.bold(palette.accent(title)) + ' ';
53
+ const sub = subtitle ? ' ' + palette.muted(truncateWidth(subtitle, Math.max(6, W - 12 - displayWidth(title)))) + ' ' : '';
54
+ return [
55
+ rule('╭─') + head + rule('─'.repeat(Math.max(0, W - 4 - displayWidth(head) - displayWidth(sub)))) + sub + rule('─╮'),
56
+ box(''),
57
+ ...body(iw, box),
58
+ rule('╰' + '─'.repeat(W - 2) + '╯'),
59
+ ' ' + footer,
60
+ ];
61
+ }
62
+ /** Where the focus starts: on the first match, not on a pinned row such as "type a name". */
63
+ export function firstMatch(shown) {
64
+ const i = shown.findIndex((item) => !item.pinned);
65
+ return i === -1 ? 0 : i;
66
+ }
67
+ export function renderFinder(options, query, shown, focus, cols, termRows = 40) {
68
+ const maxItems = Math.max(3, termRows - 9 - (options.notes?.length ?? 0));
69
+ return frame(options.title, options.subtitle ?? `${shown.filter((i) => !i.pinned).length}/${options.items.filter((i) => !i.pinned).length}`, cols, (iw, box) => {
70
+ const lines = [];
71
+ const typed = query ? palette.text(tailWidth(query, Math.max(4, iw - 6))) : palette.faint(truncateWidth(options.placeholder ?? t('finder.placeholder'), Math.max(4, iw - 6)));
72
+ lines.push(box(' ' + palette.accent('/') + ' ' + (query ? typed + palette.accent('█') : palette.accent('█') + typed)));
73
+ lines.push(box(palette.rule('─'.repeat(Math.max(0, iw - 1)))));
74
+ if (shown.length === 0) {
75
+ lines.push(box(' ' + palette.faint(t('finder.empty'))));
76
+ }
77
+ else {
78
+ const labelWidth = Math.min(Math.min(options.labelWidth ?? 24, Math.max(...shown.map((i) => displayWidth(i.label)), 4)) + 2, Math.max(10, iw - 12));
79
+ const rows = shown.map((item, index) => {
80
+ const focused = index === focus;
81
+ const isCurrent = options.current !== undefined && item.value === options.current;
82
+ const mark = focused ? palette.accent('❯') : ' ';
83
+ const dot = item.pinned ? palette.accent('+') : isCurrent ? palette.accent('●') : palette.faint('○');
84
+ const label = focused ? palette.bold(palette.text(truncateWidth(item.label, labelWidth - 1))) : palette.muted(truncateWidth(item.label, labelWidth - 1));
85
+ const prefix = ` ${mark} ${dot} ${padEndWidth(label, labelWidth)}`;
86
+ const room = Math.max(0, iw - displayWidth(prefix) - 1);
87
+ const detail = item.detail && room > 6 ? palette.faint(truncateWidth(item.detail, room)) : '';
88
+ return { text: prefix + detail, index };
89
+ });
90
+ for (const r of windowItems(rows, focus, maxItems, iw))
91
+ lines.push(box(r.text, r.index === focus));
92
+ }
93
+ const notes = (options.notes ?? []).flatMap((n) => wrapWords(n, iw - 3));
94
+ if (notes.length > 0) {
95
+ lines.push(box(''));
96
+ for (const line of notes)
97
+ lines.push(box(' ' + palette.faint(line)));
98
+ }
99
+ return lines;
100
+ }, hintLine(cols - 2, true));
101
+ }
102
+ /** What a key does to a text that is being typed: `undefined` when the key is not an edit. */
103
+ export function editText(value, key) {
104
+ if (key === '\x7f' || key === '\b')
105
+ return Array.from(value).slice(0, -1).join('');
106
+ if (key === WORD_BACKSPACE)
107
+ return value.replace(/\s*\S*$/, '');
108
+ if (key === '\x15')
109
+ return ''; // Ctrl+U
110
+ if (/^[^\x00-\x1f\x7f\x1b]+$/.test(key))
111
+ return value + key; // printable text, also a pasted chunk
112
+ return undefined;
113
+ }
114
+ function openIO(io) {
115
+ const { input } = io;
116
+ const wasRaw = !!input.isRaw;
117
+ input.setRawMode?.(true);
118
+ input.resume();
119
+ return () => { if (!wasRaw)
120
+ input.setRawMode?.(false); };
121
+ }
122
+ /** The list narrowed by typing. Resolves with the chosen value, or undefined when cancelled. */
123
+ export function runFinder(options, io = { input: process.stdin, output: process.stdout }) {
124
+ return new Promise((resolve) => {
125
+ const { input, output } = io;
126
+ const block = new LiveBlock(output);
127
+ let query = '';
128
+ let shown = filterItems(options.items, query);
129
+ const at = shown.findIndex((i) => options.current !== undefined && i.value === options.current);
130
+ let focus = at >= 0 ? at : firstMatch(shown);
131
+ let done = false;
132
+ const rows = () => output.rows || 40;
133
+ const draw = () => block.draw(renderFinder(options, query, shown, focus, output.columns || 80, rows()));
134
+ const restore = openIO(io);
135
+ const finish = (value) => {
136
+ done = true;
137
+ input.off('data', onData);
138
+ block.erase();
139
+ restore();
140
+ resolve(value);
141
+ };
142
+ const move = (delta) => {
143
+ const next = Math.max(0, Math.min(shown.length - 1, focus + delta));
144
+ if (next !== focus) {
145
+ focus = next;
146
+ draw();
147
+ }
148
+ };
149
+ const handleKey = (raw) => {
150
+ const key = normalizeKey(raw);
151
+ if (key === '\x1b[A' || key === '\x1bOA')
152
+ return move(-1);
153
+ if (key === '\x1b[B' || key === '\x1bOB')
154
+ return move(1);
155
+ if (key === '\x1b[5~')
156
+ return move(-8);
157
+ if (key === '\x1b[6~')
158
+ return move(8);
159
+ if (key === '\r' || key === '\n')
160
+ return shown.length > 0 ? finish(shown[focus].value) : undefined;
161
+ if (key === '\x1b' || key === '\x03')
162
+ return finish(undefined);
163
+ const edited = editText(query, key);
164
+ if (edited !== undefined && edited !== query) {
165
+ query = edited;
166
+ shown = filterItems(options.items, query);
167
+ focus = firstMatch(shown);
168
+ draw();
169
+ }
170
+ };
171
+ const onData = (chunk) => {
172
+ for (const key of splitKeys(chunk.toString())) {
173
+ if (done)
174
+ return;
175
+ handleKey(key);
176
+ }
177
+ };
178
+ input.on('data', onData);
179
+ draw();
180
+ });
181
+ }
182
+ export function renderTextInput(options, value, cols) {
183
+ return frame(options.title, options.subtitle, cols, (iw, box) => {
184
+ const lines = [];
185
+ for (const line of wrapWords(options.label, iw - 2))
186
+ lines.push(box(' ' + palette.bold(palette.text(line))));
187
+ const shownValue = options.secret ? '•'.repeat(Array.from(value).length) : value;
188
+ const room = Math.max(4, iw - 6);
189
+ const text = value
190
+ ? palette.text(tailWidth(shownValue, room)) + palette.accent('█')
191
+ : palette.accent('█') + palette.faint(truncateWidth(options.placeholder ?? '', room));
192
+ lines.push(box(''));
193
+ lines.push(box(' ' + palette.accent('❯') + ' ' + text, true));
194
+ const notes = (options.notes ?? []).flatMap((n) => wrapWords(n, iw - 3));
195
+ if (notes.length > 0) {
196
+ lines.push(box(''));
197
+ for (const line of notes)
198
+ lines.push(box(' ' + palette.faint(line)));
199
+ }
200
+ return lines;
201
+ }, hintLine(cols - 2, false));
202
+ }
203
+ /** A one-line input. Resolves with the text (never empty), or undefined when cancelled. */
204
+ export function runTextInput(options, io = { input: process.stdin, output: process.stdout }) {
205
+ return new Promise((resolve) => {
206
+ const { input, output } = io;
207
+ const block = new LiveBlock(output);
208
+ let value = options.initial ?? '';
209
+ let done = false;
210
+ const draw = () => block.draw(renderTextInput(options, value, output.columns || 80));
211
+ const restore = openIO(io);
212
+ const finish = (result) => {
213
+ done = true;
214
+ input.off('data', onData);
215
+ block.erase();
216
+ restore();
217
+ resolve(result);
218
+ };
219
+ const handleKey = (raw) => {
220
+ const key = normalizeKey(raw);
221
+ if (key === '\r' || key === '\n')
222
+ return value.trim() ? finish(value.trim()) : undefined;
223
+ if (key === '\x1b' || key === '\x03')
224
+ return finish(undefined);
225
+ const edited = editText(value, key);
226
+ if (edited !== undefined && edited !== value) {
227
+ value = edited;
228
+ draw();
229
+ }
230
+ };
231
+ const onData = (chunk) => {
232
+ for (const key of splitKeys(chunk.toString())) {
233
+ if (done)
234
+ return;
235
+ handleKey(key);
236
+ }
237
+ };
238
+ input.on('data', onData);
239
+ draw();
240
+ });
241
+ }