@yanzc/claude-pro 0.5.1 → 0.5.2
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 +1 -1
- package/package.json +1 -1
- package/src/supervisor.js +1 -1
- package/src/ui/repl.js +28 -7
- package/src/ui/screen.js +171 -10
- package/src/utils/clipboard.js +29 -0
package/README.md
CHANGED
|
@@ -234,7 +234,7 @@ claude-pro config set ANTHROPIC_MODEL deepseek-pro --project
|
|
|
234
234
|
- 流式 Markdown 渲染(代码高亮、表格、中文宽度对齐),思考过程折叠显示(`ctrl+o` 展开)
|
|
235
235
|
- `Esc` 中断;忙碌时输入的消息会排队并插入当前任务
|
|
236
236
|
- `!` Bash 模式、`#` 写入记忆(CLAUDE.md)、`\`+回车 或 `ctrl+j` 换行、粘贴大段文本折叠为 `[Pasted text #1 +42 lines]`
|
|
237
|
-
- **全屏模式(默认,和 Claude Code 的 `/tui fullscreen` 一样)**:程序画在终端的备用屏幕上(像 vim),输入框固定在窗口最底下,不随输出或终端滚动条移动;对话在程序里滚动:鼠标滚轮(每格 `CLAUDE_PRO_SCROLL_SPEED` 行,默认 3)、`PgUp`/`PgDn`(半屏)、`Ctrl+Home`(对话开头)、`Ctrl+End`(回到最新)。往上翻时新输出不会把画面拉回底部,底部浮着 `Jump to bottom (ctrl+End) ↓ · N new lines
|
|
237
|
+
- **全屏模式(默认,和 Claude Code 的 `/tui fullscreen` 一样)**:程序画在终端的备用屏幕上(像 vim),输入框固定在窗口最底下,不随输出或终端滚动条移动;对话在程序里滚动:鼠标滚轮(每格 `CLAUDE_PRO_SCROLL_SPEED` 行,默认 3)、`PgUp`/`PgDn`(半屏)、`Ctrl+Home`(对话开头)、`Ctrl+End`(回到最新)。往上翻时新输出不会把画面拉回底部,底部浮着 `Jump to bottom (ctrl+End) ↓ · N new lines`,**点它**或按 `Ctrl+End` 回到底部;发消息时自动回到底部。**选中文字直接用鼠标拖**(不用按 Shift),松开就复制到剪贴板;双击选一个词,三击选一整行;拖到对话区上下边缘会跟着滚动。选中的部分反色显示,点一下别处、按 `Esc` 或任意键就消失;有选中时 `Ctrl+C` 是复制(不会中断任务)。仍可按住 Shift 拖动用终端自己的选中。退出后终端回到启动前的样子(对话不留在终端滚动记录里)。`/tui default` 切回经典模式(对话写进终端自己的滚动记录,输入框在窗口底部、靠空行顶到底;`CLAUDE_PRO_PIN_INPUT=0` 让它紧跟在输出下面),`/tui fullscreen` 切回全屏,选择会记住(`CLAUDE_PRO_TUI`)
|
|
238
238
|
- `ctrl+c` 或连按两次 `Esc` 清掉的草稿不会丢:在空输入框按 `↑` 找回(连同粘贴的文本和图片),中间发过别的消息也可以
|
|
239
239
|
- `@文件` 补全并附加内容:走的就是 `Read` 工具,等于模型自己读了这个文件。最多带前 2000 行(被截断时会明确告诉模型,并把该文件记为"只读了一部分",之后 `Write` 不会去覆盖没给它看过的行);图片作为图片附加;二进制文件和超过 512KB 的文本会被跳过并提示原因;`@目录` 列出目录内容
|
|
240
240
|
- `alt+v` 粘贴剪贴板里的图片(截图用 `Win+Shift+S`),输入框里显示为 `[Image #1]`;超过 1568px 的图会自动缩小。把图片文件拖进终端(或粘贴图片路径)也会作为图片附加
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yanzc/claude-pro",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "claude-pro: a self-implemented coding agent for Anthropic-compatible APIs or local Claude Code subscription login. Full access with no permission prompts; file edits are checkpointed and can be rewound.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/supervisor.js
CHANGED
|
@@ -49,7 +49,7 @@ export function shouldSupervise(argv, env = process.env, isTty = (fd) => tty.isa
|
|
|
49
49
|
function putTerminalRight() {
|
|
50
50
|
// The child may have died with the terminal in the mode it had set for itself.
|
|
51
51
|
// (the fullscreen renderer's alternate screen and mouse reporting included)
|
|
52
|
-
try { process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?1049l\x1b[?2004l\x1b[?25h'); } catch { /* nothing to do */ }
|
|
52
|
+
try { process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1049l\x1b[?2004l\x1b[?25h'); } catch { /* nothing to do */ }
|
|
53
53
|
try { if (tty.isatty(0)) process.stdin.setRawMode(false); } catch { /* nothing to do */ }
|
|
54
54
|
}
|
|
55
55
|
|
package/src/ui/repl.js
CHANGED
|
@@ -27,7 +27,7 @@ import { ripgrepPath } from '../utils/shell.js';
|
|
|
27
27
|
import { describePause, formatClock, humanDuration, resumeText, shouldAnnounce } from '../ratelimit.js';
|
|
28
28
|
import { shutdownBackgroundShells, listBackgroundShells } from '../tools/bash.js';
|
|
29
29
|
import { estimateTokens } from '../agent.js';
|
|
30
|
-
import { readClipboardImage, imageFromFile, watchClipboard } from '../utils/clipboard.js';
|
|
30
|
+
import { readClipboardImage, imageFromFile, watchClipboard, writeClipboard } from '../utils/clipboard.js';
|
|
31
31
|
import { fitImagesIn } from '../utils/images.js';
|
|
32
32
|
import { downloadsDir, filesToSend, formatBytes } from '../utils/downloads.js';
|
|
33
33
|
import { copyUnique } from '../transfer.js';
|
|
@@ -1077,25 +1077,46 @@ export class App {
|
|
|
1077
1077
|
return mode === 'fullscreen' && this.stdout.isTTY !== false;
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
-
// Fullscreen: the wheel scrolls the conversation (CLAUDE_PRO_SCROLL_SPEED rows a notch, 3 by default)
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1080
|
+
// Fullscreen: the wheel scrolls the conversation (CLAUDE_PRO_SCROLL_SPEED rows a notch, 3 by default); a drag,
|
|
1081
|
+
// a double or a triple click selects, and what is selected goes to the clipboard (see Screen.mouse).
|
|
1082
|
+
onMouse(m) {
|
|
1083
|
+
if (!this.screen.full || this.exiting) return;
|
|
1084
|
+
if (m.button & 64) {
|
|
1085
|
+
if (m.button & 128) return;
|
|
1086
|
+
const speed = Math.max(1, Math.min(20, Number(this.config.env.CLAUDE_PRO_SCROLL_SPEED) || 3));
|
|
1087
|
+
this.screen.scrollBy(m.button & 1 ? -speed : speed);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const r = this.screen.mouse(m);
|
|
1091
|
+
if (r?.copy) this.copyText(r.copy);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
copyText(text) {
|
|
1095
|
+
writeClipboard(text).then((ok) => {
|
|
1096
|
+
if (ok) this.flash(`Copied ${text.length.toLocaleString()} characters to the clipboard`);
|
|
1097
|
+
else this.flash('Could not copy: no clipboard tool found (hold Shift and drag to select with the terminal)', 'warn');
|
|
1098
|
+
});
|
|
1085
1099
|
}
|
|
1086
1100
|
|
|
1087
1101
|
onKey(str, key) {
|
|
1088
1102
|
if (this.exiting) return;
|
|
1089
1103
|
if (key.sequence === '\x1b[I') { this.focused = true; return; }
|
|
1090
1104
|
if (key.sequence === '\x1b[O') { this.focused = false; return; }
|
|
1091
|
-
if (key.ctrl && key.name === 'c') { this.onCtrlC(); return; }
|
|
1092
1105
|
if (this.screen.full) {
|
|
1093
1106
|
const half = Math.max(1, Math.floor((this.screen.lastArea || this.screen.height) / 2));
|
|
1094
1107
|
if (key.name === 'pageup') { this.screen.scrollBy(half); return; }
|
|
1095
1108
|
if (key.name === 'pagedown') { this.screen.scrollBy(-half); return; }
|
|
1096
1109
|
if (key.ctrl && key.name === 'home') { this.screen.scrollBy('top'); return; }
|
|
1097
1110
|
if (key.ctrl && key.name === 'end') { this.screen.scrollBy('bottom'); return; }
|
|
1111
|
+
// A selection: ctrl+c copies it (and does not interrupt), esc only clears it, any other key clears it and
|
|
1112
|
+
// then does what it does.
|
|
1113
|
+
if (this.screen.selection) {
|
|
1114
|
+
if (key.ctrl && key.name === 'c') { this.copyText(this.screen.selectedText()); this.screen.clearSelection(); return; }
|
|
1115
|
+
this.screen.clearSelection();
|
|
1116
|
+
if (key.name === 'escape') return;
|
|
1117
|
+
}
|
|
1098
1118
|
}
|
|
1119
|
+
if (key.ctrl && key.name === 'c') { this.onCtrlC(); return; }
|
|
1099
1120
|
const dialog = this.dialogs[0];
|
|
1100
1121
|
if (dialog) {
|
|
1101
1122
|
dialog.handleKey(str, key);
|
package/src/ui/screen.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// of a restart) goes on right below the output.
|
|
30
30
|
|
|
31
31
|
import { PassThrough } from 'node:stream';
|
|
32
|
-
import { ESC, wrapLine, termWidth, truncate, stringWidth } from './ansi.js';
|
|
32
|
+
import { ESC, wrapLine, termWidth, truncate, stringWidth, stripAnsi, graphemes, graphemeWidth } from './ansi.js';
|
|
33
33
|
|
|
34
34
|
const SYNC_START = '\x1b[?2026h';
|
|
35
35
|
const SYNC_END = '\x1b[?2026l';
|
|
@@ -39,8 +39,24 @@ const RESIZE_MAX_WAIT_MS = 300; // ... or at the latest this long after it start
|
|
|
39
39
|
const HISTORY_KEEP = 600; // static lines remembered for repainting (a screen shows far fewer)
|
|
40
40
|
const HISTORY_LINE_CHARS = 20_000;
|
|
41
41
|
const FULL_KEEP = 20_000; // fullscreen: lines of the conversation kept to scroll back through
|
|
42
|
-
|
|
43
|
-
const
|
|
42
|
+
// The alternate screen, and the mouse reported (SGR): buttons, the wheel, and moves while a button is held (a drag).
|
|
43
|
+
const ALT_ON = '\x1b[?1049h\x1b[?1002h\x1b[?1006h';
|
|
44
|
+
const ALT_OFF = '\x1b[?1002l\x1b[?1006l\x1b[?1049l';
|
|
45
|
+
const MULTI_CLICK_MS = 450;
|
|
46
|
+
|
|
47
|
+
// The part of `plain` (no escape codes) between columns `from` (included) and `to` (not).
|
|
48
|
+
function sliceCols(plain, from, to) {
|
|
49
|
+
let out = '';
|
|
50
|
+
let col = 0;
|
|
51
|
+
for (const g of graphemes(plain)) {
|
|
52
|
+
if (col >= to) break;
|
|
53
|
+
if (col >= from) out += g;
|
|
54
|
+
col += graphemeWidth(g);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const before = (a, b) => a.line < b.line || (a.line === b.line && (a.sub < b.sub || (a.sub === b.sub && a.col <= b.col)));
|
|
44
60
|
const MOUSE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
45
61
|
|
|
46
62
|
// The keys of `stdin` without the mouse reports in it (SGR: ESC [ < b ; x ; y M/m), which go to
|
|
@@ -128,6 +144,11 @@ export class Screen {
|
|
|
128
144
|
this.wrapped = []; // fullscreen: history[i] wrapped to wrapCols - 1, made when first needed
|
|
129
145
|
this.wrapCols = 0;
|
|
130
146
|
this.lastArea = 0; // fullscreen: rows the conversation had on screen at the last draw
|
|
147
|
+
this.viewMap = []; // fullscreen: what each row of the conversation area shows, {line, sub} (null: blank)
|
|
148
|
+
this.barRow = null; // fullscreen: the row of the "Jump to bottom" bar, when it is shown
|
|
149
|
+
this.selection = null; // fullscreen: {anchor, focus}, points {line, sub, col} of the conversation (history line, wrapped row, column)
|
|
150
|
+
this.dragging = false;
|
|
151
|
+
this.lastClick = { at: 0, x: 0, y: 0, count: 0 };
|
|
131
152
|
this.cursorRow = 0; // row of the terminal cursor, relative to the top of the live region
|
|
132
153
|
this.staticQueue = [];
|
|
133
154
|
this.history = []; // the most recent static lines, for repainting after a resize
|
|
@@ -201,7 +222,7 @@ export class Screen {
|
|
|
201
222
|
}
|
|
202
223
|
|
|
203
224
|
wrappedAt(i, cols) {
|
|
204
|
-
if (this.wrapCols !== cols) { this.wrapped = []; this.wrapCols = cols; }
|
|
225
|
+
if (this.wrapCols !== cols) { this.wrapped = []; this.wrapCols = cols; this.selection = null; } // its rows are other rows now
|
|
205
226
|
return (this.wrapped[i] ??= wrapLine(this.history[i], Math.max(1, cols - 1)));
|
|
206
227
|
}
|
|
207
228
|
|
|
@@ -212,17 +233,149 @@ export class Screen {
|
|
|
212
233
|
return n;
|
|
213
234
|
}
|
|
214
235
|
|
|
215
|
-
// `rows` rows of the conversation, ending `this.scroll` rows above its end
|
|
236
|
+
// `rows` rows of the conversation, ending `this.scroll` rows above its end, as {text, line, sub}; padded below
|
|
237
|
+
// with null when it is shorter.
|
|
216
238
|
view(rows, cols) {
|
|
217
239
|
const need = rows + this.scroll;
|
|
218
|
-
const
|
|
219
|
-
|
|
240
|
+
const chunks = [];
|
|
241
|
+
let count = 0;
|
|
242
|
+
for (let i = this.history.length - 1; i >= 0 && count < need; i--) {
|
|
243
|
+
const w = this.wrappedAt(i, cols);
|
|
244
|
+
chunks.push(w.map((text, sub) => ({ text, line: i, sub })));
|
|
245
|
+
count += w.length;
|
|
246
|
+
}
|
|
247
|
+
const got = chunks.reverse().flat();
|
|
220
248
|
const end = Math.max(0, got.length - this.scroll);
|
|
221
249
|
const shown = got.slice(Math.max(0, end - rows), end);
|
|
222
|
-
while (shown.length < rows) shown.push(
|
|
250
|
+
while (shown.length < rows) shown.push(null);
|
|
223
251
|
return shown;
|
|
224
252
|
}
|
|
225
253
|
|
|
254
|
+
// ------------------------------------------------------------ selecting with the mouse (fullscreen)
|
|
255
|
+
|
|
256
|
+
// The selection in order: [start, end].
|
|
257
|
+
ordered() {
|
|
258
|
+
const { anchor, focus } = this.selection;
|
|
259
|
+
return before(anchor, focus) ? [anchor, focus] : [focus, anchor];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The columns of row {line, sub} that are selected: [from, to), or null.
|
|
263
|
+
selectedCols(e) {
|
|
264
|
+
if (!this.selection) return null;
|
|
265
|
+
const [a, b] = this.ordered();
|
|
266
|
+
const row = { line: e.line, sub: e.sub, col: 0 };
|
|
267
|
+
if (before(row, { ...a, col: -1 }) || !before({ ...row, col: 0 }, { ...b, col: Infinity })) return null;
|
|
268
|
+
const from = e.line === a.line && e.sub === a.sub ? a.col : 0;
|
|
269
|
+
const to = e.line === b.line && e.sub === b.sub ? b.col + 1 : Infinity;
|
|
270
|
+
return to > from ? [from, to] : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
paintRow(e) {
|
|
274
|
+
const cols = this.selectedCols(e);
|
|
275
|
+
if (!cols) return e.text;
|
|
276
|
+
const plain = stripAnsi(e.text);
|
|
277
|
+
return sliceCols(plain, 0, cols[0]) + '\x1b[7m' + sliceCols(plain, cols[0], cols[1]) + '\x1b[27m' + sliceCols(plain, cols[1], Infinity);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// The text selected, as it was printed: rows a long line was wrapped onto are joined again.
|
|
281
|
+
selectedText() {
|
|
282
|
+
if (!this.selection) return '';
|
|
283
|
+
const cols = this.width;
|
|
284
|
+
const [a, b] = this.ordered();
|
|
285
|
+
const lines = [];
|
|
286
|
+
for (let line = a.line; line <= b.line && line < this.history.length; line++) {
|
|
287
|
+
const rows = this.wrappedAt(line, cols);
|
|
288
|
+
let text = '';
|
|
289
|
+
rows.forEach((row, sub) => {
|
|
290
|
+
const range = this.selectedCols({ line, sub });
|
|
291
|
+
if (range) text += sliceCols(stripAnsi(row), range[0], range[1]);
|
|
292
|
+
});
|
|
293
|
+
lines.push(text.replace(/\s+$/, ''));
|
|
294
|
+
}
|
|
295
|
+
return lines.join('\n');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
clearSelection() {
|
|
299
|
+
if (!this.selection) return false;
|
|
300
|
+
this.selection = null;
|
|
301
|
+
this.dragging = false;
|
|
302
|
+
this.draw();
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// The point of the conversation at screen cell (x, y), 1-based: the nearest row that shows some of it.
|
|
307
|
+
pointAt(x, y) {
|
|
308
|
+
let i = Math.max(0, Math.min(this.lastArea - 1, y - 1));
|
|
309
|
+
while (i > 0 && !this.viewMap[i]) i--;
|
|
310
|
+
const e = this.viewMap[i];
|
|
311
|
+
return e ? { line: e.line, sub: e.sub, col: Math.max(0, x - 1) } : null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Word (`whole`: the whole line) around point `p`.
|
|
315
|
+
expand(p, whole) {
|
|
316
|
+
const rows = this.wrappedAt(p.line, this.width);
|
|
317
|
+
if (whole) return { anchor: { line: p.line, sub: 0, col: 0 }, focus: { line: p.line, sub: rows.length - 1, col: Infinity } };
|
|
318
|
+
const cells = [];
|
|
319
|
+
for (const g of graphemes(stripAnsi(rows[p.sub] || ''))) {
|
|
320
|
+
for (let k = 0; k < Math.max(1, graphemeWidth(g)); k++) cells.push(g);
|
|
321
|
+
}
|
|
322
|
+
const word = (g) => g !== undefined && !/\s/.test(g);
|
|
323
|
+
if (!word(cells[p.col])) return null;
|
|
324
|
+
let from = p.col;
|
|
325
|
+
let to = p.col;
|
|
326
|
+
while (word(cells[from - 1])) from--;
|
|
327
|
+
while (word(cells[to + 1])) to++;
|
|
328
|
+
return { anchor: { ...p, col: from }, focus: { ...p, col: to } };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// A mouse report (fullscreen). Returns {copy: text} when a selection was made, for the caller to put on the
|
|
332
|
+
// clipboard. Drag to select (the view scrolls at its edges), double-click a word, triple-click a line; a click
|
|
333
|
+
// clears the selection; a click on the "Jump to bottom" bar goes there.
|
|
334
|
+
mouse({ button, x, y, release }) {
|
|
335
|
+
if (!this.full || this.closed) return null;
|
|
336
|
+
if (button & 64) return null; // the wheel: the caller scrolls
|
|
337
|
+
const left = (button & 3) === 0;
|
|
338
|
+
if (!left) return null;
|
|
339
|
+
const motion = Boolean(button & 32);
|
|
340
|
+
if (!motion && !release) { // a press
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const c = this.lastClick;
|
|
343
|
+
const count = now - c.at < MULTI_CLICK_MS && Math.abs(c.x - x) <= 1 && c.y === y ? c.count + 1 : 1;
|
|
344
|
+
this.lastClick = { at: now, x, y, count };
|
|
345
|
+
if (this.barRow !== null && y - 1 === this.barRow) { this.selection = null; this.scrollBy('bottom'); return null; }
|
|
346
|
+
if (y - 1 >= this.lastArea) { this.clearSelection(); return null; } // the input box, the footer
|
|
347
|
+
const p = this.pointAt(x, y);
|
|
348
|
+
if (!p) { this.clearSelection(); return null; }
|
|
349
|
+
if (count >= 2) {
|
|
350
|
+
const sel = this.expand(p, count >= 3);
|
|
351
|
+
this.selection = sel;
|
|
352
|
+
this.dragging = false;
|
|
353
|
+
this.draw();
|
|
354
|
+
const text = sel ? this.selectedText() : '';
|
|
355
|
+
return text ? { copy: text } : null;
|
|
356
|
+
}
|
|
357
|
+
this.selection = { anchor: p, focus: { ...p }, moved: false };
|
|
358
|
+
this.dragging = true;
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
if (!this.dragging || !this.selection) return null;
|
|
362
|
+
if (motion) {
|
|
363
|
+
// past the top or the bottom of the conversation: it scrolls, and the selection goes on with it
|
|
364
|
+
if (y <= 1) this.scrollBy(1);
|
|
365
|
+
else if (y - 1 >= this.lastArea - 1 && this.scroll > 0) this.scrollBy(-1);
|
|
366
|
+
const p = this.pointAt(x, y);
|
|
367
|
+
if (p) { this.selection.focus = p; this.selection.moved = true; }
|
|
368
|
+
this.draw();
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
// released
|
|
372
|
+
this.dragging = false;
|
|
373
|
+
if (!this.selection.moved) { this.clearSelection(); return null; } // a plain click
|
|
374
|
+
this.lastClick.count = 0; // a drag: the next click starts a count of its own
|
|
375
|
+
const text = this.selectedText();
|
|
376
|
+
return text ? { copy: text } : null;
|
|
377
|
+
}
|
|
378
|
+
|
|
226
379
|
// Scroll the conversation up (`rows` > 0) or down. 'top' and 'bottom' go to its ends.
|
|
227
380
|
scrollBy(rows) {
|
|
228
381
|
if (!this.full) return;
|
|
@@ -254,8 +407,11 @@ export class Screen {
|
|
|
254
407
|
const layout = this.layoutLive(cols);
|
|
255
408
|
const area = Math.max(0, screenRows - layout.rows.length);
|
|
256
409
|
this.lastArea = area;
|
|
257
|
-
const
|
|
258
|
-
|
|
410
|
+
const shown = this.view(area, cols);
|
|
411
|
+
this.viewMap = shown;
|
|
412
|
+
const top = shown.map((e) => (e ? this.paintRow(e) : ''));
|
|
413
|
+
this.barRow = this.scroll > 0 && area > 0 ? area - 1 : null;
|
|
414
|
+
if (this.barRow !== null) top[this.barRow] = this.jumpBar(cols);
|
|
259
415
|
const frame = [...top, ...layout.rows];
|
|
260
416
|
let buf = SYNC_START + ESC.hideCursor + this.titleUpdate();
|
|
261
417
|
if (full) { buf += '\x1b[2J'; this.frame = []; }
|
|
@@ -318,6 +474,10 @@ export class Screen {
|
|
|
318
474
|
const drop = this.history.length - keep;
|
|
319
475
|
this.history.splice(0, drop);
|
|
320
476
|
this.wrapped.splice(0, drop);
|
|
477
|
+
if (this.selection) {
|
|
478
|
+
for (const p of [this.selection.anchor, this.selection.focus]) p.line -= drop;
|
|
479
|
+
if (this.selection.anchor.line < 0 || this.selection.focus.line < 0) this.selection = null;
|
|
480
|
+
}
|
|
321
481
|
}
|
|
322
482
|
return lines;
|
|
323
483
|
}
|
|
@@ -472,6 +632,7 @@ export class Screen {
|
|
|
472
632
|
if (this.full) {
|
|
473
633
|
this.history = [];
|
|
474
634
|
this.wrapped = [];
|
|
635
|
+
this.selection = null;
|
|
475
636
|
this.scroll = 0;
|
|
476
637
|
this.unseen = 0;
|
|
477
638
|
this.staticQueue = [];
|
package/src/utils/clipboard.js
CHANGED
|
@@ -135,6 +135,35 @@ async function readLinux() {
|
|
|
135
135
|
return { error: null };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// Put `text` on the system clipboard (the fullscreen renderer's selection): Windows PowerShell (read as UTF-8 from
|
|
139
|
+
// stdin, so any text and any length), pbcopy on macOS, wl-copy / xclip / xsel elsewhere. Resolves with whether
|
|
140
|
+
// one of them took it.
|
|
141
|
+
export async function writeClipboard(text) {
|
|
142
|
+
const tries = process.platform === 'win32'
|
|
143
|
+
? [['powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '$s=[IO.StreamReader]::new([Console]::OpenStandardInput(),[Text.Encoding]::UTF8).ReadToEnd(); Set-Clipboard -Value $s']]]
|
|
144
|
+
: process.platform === 'darwin'
|
|
145
|
+
? [['pbcopy', []]]
|
|
146
|
+
: [['wl-copy', []], ['xclip', ['-selection', 'clipboard']], ['xsel', ['--clipboard', '--input']]];
|
|
147
|
+
for (const [cmd, args] of tries) {
|
|
148
|
+
const ok = await new Promise((resolve) => {
|
|
149
|
+
let child;
|
|
150
|
+
try {
|
|
151
|
+
child = spawn(cmd, args, { windowsHide: true, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
152
|
+
} catch {
|
|
153
|
+
resolve(false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const timer = setTimeout(() => child.kill(), 10_000);
|
|
157
|
+
child.on('error', () => { clearTimeout(timer); resolve(false); });
|
|
158
|
+
child.on('close', (code) => { clearTimeout(timer); resolve(code === 0); });
|
|
159
|
+
child.stdin.on('error', () => {});
|
|
160
|
+
child.stdin.end(Buffer.from(process.platform === 'win32' ? text.replace(/\r?\n/g, '\r\n') : text, 'utf8'));
|
|
161
|
+
});
|
|
162
|
+
if (ok) return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
138
167
|
// Returns {image} on success, {error: string} on failure, or {error: null} when the clipboard has no image.
|
|
139
168
|
export async function readClipboardImage({ source } = {}) {
|
|
140
169
|
if (process.platform === 'win32') return readWindows(source);
|