bunmicro 0.9.10 → 0.9.20
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/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/runtime/help/options.md +8 -0
- package/runtime/jsplugins/example/example.js +3 -1
- package/src/config/clean.js +172 -0
- package/src/config/config.js +3 -1
- package/src/config/defaults.js +8 -1
- package/src/index.js +530 -152
- package/src/platform/clipboard.js +125 -7
- package/src/plugins/js-bridge.js +10 -8
- package/src/screen/screen.js +16 -4
- package/todo.txt +8 -3
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { Config } from "./config/config.js";
|
|
10
10
|
import { defaultAllSettings, OPTION_CHOICES, LOCAL_SETTINGS } from "./config/defaults.js";
|
|
11
|
+
import { cleanConfig } from "./config/clean.js";
|
|
11
12
|
import { RuntimeRegistry, RTColorscheme, RTHelp } from "./runtime/registry.js";
|
|
12
13
|
import { PluginManager } from "./plugins/manager.js";
|
|
13
14
|
import { JsPluginManager, buildMicroGlobal, runAction, listActions } from "./plugins/js-bridge.js";
|
|
@@ -17,8 +18,8 @@ import { Highlighter } from "./highlight/highlighter.js";
|
|
|
17
18
|
import { DISABLE_MOUSE, parseInputEvents, parseKey } from "./screen/events.js";
|
|
18
19
|
import { Screen } from "./screen/screen.js";
|
|
19
20
|
import { VT100 } from "./screen/vt100.js";
|
|
20
|
-
import { ClipboardManager } from "./platform/clipboard.js";
|
|
21
|
-
import { platformId, run as runCommand, runSync,
|
|
21
|
+
import { ClipboardManager, probeOSC52, osc52Clipboard } from "./platform/clipboard.js";
|
|
22
|
+
import { platformId, run as runCommand, runSync, fetchHttpBytes, detectHttpBackend } from "./platform/commands.js";
|
|
22
23
|
import { shellSplit } from "./shell/shell.js";
|
|
23
24
|
import { styleToAnsi } from "./display/ansi-style.js";
|
|
24
25
|
import { encodeBinaryToBuffer, decodeBinaryBytes } from "./buffer/fixed3-codec.js";
|
|
@@ -80,6 +81,7 @@ const DEFAULT_SETTINGS = {
|
|
|
80
81
|
tabsize: 4,
|
|
81
82
|
tabstospaces: false,
|
|
82
83
|
autosave: 0,
|
|
84
|
+
cursorshape: "block",
|
|
83
85
|
cursorline: true,
|
|
84
86
|
diffgutter: false,
|
|
85
87
|
eofnewline: true,
|
|
@@ -92,9 +94,11 @@ const DEFAULT_SETTINGS = {
|
|
|
92
94
|
savecursor: false,
|
|
93
95
|
softwrap: false,
|
|
94
96
|
wordwrap: false,
|
|
97
|
+
pageoverlap: 2,
|
|
95
98
|
scrollmargin: 3,
|
|
96
99
|
reload: "prompt",
|
|
97
100
|
encoding: "utf-8",
|
|
101
|
+
fileformat: process.platform === "win32" ? "dos" : "unix",
|
|
98
102
|
"comment.type": "",
|
|
99
103
|
commenttype: "",
|
|
100
104
|
trailingws: false,
|
|
@@ -136,8 +140,7 @@ function isHttpUrl(value) {
|
|
|
136
140
|
return String(value ?? "").startsWith("http://") || String(value ?? "").startsWith("https://");
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
|
|
140
|
-
const bytes = new Uint8Array(await Bun.file(path).arrayBuffer());
|
|
143
|
+
function decodeTextBytesWithEncoding(bytes, encoding = "utf-8") {
|
|
141
144
|
if (normalizeEncodingLabel(encoding) === "hex3") {
|
|
142
145
|
return { text: encodeBinaryToBuffer(bytes).toString("latin1"), encoding: "hex3" };
|
|
143
146
|
}
|
|
@@ -145,13 +148,14 @@ async function readTextFileWithEncoding(path, encoding = "utf-8") {
|
|
|
145
148
|
return { text: decoder.decode(bytes), encoding: decoder.encoding };
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
async function readTextFileWithEncoding(path, encoding = "utf-8") {
|
|
152
|
+
const bytes = new Uint8Array(await Bun.file(path).arrayBuffer());
|
|
153
|
+
return decodeTextBytesWithEncoding(bytes, encoding);
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
async function fetchTextWithEncoding(url, encoding = "utf-8") {
|
|
149
157
|
const bytes = await fetchHttpBytes(url);
|
|
150
|
-
|
|
151
|
-
return { text: encodeBinaryToBuffer(new Uint8Array(bytes)).toString("latin1"), encoding: "hex3" };
|
|
152
|
-
}
|
|
153
|
-
const decoder = new TextDecoder(normalizeEncodingLabel(encoding));
|
|
154
|
-
return { text: decoder.decode(bytes), encoding: decoder.encoding };
|
|
158
|
+
return decodeTextBytesWithEncoding(new Uint8Array(bytes), encoding);
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
function normalizeEncodingLabel(encoding = "utf-8") {
|
|
@@ -160,6 +164,21 @@ function normalizeEncodingLabel(encoding = "utf-8") {
|
|
|
160
164
|
return new TextDecoder(s).encoding;
|
|
161
165
|
}
|
|
162
166
|
|
|
167
|
+
function detectFileFormat(text, fallback = DEFAULT_SETTINGS.fileformat) {
|
|
168
|
+
if (text.length === 0) return fallback === "dos" ? "dos" : "unix";
|
|
169
|
+
const newlineIdx = text.indexOf("\n");
|
|
170
|
+
if (newlineIdx < 0) return "unix";
|
|
171
|
+
return newlineIdx > 0 && text.charCodeAt(newlineIdx - 1) === 13 ? "dos" : "unix";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeBufferText(text) {
|
|
175
|
+
return String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function encodeBufferTextForFile(text, fileformat) {
|
|
179
|
+
return fileformat === "dos" ? text.replace(/\n/g, "\r\n") : text;
|
|
180
|
+
}
|
|
181
|
+
|
|
163
182
|
function isReadonlyBuffer(buf) {
|
|
164
183
|
return Boolean(buf?.readonly || buf?.Settings?.readonly || buf?.Type?.Readonly);
|
|
165
184
|
}
|
|
@@ -469,6 +488,7 @@ function parseArgs(argv) {
|
|
|
469
488
|
help: false,
|
|
470
489
|
clean: false,
|
|
471
490
|
cat: false,
|
|
491
|
+
docs: false,
|
|
472
492
|
configDir: "",
|
|
473
493
|
debug: false,
|
|
474
494
|
profile: false,
|
|
@@ -484,6 +504,7 @@ function parseArgs(argv) {
|
|
|
484
504
|
else if (arg === "-help" || arg === "--help" || arg === "-h") flags.help = true;
|
|
485
505
|
else if (arg === "-clean") flags.clean = true;
|
|
486
506
|
else if (arg === "--cat" || arg === "-cat" || arg === "--ccat" || arg === "-ccat" || arg === "--bat" || arg === "-bat" || arg === "--glow" || arg === "-glow") flags.cat = true;
|
|
507
|
+
else if (arg === "--docs" || arg === "--readme") flags.docs = true;
|
|
487
508
|
else if (arg === "-debug") flags.debug = true;
|
|
488
509
|
else if (arg === "-profile") flags.profile = true;
|
|
489
510
|
else if (arg === "-config-dir") flags.configDir = argv[++i] ?? "";
|
|
@@ -500,17 +521,13 @@ function parseArgs(argv) {
|
|
|
500
521
|
|
|
501
522
|
function usage() {
|
|
502
523
|
return [
|
|
503
|
-
`Usage:
|
|
524
|
+
`Usage:
|
|
525
|
+
${pkg.name} [OPTIONs] [FILEs] [+line[.subrow][:col]]\n`,
|
|
504
526
|
"-clean",
|
|
505
|
-
" Clean configuration directory and exit
|
|
527
|
+
" Clean configuration directory and exit",
|
|
506
528
|
"-config-dir dir",
|
|
507
529
|
" Specify a custom location for configuration directory",
|
|
508
|
-
"
|
|
509
|
-
" Enable debug logging",
|
|
510
|
-
"-help, --help, -h",
|
|
511
|
-
" Show this help and exit",
|
|
512
|
-
"-options",
|
|
513
|
-
" Show option help and exit",
|
|
530
|
+
"",
|
|
514
531
|
"-plugin list",
|
|
515
532
|
" List installed plugins",
|
|
516
533
|
"-plugin available|avail",
|
|
@@ -523,20 +540,28 @@ function usage() {
|
|
|
523
540
|
" Remove installed plugin(s)",
|
|
524
541
|
"-plugin update [name]...",
|
|
525
542
|
" Update installed plugin(s) (all if no name given)",
|
|
526
|
-
|
|
527
|
-
"-version, -V",
|
|
528
|
-
" Show version number and information and exit",
|
|
529
|
-
"--cat, --ccat, --bat, --glow",
|
|
530
|
-
" Syntax-highlight file(s) and write to stdout, then exit (.md uses Bun.markdown.ansi)",
|
|
543
|
+
"",
|
|
531
544
|
"-<option> value",
|
|
532
545
|
" Set an option for this session",
|
|
546
|
+
"-options",
|
|
547
|
+
" Show option help and exit\n",
|
|
548
|
+
"--cat, --ccat, --bat, --glow",
|
|
549
|
+
" Syntax-highlight file(s) and write to stdout, then exit (.md uses Bun.markdown.ansi)\n",
|
|
550
|
+
"-help, -h, --help",
|
|
551
|
+
" Show this help & exit",
|
|
552
|
+
"-version, -V, --version",
|
|
553
|
+
" Show version+backend info & exit",
|
|
554
|
+
"--docs, --readme",
|
|
555
|
+
` Show ${pkg.name}'s README.md & exit`,
|
|
556
|
+
|
|
557
|
+
|
|
533
558
|
].join("\n");
|
|
534
559
|
}
|
|
535
560
|
|
|
536
561
|
function parseInput(args) {
|
|
537
562
|
const files = [];
|
|
538
563
|
const command = {
|
|
539
|
-
startCursor: {
|
|
564
|
+
startCursor: { line: -1, subRow: 0, col: 1 },
|
|
540
565
|
searchRegex: "",
|
|
541
566
|
searchAfterStart: false,
|
|
542
567
|
};
|
|
@@ -545,25 +570,19 @@ function parseInput(args) {
|
|
|
545
570
|
|
|
546
571
|
for (let i = 0; i < args.length; i++) {
|
|
547
572
|
const arg = args[i];
|
|
548
|
-
const pos = arg.match(/^\+(
|
|
573
|
+
const pos = arg.match(/^\+(-?\d+(?:\.\d+)?)(?::(-?\d+))?$/);
|
|
549
574
|
const search = arg.match(/^\+\/(.+)$/);
|
|
550
|
-
const cursorFile = arg.match(/^(.+):(
|
|
575
|
+
const cursorFile = arg.match(/^(.+):(-?\d+(?:\.\d+)?)(?::(-?\d+))?$/);
|
|
551
576
|
|
|
552
577
|
if (pos) {
|
|
553
|
-
command.startCursor = {
|
|
554
|
-
x: pos[2] ? Number(pos[2]) - 1 : 0,
|
|
555
|
-
y: Number(pos[1]) - 1,
|
|
556
|
-
};
|
|
578
|
+
command.startCursor = parseLineCol(`${pos[1]}${pos[2] ? `:${pos[2]}` : ""}`);
|
|
557
579
|
posIndex = i;
|
|
558
580
|
} else if (search) {
|
|
559
581
|
command.searchRegex = search[1];
|
|
560
582
|
searchIndex = i;
|
|
561
583
|
} else if (DEFAULT_SETTINGS.parsecursor && cursorFile && existsSync(cursorFile[1])) {
|
|
562
584
|
files.push(cursorFile[1]);
|
|
563
|
-
command.startCursor = {
|
|
564
|
-
x: cursorFile[3] ? Number(cursorFile[3]) - 1 : 0,
|
|
565
|
-
y: Number(cursorFile[2]) - 1,
|
|
566
|
-
};
|
|
585
|
+
command.startCursor = parseLineCol(`${cursorFile[2]}${cursorFile[3] ? `:${cursorFile[3]}` : ""}`);
|
|
567
586
|
posIndex = i;
|
|
568
587
|
} else {
|
|
569
588
|
files.push(arg);
|
|
@@ -582,9 +601,9 @@ class BufferModel {
|
|
|
582
601
|
this.path = path;
|
|
583
602
|
this.type = type;
|
|
584
603
|
this.name = path ? basename(path) : "No name";
|
|
585
|
-
this.fileformat = text.
|
|
604
|
+
this.fileformat = detectFileFormat(text, DEFAULT_SETTINGS.fileformat);
|
|
586
605
|
this.encoding = normalizeEncodingLabel(encoding);
|
|
587
|
-
this.lines = text
|
|
606
|
+
this.lines = normalizeBufferText(text).split("\n");
|
|
588
607
|
if (this.lines.length === 0) this.lines = [""];
|
|
589
608
|
this.cursor = { x: 0, y: 0 };
|
|
590
609
|
this.scroll = { x: 0, y: 0, row: 0 };
|
|
@@ -623,8 +642,11 @@ class BufferModel {
|
|
|
623
642
|
savecursor: DEFAULT_SETTINGS.savecursor,
|
|
624
643
|
softwrap: DEFAULT_SETTINGS.softwrap,
|
|
625
644
|
wordwrap: DEFAULT_SETTINGS.wordwrap,
|
|
645
|
+
pageoverlap: DEFAULT_SETTINGS.pageoverlap,
|
|
626
646
|
scrollmargin: DEFAULT_SETTINGS.scrollmargin,
|
|
627
647
|
reload: DEFAULT_SETTINGS.reload,
|
|
648
|
+
eofnewline: DEFAULT_SETTINGS.eofnewline,
|
|
649
|
+
fileformat: this.fileformat,
|
|
628
650
|
trailingws: DEFAULT_SETTINGS.trailingws,
|
|
629
651
|
encoding: this.encoding,
|
|
630
652
|
readonly,
|
|
@@ -633,9 +655,13 @@ class BufferModel {
|
|
|
633
655
|
this.AbsPath = path;
|
|
634
656
|
this.Type = { Scratch: type !== "default", Kind: 0, Readonly: readonly };
|
|
635
657
|
|
|
636
|
-
if (command
|
|
637
|
-
|
|
638
|
-
|
|
658
|
+
if (commandHasStartCursor(command)) {
|
|
659
|
+
if (command.startCursor.subRow > 0) {
|
|
660
|
+
this.gotoLoc(command.startCursor.line, 1);
|
|
661
|
+
this._pendingVisualGoto = { subRow: command.startCursor.subRow, col: command.startCursor.col };
|
|
662
|
+
} else {
|
|
663
|
+
this.gotoLoc(command.startCursor.line, command.startCursor.col);
|
|
664
|
+
}
|
|
639
665
|
}
|
|
640
666
|
if (command.searchRegex) this.search(command.searchRegex, command.searchAfterStart);
|
|
641
667
|
}
|
|
@@ -897,8 +923,8 @@ class BufferModel {
|
|
|
897
923
|
return true;
|
|
898
924
|
}
|
|
899
925
|
|
|
900
|
-
page(delta,
|
|
901
|
-
this.cursor.y += delta * Math.max(1,
|
|
926
|
+
page(delta, amount) {
|
|
927
|
+
this.cursor.y += delta * Math.max(1, amount);
|
|
902
928
|
this.ensureCursor();
|
|
903
929
|
}
|
|
904
930
|
|
|
@@ -1019,8 +1045,9 @@ class BufferModel {
|
|
|
1019
1045
|
const text = decoded.text;
|
|
1020
1046
|
this.encoding = decoded.encoding;
|
|
1021
1047
|
this.Settings.encoding = decoded.encoding;
|
|
1022
|
-
this.fileformat = text.
|
|
1023
|
-
this.
|
|
1048
|
+
this.fileformat = detectFileFormat(text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
1049
|
+
this.Settings.fileformat = this.fileformat;
|
|
1050
|
+
this.lines = normalizeBufferText(text).split("\n");
|
|
1024
1051
|
if (this.lines.length === 0) this.lines = [""];
|
|
1025
1052
|
this.modTimeMs = null;
|
|
1026
1053
|
this.readonly = false;
|
|
@@ -1044,8 +1071,9 @@ class BufferModel {
|
|
|
1044
1071
|
const text = decoded.text;
|
|
1045
1072
|
this.encoding = decoded.encoding;
|
|
1046
1073
|
this.Settings.encoding = decoded.encoding;
|
|
1047
|
-
this.fileformat = text.
|
|
1048
|
-
this.
|
|
1074
|
+
this.fileformat = detectFileFormat(text, this.Settings.fileformat ?? DEFAULT_SETTINGS.fileformat);
|
|
1075
|
+
this.Settings.fileformat = this.fileformat;
|
|
1076
|
+
this.lines = normalizeBufferText(text).split("\n");
|
|
1049
1077
|
if (this.lines.length === 0) this.lines = [""];
|
|
1050
1078
|
this.modTimeMs = info.mtimeMs;
|
|
1051
1079
|
this.readonly = !canWritePath(this.path);
|
|
@@ -1080,8 +1108,8 @@ class BufferModel {
|
|
|
1080
1108
|
this.message = `Saved ${path}`;
|
|
1081
1109
|
return;
|
|
1082
1110
|
}
|
|
1083
|
-
if (DEFAULT_SETTINGS.eofnewline && !text.endsWith("\n")) text += "\n";
|
|
1084
|
-
await Bun.write(path, text);
|
|
1111
|
+
if ((this.Settings.eofnewline ?? DEFAULT_SETTINGS.eofnewline) && !text.endsWith("\n")) text += "\n";
|
|
1112
|
+
await Bun.write(path, encodeBufferTextForFile(text, this.Settings.fileformat ?? this.fileformat));
|
|
1085
1113
|
this.encoding = "utf-8";
|
|
1086
1114
|
this.Settings.encoding = "utf-8";
|
|
1087
1115
|
this.path = path;
|
|
@@ -1222,19 +1250,33 @@ class BufferModel {
|
|
|
1222
1250
|
SetOption(option, value) {
|
|
1223
1251
|
const oldValue = this.Settings[option];
|
|
1224
1252
|
const parsed = parseOptionValue(value);
|
|
1225
|
-
|
|
1253
|
+
if (option === "cursorshape" && !OPTION_CHOICES.cursorshape.includes(String(parsed))) {
|
|
1254
|
+
throw new Error(`Invalid value for cursorshape: ${parsed}`);
|
|
1255
|
+
}
|
|
1256
|
+
if (option === "fileformat" && !OPTION_CHOICES.fileformat.includes(String(parsed))) {
|
|
1257
|
+
throw new Error(`Invalid value for fileformat: ${parsed}`);
|
|
1258
|
+
}
|
|
1259
|
+
this.Settings[option] = option === "encoding" ? normalizeEncodingLabel(parsed) : option === "fileformat" ? String(parsed) : parsed;
|
|
1226
1260
|
if (option === "filetype") this.filetype = String(parsed);
|
|
1227
1261
|
if (option === "encoding") this.encoding = this.Settings.encoding;
|
|
1262
|
+
if (option === "fileformat") this.fileformat = this.Settings.fileformat === "dos" ? "dos" : "unix";
|
|
1228
1263
|
if (option === "readonly") { this.readonly = Boolean(parsed); this.Type.Readonly = this.readonly; }
|
|
1229
|
-
if (option in DEFAULT_SETTINGS) DEFAULT_SETTINGS[option] = this.Settings[option];
|
|
1264
|
+
if (option in DEFAULT_SETTINGS && option !== "fileformat") DEFAULT_SETTINGS[option] = this.Settings[option];
|
|
1230
1265
|
this._onOptionChange?.(option, oldValue, this.Settings[option]);
|
|
1231
1266
|
}
|
|
1232
1267
|
|
|
1233
1268
|
DoSetOptionNative(option, value) {
|
|
1234
1269
|
const oldValue = this.Settings[option];
|
|
1235
|
-
|
|
1270
|
+
if (option === "cursorshape" && !OPTION_CHOICES.cursorshape.includes(String(value))) {
|
|
1271
|
+
throw new Error(`Invalid value for cursorshape: ${value}`);
|
|
1272
|
+
}
|
|
1273
|
+
if (option === "fileformat" && !OPTION_CHOICES.fileformat.includes(String(value))) {
|
|
1274
|
+
throw new Error(`Invalid value for fileformat: ${value}`);
|
|
1275
|
+
}
|
|
1276
|
+
this.Settings[option] = option === "encoding" ? normalizeEncodingLabel(value) : option === "fileformat" ? String(value) : value;
|
|
1236
1277
|
if (option === "filetype") this.filetype = String(value);
|
|
1237
1278
|
if (option === "encoding") this.encoding = this.Settings.encoding;
|
|
1279
|
+
if (option === "fileformat") this.fileformat = this.Settings.fileformat === "dos" ? "dos" : "unix";
|
|
1238
1280
|
if (option === "readonly") { this.readonly = Boolean(value); this.Type.Readonly = this.readonly; }
|
|
1239
1281
|
this._onOptionChange?.(option, oldValue, this.Settings[option]);
|
|
1240
1282
|
}
|
|
@@ -1263,7 +1305,7 @@ class BufferModel {
|
|
|
1263
1305
|
}
|
|
1264
1306
|
|
|
1265
1307
|
Bytes() {
|
|
1266
|
-
return this.lines.join("\n");
|
|
1308
|
+
return encodeBufferTextForFile(this.lines.join("\n"), this.Settings.fileformat ?? this.fileformat);
|
|
1267
1309
|
}
|
|
1268
1310
|
|
|
1269
1311
|
Size() {
|
|
@@ -1636,6 +1678,10 @@ class App {
|
|
|
1636
1678
|
this._acHScroll = 0;
|
|
1637
1679
|
this._suppressMouseUntilUp = false;
|
|
1638
1680
|
this._undoInsertChain = false;
|
|
1681
|
+
this._freshClip = false;
|
|
1682
|
+
this._messageClickAction = null;
|
|
1683
|
+
this._messageRowY = null;
|
|
1684
|
+
this._messageRowClickZone = null;
|
|
1639
1685
|
}
|
|
1640
1686
|
|
|
1641
1687
|
get tab() { return this.tabs[this.activeTabIdx]; }
|
|
@@ -1645,6 +1691,22 @@ class App {
|
|
|
1645
1691
|
get active() { return this.activeTabIdx; }
|
|
1646
1692
|
get buffers() { return this.tabs.map(t => t.buffer).filter(Boolean); }
|
|
1647
1693
|
|
|
1694
|
+
paneForBuffer(buffer) {
|
|
1695
|
+
for (const tab of this.tabs) {
|
|
1696
|
+
const pane = tab.panes().find((p) => p.buffer === buffer);
|
|
1697
|
+
if (pane) return pane;
|
|
1698
|
+
}
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
formatCursorLocation(buffer = this.buffer, pane = null) {
|
|
1703
|
+
return formatCursorLocation(buffer, pane ?? this.paneForBuffer(buffer) ?? this.pane);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
formatAbsoluteCursorLocation(buffer = this.buffer) {
|
|
1707
|
+
return formatAbsoluteCursorLocation(buffer);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1648
1710
|
async start() {
|
|
1649
1711
|
this._started = true;
|
|
1650
1712
|
// When stdin was a pipe (content already consumed in loadBuffers), open the
|
|
@@ -1666,7 +1728,10 @@ class App {
|
|
|
1666
1728
|
_activeTtyStream = this._ttyStream;
|
|
1667
1729
|
this._ttyStream.setRawMode?.(true);
|
|
1668
1730
|
this._ttyStream.resume();
|
|
1669
|
-
this.
|
|
1731
|
+
const clipSetting = this.context?.config?.getGlobalOption("clipboard") ?? "external";
|
|
1732
|
+
await this.reinitializeClipboard(clipSetting);
|
|
1733
|
+
this._inputHandler = (data) => this.handleInput(data);
|
|
1734
|
+
this._ttyStream.on("data", this._inputHandler);
|
|
1670
1735
|
process.stdout.on("resize", () => {
|
|
1671
1736
|
const resize = this.screen.updateSize();
|
|
1672
1737
|
this.rows = resize.rows;
|
|
@@ -1687,6 +1752,15 @@ class App {
|
|
|
1687
1752
|
}
|
|
1688
1753
|
}
|
|
1689
1754
|
|
|
1755
|
+
async reinitializeClipboard(setting) {
|
|
1756
|
+
if (this._inputHandler) this._ttyStream?.removeListener("data", this._inputHandler);
|
|
1757
|
+
try {
|
|
1758
|
+
await this.clipboard.initFromSetting(setting, this._ttyStream, process.stdout, 150);
|
|
1759
|
+
} finally {
|
|
1760
|
+
if (this._inputHandler) this._ttyStream?.on("data", this._inputHandler);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1690
1764
|
async stop(code = 0) {
|
|
1691
1765
|
this.running = false;
|
|
1692
1766
|
for (const tab of this.tabs)
|
|
@@ -1899,8 +1973,19 @@ class App {
|
|
|
1899
1973
|
cursorCol = p.x + gutterW + displayWidth(line.slice(buf.scroll.x, cursorX));
|
|
1900
1974
|
}
|
|
1901
1975
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1976
|
+
// Go micro hides the terminal cursor while a non-empty selection is
|
|
1977
|
+
// active. Otherwise its block cursor makes the exclusive selection end
|
|
1978
|
+
// look selected even though copy/cut correctly omit that character.
|
|
1979
|
+
const hasSelection = p.selection && !sameLoc(p.selection.start, p.selection.end);
|
|
1980
|
+
const cursorVisible = !hasSelection &&
|
|
1981
|
+
cursorRow >= p.y && cursorRow < p.y + p.h &&
|
|
1982
|
+
cursorCol >= p.x && cursorCol < p.x + p.w;
|
|
1983
|
+
this.screen.setCursor(
|
|
1984
|
+
clamp(cursorCol, 0, this.cols - 1),
|
|
1985
|
+
clamp(cursorRow, 0, this.rows - 1),
|
|
1986
|
+
cursorVisible,
|
|
1987
|
+
DEFAULT_SETTINGS.cursorshape,
|
|
1988
|
+
);
|
|
1904
1989
|
} else if (!this.prompt && activePaneObj?.type !== "term") {
|
|
1905
1990
|
this.screen.setCursor(0, 0, false);
|
|
1906
1991
|
}
|
|
@@ -1912,7 +1997,23 @@ class App {
|
|
|
1912
1997
|
const statusStyle = this.context.colorscheme?.get("statusline") ?? { ...defaultStyle, reverse: true };
|
|
1913
1998
|
const style = { ...statusStyle, reverse: false };
|
|
1914
1999
|
putText(this.screen, 0, row, " ".repeat(this.cols), style, this.cols);
|
|
1915
|
-
|
|
2000
|
+
this._messageRowY = row;
|
|
2001
|
+
this._messageRowClickZone = null;
|
|
2002
|
+
const msg = String(message);
|
|
2003
|
+
// detect [AltMethod] prefix — render it underlined as a clickable button
|
|
2004
|
+
if (this._messageClickAction && msg.startsWith("[")) {
|
|
2005
|
+
const close = msg.indexOf("]");
|
|
2006
|
+
if (close > 0) {
|
|
2007
|
+
const btnText = msg.slice(0, close + 1);
|
|
2008
|
+
const rest = msg.slice(close + 1);
|
|
2009
|
+
const btnStyle = { ...style, underline: true };
|
|
2010
|
+
let sx = putText(this.screen, 0, row, btnText, btnStyle, this.cols);
|
|
2011
|
+
putText(this.screen, sx, row, rest.slice(0, this.cols - sx), style, this.cols - sx);
|
|
2012
|
+
this._messageRowClickZone = { start: 0, end: sx };
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
putText(this.screen, 0, row, msg.slice(0, this.cols), style, this.cols);
|
|
1916
2017
|
}
|
|
1917
2018
|
|
|
1918
2019
|
renderKeyMenu(defaultStyle, statusRow) {
|
|
@@ -2008,9 +2109,46 @@ class App {
|
|
|
2008
2109
|
return -1;
|
|
2009
2110
|
}
|
|
2010
2111
|
|
|
2112
|
+
gotoLocation(buf, loc, pane = this.pane) {
|
|
2113
|
+
if (!buf) return;
|
|
2114
|
+
if (!loc?.subRow) {
|
|
2115
|
+
buf.gotoLoc(loc.line, loc.col);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
buf.gotoLoc(loc.line, 1);
|
|
2119
|
+
this.applyVisualGoto(buf, pane, loc.subRow, loc.col);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
applyVisualGoto(buf, pane, subRow, col = 1) {
|
|
2123
|
+
if (!buf || !pane) return;
|
|
2124
|
+
const softwrap = buf.Settings?.softwrap ?? false;
|
|
2125
|
+
if (!softwrap) {
|
|
2126
|
+
buf.gotoLoc(buf.cursor.y + 1, col);
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
const gutterW = editorGutterWidth(buf);
|
|
2130
|
+
const bufW = Math.max(1, pane.w - gutterW);
|
|
2131
|
+
const wordwrap = buf.Settings?.wordwrap ?? false;
|
|
2132
|
+
const tabsize = buf.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
2133
|
+
const line = buf.lines[buf.cursor.y] ?? "";
|
|
2134
|
+
const breaks = softwrapBreaks(line, bufW, wordwrap, tabsize);
|
|
2135
|
+
const targetSubRow = clamp(Math.trunc(Number(subRow) || 0), 0, Math.max(0, breaks.length - 1));
|
|
2136
|
+
const segStart = breaks[targetSubRow] ?? 0;
|
|
2137
|
+
buf.cursor.x = visualColToCharIdx(line, segStart, Math.max(0, Math.trunc(Number(col) || 1) - 1));
|
|
2138
|
+
buf.ensureCursor();
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
applyPendingVisualGoto(pane) {
|
|
2142
|
+
const pending = pane?.buffer?._pendingVisualGoto;
|
|
2143
|
+
if (!pending) return;
|
|
2144
|
+
delete pane.buffer._pendingVisualGoto;
|
|
2145
|
+
this.applyVisualGoto(pane.buffer, pane, pending.subRow, pending.col);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2011
2148
|
renderEditorPane(pane, defaultStyle) {
|
|
2012
2149
|
const buf = pane.buffer;
|
|
2013
2150
|
if (!buf) return;
|
|
2151
|
+
this.applyPendingVisualGoto(pane);
|
|
2014
2152
|
this.updateScrollForPane(pane);
|
|
2015
2153
|
const gutterW = editorGutterWidth(buf);
|
|
2016
2154
|
const braceMatches = findMatchingBracePositions(buf);
|
|
@@ -2066,7 +2204,7 @@ class App {
|
|
|
2066
2204
|
if (lineNumW > 0) {
|
|
2067
2205
|
const prefix = subRow === 0
|
|
2068
2206
|
? lineNumberText(buf, lineNo, row, lineNumW)
|
|
2069
|
-
:
|
|
2207
|
+
: visualLineNumberText(subRow, lineNumW);
|
|
2070
2208
|
putText(this.screen, pane.x + msgW + diffCol, screenRow, prefix, isDirtyLongLine(buf, lineNo) ? dirtyGutterStyle : gutterStyle, lineNumW);
|
|
2071
2209
|
}
|
|
2072
2210
|
};
|
|
@@ -2287,6 +2425,86 @@ class App {
|
|
|
2287
2425
|
buf.scroll.x = charIdxForScrollRight(buf.lines[buf.cursor.y] ?? "", buf.cursor.x, bufW);
|
|
2288
2426
|
}
|
|
2289
2427
|
}
|
|
2428
|
+
|
|
2429
|
+
pageScroll(pane, delta, amount = null) {
|
|
2430
|
+
const buf = pane?.buffer;
|
|
2431
|
+
if (!buf) return;
|
|
2432
|
+
const gutterW = editorGutterWidth(buf);
|
|
2433
|
+
const bufW = Math.max(1, (pane?.w ?? this.cols) - gutterW);
|
|
2434
|
+
const softwrap = buf.Settings?.softwrap ?? false;
|
|
2435
|
+
const wordwrap = softwrap && (buf.Settings?.wordwrap ?? false);
|
|
2436
|
+
const tabsize = buf.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
2437
|
+
const pageOverlap = Math.trunc(Number(buf.Settings?.pageoverlap ?? DEFAULT_SETTINGS.pageoverlap) || 0);
|
|
2438
|
+
const scrollAmount = amount ?? Math.max(1, (pane?.h ?? this.rows) - pageOverlap);
|
|
2439
|
+
|
|
2440
|
+
if (softwrap) {
|
|
2441
|
+
const start = { line: buf.scroll.y, row: buf.scroll.row ?? 0 };
|
|
2442
|
+
const next = delta < 0
|
|
2443
|
+
? slocRetreatN(buf.lines, start, scrollAmount, bufW, wordwrap, tabsize)
|
|
2444
|
+
: slocAdvanceN(buf.lines, start, scrollAmount, bufW, wordwrap, tabsize);
|
|
2445
|
+
buf.scroll.y = next.line;
|
|
2446
|
+
buf.scroll.row = next.row;
|
|
2447
|
+
buf.scroll.x = 0;
|
|
2448
|
+
if (delta > 0) this.scrollAdjust(pane);
|
|
2449
|
+
} else {
|
|
2450
|
+
buf.scroll.y = Math.max(0, (buf.scroll.y ?? 0) + delta * scrollAmount);
|
|
2451
|
+
buf.scroll.row = 0;
|
|
2452
|
+
if (delta > 0) this.scrollAdjust(pane);
|
|
2453
|
+
}
|
|
2454
|
+
buf.allowCursorOffscreen = true;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
scrollAdjust(pane) {
|
|
2458
|
+
const buf = pane?.buffer;
|
|
2459
|
+
if (!buf || buf.lines.length === 0) return;
|
|
2460
|
+
const gutterW = editorGutterWidth(buf);
|
|
2461
|
+
const bufW = Math.max(1, (pane?.w ?? this.cols) - gutterW);
|
|
2462
|
+
const softwrap = buf.Settings?.softwrap ?? false;
|
|
2463
|
+
const wordwrap = softwrap && (buf.Settings?.wordwrap ?? false);
|
|
2464
|
+
const tabsize = buf.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
2465
|
+
if (softwrap) {
|
|
2466
|
+
const endLine = Math.max(0, buf.lines.length - 1);
|
|
2467
|
+
const endBreaks = softwrapBreaks(buf.lines[endLine] ?? "", bufW, wordwrap, tabsize);
|
|
2468
|
+
const end = { line: endLine, row: Math.max(0, endBreaks.length - 1) };
|
|
2469
|
+
const start = { line: buf.scroll.y, row: buf.scroll.row ?? 0 };
|
|
2470
|
+
if (slocDiff(buf.lines, start, end, bufW, wordwrap, tabsize) < (pane?.h ?? this.rows) - 1) {
|
|
2471
|
+
const adjusted = slocRetreatN(buf.lines, end, Math.max(0, (pane?.h ?? this.rows) - 1), bufW, wordwrap, tabsize);
|
|
2472
|
+
buf.scroll.y = adjusted.line;
|
|
2473
|
+
buf.scroll.row = adjusted.row;
|
|
2474
|
+
}
|
|
2475
|
+
} else {
|
|
2476
|
+
buf.scroll.y = Math.min(buf.scroll.y ?? 0, Math.max(0, buf.lines.length - (pane?.h ?? this.rows)));
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
cursorPage(pane, delta, { select = false, amount = null } = {}) {
|
|
2481
|
+
const buf = pane?.buffer;
|
|
2482
|
+
if (!buf) return;
|
|
2483
|
+
const pageOverlap = Math.trunc(Number(buf.Settings?.pageoverlap ?? DEFAULT_SETTINGS.pageoverlap) || 0);
|
|
2484
|
+
const selectionEndNewline = !select && delta > 0 && pane.selection?.end?.x === 0;
|
|
2485
|
+
let scrollAmount = amount ?? Math.max(1, (pane?.h ?? this.rows) - pageOverlap);
|
|
2486
|
+
if (selectionEndNewline) scrollAmount = Math.max(1, scrollAmount - 1);
|
|
2487
|
+
const move = () => {
|
|
2488
|
+
const softwrap = buf.Settings?.softwrap ?? false;
|
|
2489
|
+
if (softwrap) {
|
|
2490
|
+
for (let i = 0; i < scrollAmount; i++) {
|
|
2491
|
+
if (delta < 0) this._moveUpVisual(buf, pane);
|
|
2492
|
+
else this._moveDownVisual(buf, pane);
|
|
2493
|
+
}
|
|
2494
|
+
} else {
|
|
2495
|
+
buf.page(delta, scrollAmount);
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
if (select) extendSelection(pane, buf, move);
|
|
2499
|
+
else {
|
|
2500
|
+
pane.selection = null;
|
|
2501
|
+
move();
|
|
2502
|
+
}
|
|
2503
|
+
if (selectionEndNewline) buf.moveHome();
|
|
2504
|
+
this.pageScroll(pane, delta, scrollAmount);
|
|
2505
|
+
buf.allowCursorOffscreen = false;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2290
2508
|
// Softwrap-aware vertical cursor movement.
|
|
2291
2509
|
// Moves cursor by one visual row, maintaining the target visual X column.
|
|
2292
2510
|
_softwrapGetContext(buf, pane) {
|
|
@@ -2395,6 +2613,12 @@ class App {
|
|
|
2395
2613
|
async _dispatchInput(data) {
|
|
2396
2614
|
const text = decoder.decode(data);
|
|
2397
2615
|
|
|
2616
|
+
// Any non-mouse input clears the clipboard alt-copy action
|
|
2617
|
+
{
|
|
2618
|
+
const _evts = parseInputEvents(data);
|
|
2619
|
+
if (_evts.some(e => e.type !== "mouse")) this._messageClickAction = null;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2398
2622
|
// Any non-mouse input stops TTS
|
|
2399
2623
|
if (this._ttsState) {
|
|
2400
2624
|
const events = parseInputEvents(data);
|
|
@@ -2504,12 +2728,14 @@ class App {
|
|
|
2504
2728
|
else if (event.type === "paste") await this.handlePrompt(event.text);
|
|
2505
2729
|
else await this.handleEvent(event);
|
|
2506
2730
|
}
|
|
2731
|
+
this._syncPrimarySelection();
|
|
2507
2732
|
return;
|
|
2508
2733
|
}
|
|
2509
2734
|
|
|
2510
2735
|
for (const event of parseInputEvents(data)) {
|
|
2511
2736
|
await this.handleEvent(event);
|
|
2512
2737
|
}
|
|
2738
|
+
this._syncPrimarySelection();
|
|
2513
2739
|
}
|
|
2514
2740
|
|
|
2515
2741
|
async handleEvent(event) {
|
|
@@ -2575,40 +2801,9 @@ class App {
|
|
|
2575
2801
|
};
|
|
2576
2802
|
buf.cursor = { ...this.pane.selection.end };
|
|
2577
2803
|
break;
|
|
2578
|
-
case "ctrl-c":
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
const text = getSelectionText(buf, sel);
|
|
2582
|
-
this.clipboard.write(text);
|
|
2583
|
-
this.message = `Copied to ${this.clipboard.methodName()} clipboard`;
|
|
2584
|
-
} else {
|
|
2585
|
-
this.clipboard.write(buf.currentLineText() + "\n");
|
|
2586
|
-
this.message = `Copied line to ${this.clipboard.methodName()} clipboard`;
|
|
2587
|
-
}
|
|
2588
|
-
break;
|
|
2589
|
-
}
|
|
2590
|
-
case "ctrl-x": { //cut
|
|
2591
|
-
buf.pushUndo();
|
|
2592
|
-
if (this.pane?.selection) {
|
|
2593
|
-
const text = deleteSelection(buf, this.pane);
|
|
2594
|
-
this.clipboard.write(text);
|
|
2595
|
-
this.message = `Cut to ${this.clipboard.methodName()} clipboard`;
|
|
2596
|
-
} else {
|
|
2597
|
-
this.clipboard.write(buf.cutLine() + "\n");
|
|
2598
|
-
this.message = `Cut line to ${this.clipboard.methodName()} clipboard`;
|
|
2599
|
-
}
|
|
2600
|
-
break;
|
|
2601
|
-
}
|
|
2602
|
-
case "ctrl-v": { //paste
|
|
2603
|
-
const pasted = this.clipboard.read();
|
|
2604
|
-
if (pasted) {
|
|
2605
|
-
buf.pushUndo();
|
|
2606
|
-
if (this.pane?.selection) deleteSelection(buf, this.pane);
|
|
2607
|
-
buf.insert(pasted);
|
|
2608
|
-
this.message = pasteStatusMessage(this.clipboard.methodName(), pasted);
|
|
2609
|
-
}
|
|
2610
|
-
break;
|
|
2611
|
-
}
|
|
2804
|
+
case "ctrl-c": await this.handleCommand("copy"); break; //copy
|
|
2805
|
+
case "ctrl-x": await this.handleCommand("cut"); break; //cut
|
|
2806
|
+
case "ctrl-v": await this.handleCommand("paste"); break; //paste
|
|
2612
2807
|
case "ctrl-z": //undo
|
|
2613
2808
|
if (buf.undo()) this.pane.selection = null;
|
|
2614
2809
|
else this.message = "Nothing to undo";
|
|
@@ -2722,17 +2917,7 @@ class App {
|
|
|
2722
2917
|
}
|
|
2723
2918
|
break;
|
|
2724
2919
|
}
|
|
2725
|
-
case "ctrl-k": //cutLine
|
|
2726
|
-
buf.pushUndo();
|
|
2727
|
-
if (this.pane?.selection) {
|
|
2728
|
-
const text = deleteSelection(buf, this.pane);
|
|
2729
|
-
this.clipboard.write(text);
|
|
2730
|
-
this.message = `Cut to ${this.clipboard.methodName()} clipboard`;
|
|
2731
|
-
} else {
|
|
2732
|
-
this.clipboard.write(buf.cutLine() + "\n");
|
|
2733
|
-
this.message = `Cut line to ${this.clipboard.methodName()} clipboard`;
|
|
2734
|
-
}
|
|
2735
|
-
break;
|
|
2920
|
+
case "ctrl-k": await this.handleCommand("cutline"); break; //cutLine
|
|
2736
2921
|
case "ctrl-o": //open
|
|
2737
2922
|
this.openCommandMode("open ");
|
|
2738
2923
|
break;
|
|
@@ -2840,11 +3025,11 @@ class App {
|
|
|
2840
3025
|
break;
|
|
2841
3026
|
case "shift-pageup":
|
|
2842
3027
|
buf._lastVisX = null;
|
|
2843
|
-
|
|
3028
|
+
this.cursorPage(this.pane, -1, { select: true });
|
|
2844
3029
|
break;
|
|
2845
3030
|
case "shift-pagedown":
|
|
2846
3031
|
buf._lastVisX = null;
|
|
2847
|
-
|
|
3032
|
+
this.cursorPage(this.pane, 1, { select: true });
|
|
2848
3033
|
break;
|
|
2849
3034
|
case "home":
|
|
2850
3035
|
buf._lastVisX = null;
|
|
@@ -2912,11 +3097,11 @@ class App {
|
|
|
2912
3097
|
break;
|
|
2913
3098
|
case "pageup":
|
|
2914
3099
|
buf._lastVisX = null;
|
|
2915
|
-
await runAction("
|
|
3100
|
+
await runAction("CursorPageUp", this);
|
|
2916
3101
|
break;
|
|
2917
3102
|
case "pagedown":
|
|
2918
3103
|
buf._lastVisX = null;
|
|
2919
|
-
await runAction("
|
|
3104
|
+
await runAction("CursorPageDown", this);
|
|
2920
3105
|
break;
|
|
2921
3106
|
case "tab":
|
|
2922
3107
|
if (buf.acHas) buf.cycleAutocomplete(true);
|
|
@@ -2988,14 +3173,14 @@ class App {
|
|
|
2988
3173
|
}
|
|
2989
3174
|
if (ch < " " && ch !== "\t") continue;
|
|
2990
3175
|
buf.insertChar(ch);
|
|
2991
|
-
await this.context.plugins?.run("onRune", makePaneAdapter(buf), ch);
|
|
2992
|
-
await this.context.jsPlugins?.run("onRune", makePaneAdapter(buf), ch);
|
|
3176
|
+
await this.context.plugins?.run("onRune", makePaneAdapter(buf, this), ch);
|
|
3177
|
+
await this.context.jsPlugins?.run("onRune", makePaneAdapter(buf, this), ch);
|
|
2993
3178
|
}
|
|
2994
3179
|
}
|
|
2995
3180
|
|
|
2996
3181
|
async runPluginBool(fn) {
|
|
2997
|
-
const luaOk = await this.context.plugins?.runBool(fn, makePaneAdapter(this.buffer)) ?? true;
|
|
2998
|
-
const jsOk = await this.context.jsPlugins?.runBool(fn, makePaneAdapter(this.buffer)) ?? true;
|
|
3182
|
+
const luaOk = await this.context.plugins?.runBool(fn, makePaneAdapter(this.buffer, this)) ?? true;
|
|
3183
|
+
const jsOk = await this.context.jsPlugins?.runBool(fn, makePaneAdapter(this.buffer, this)) ?? true;
|
|
2999
3184
|
return luaOk && jsOk;
|
|
3000
3185
|
}
|
|
3001
3186
|
|
|
@@ -3010,6 +3195,10 @@ class App {
|
|
|
3010
3195
|
this.render();
|
|
3011
3196
|
return;
|
|
3012
3197
|
}
|
|
3198
|
+
if (await this.handleMessageRowMouse(event)) {
|
|
3199
|
+
this.render();
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3013
3202
|
if (await this.handleStatusBarMouse(event)) {
|
|
3014
3203
|
this.render();
|
|
3015
3204
|
return;
|
|
@@ -3060,7 +3249,7 @@ class App {
|
|
|
3060
3249
|
}
|
|
3061
3250
|
return;
|
|
3062
3251
|
}
|
|
3063
|
-
if (!["down", "up", "drag"].includes(event.action) || !["left", "none"].includes(event.button)) return;
|
|
3252
|
+
if (!["down", "up", "drag"].includes(event.action) || !["left", "none", "middle"].includes(event.button)) return;
|
|
3064
3253
|
buf.allowCursorOffscreen = false;
|
|
3065
3254
|
const gutterW = _swGutterW;
|
|
3066
3255
|
const localY = event.y - clicked.y;
|
|
@@ -3081,6 +3270,18 @@ class App {
|
|
|
3081
3270
|
}
|
|
3082
3271
|
buf.cursor.y = y;
|
|
3083
3272
|
buf.cursor.x = x;
|
|
3273
|
+
if (event.button === "middle") {
|
|
3274
|
+
if (event.action === "down") {
|
|
3275
|
+
const pasted = this.clipboard.read("primary");
|
|
3276
|
+
if (pasted) {
|
|
3277
|
+
buf.pushUndo();
|
|
3278
|
+
this.pane.selection = null;
|
|
3279
|
+
buf.insert(pasted);
|
|
3280
|
+
this.message = pasteStatusMessage("primary", pasted);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3084
3285
|
if (event.action === "down") {
|
|
3085
3286
|
if (inGutter) {
|
|
3086
3287
|
// Message column (first msgW cols): show message text in infobar, no selection.
|
|
@@ -3162,6 +3363,15 @@ class App {
|
|
|
3162
3363
|
buf.ensureCursor();
|
|
3163
3364
|
}
|
|
3164
3365
|
|
|
3366
|
+
_syncPrimarySelection() {
|
|
3367
|
+
const sel = this.pane?.selection;
|
|
3368
|
+
if (!sel || sameLoc(sel.start, sel.end)) return;
|
|
3369
|
+
const buf = this.buffer;
|
|
3370
|
+
if (!buf) return;
|
|
3371
|
+
const text = getSelectionText(buf, sel);
|
|
3372
|
+
if (text) this.clipboard.write(text, "primary");
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3165
3375
|
handleSuggestionMouse(event) {
|
|
3166
3376
|
if (this._suggestionsRow == null || event.y !== this._suggestionsRow) return false;
|
|
3167
3377
|
if (event.action !== "down" || event.button !== "left") return false;
|
|
@@ -3181,6 +3391,19 @@ class App {
|
|
|
3181
3391
|
return true;
|
|
3182
3392
|
}
|
|
3183
3393
|
|
|
3394
|
+
async handleMessageRowMouse(event) {
|
|
3395
|
+
if (this._messageRowY == null || event.y !== this._messageRowY) return false;
|
|
3396
|
+
if (event.action !== "down" || event.button !== "left") return false;
|
|
3397
|
+
const zone = this._messageRowClickZone;
|
|
3398
|
+
if (!zone || !this._messageClickAction) return false;
|
|
3399
|
+
if (event.x < zone.start || event.x >= zone.end) return false;
|
|
3400
|
+
const result = this._messageClickAction();
|
|
3401
|
+
this._messageClickAction = null;
|
|
3402
|
+
this._messageRowClickZone = null;
|
|
3403
|
+
if (typeof result === "string") this.message = result;
|
|
3404
|
+
return true;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3184
3407
|
async handleStatusBarMouse(event) {
|
|
3185
3408
|
if (this._statusBarRow == null || event.y !== this._statusBarRow) return false;
|
|
3186
3409
|
if (event.action !== "down" || event.button !== "left") return false;
|
|
@@ -3238,7 +3461,11 @@ class App {
|
|
|
3238
3461
|
break;
|
|
3239
3462
|
}
|
|
3240
3463
|
case "fmt":
|
|
3241
|
-
if (buf) {
|
|
3464
|
+
if (buf) {
|
|
3465
|
+
buf.fileformat = buf.fileformat === "dos" ? "unix" : "dos";
|
|
3466
|
+
buf.Settings.fileformat = buf.fileformat;
|
|
3467
|
+
buf.modified = true;
|
|
3468
|
+
}
|
|
3242
3469
|
break;
|
|
3243
3470
|
case "enc":
|
|
3244
3471
|
if (buf) {
|
|
@@ -3298,9 +3525,9 @@ class App {
|
|
|
3298
3525
|
if (index < 0 || index >= this.tabs.length || index === this.activeTabIdx) return false;
|
|
3299
3526
|
this.activeTabIdx = index;
|
|
3300
3527
|
this.message = "";
|
|
3301
|
-
if (this.context.plugins && this.buffer) this.context.plugins.curPaneAdapter = makePaneAdapter(this.buffer);
|
|
3302
|
-
this.context.plugins?.run("onSetActive", makePaneAdapter(this.buffer));
|
|
3303
|
-
if (this.buffer) this.context.jsPlugins?.run("onSetActive", makePaneAdapter(this.buffer));
|
|
3528
|
+
if (this.context.plugins && this.buffer) this.context.plugins.curPaneAdapter = makePaneAdapter(this.buffer, this);
|
|
3529
|
+
this.context.plugins?.run("onSetActive", makePaneAdapter(this.buffer, this));
|
|
3530
|
+
if (this.buffer) this.context.jsPlugins?.run("onSetActive", makePaneAdapter(this.buffer, this));
|
|
3304
3531
|
return true;
|
|
3305
3532
|
}
|
|
3306
3533
|
|
|
@@ -3551,14 +3778,14 @@ class App {
|
|
|
3551
3778
|
this.openPrompt("Save as: ", async (value) => {
|
|
3552
3779
|
if (value) {
|
|
3553
3780
|
await this.buffer.save(resolve(expandHome(value)));
|
|
3554
|
-
await this.context.plugins?.run("onSave", makePaneAdapter(this.buffer));
|
|
3555
|
-
await this.context.jsPlugins?.run("onSave", makePaneAdapter(this.buffer));
|
|
3781
|
+
await this.context.plugins?.run("onSave", makePaneAdapter(this.buffer, this));
|
|
3782
|
+
await this.context.jsPlugins?.run("onSave", makePaneAdapter(this.buffer, this));
|
|
3556
3783
|
}
|
|
3557
3784
|
}, { completer: fileComplete, initial });
|
|
3558
3785
|
} else {
|
|
3559
3786
|
await this.buffer.save();
|
|
3560
|
-
await this.context.plugins?.run("onSave", makePaneAdapter(this.buffer));
|
|
3561
|
-
await this.context.jsPlugins?.run("onSave", makePaneAdapter(this.buffer));
|
|
3787
|
+
await this.context.plugins?.run("onSave", makePaneAdapter(this.buffer, this));
|
|
3788
|
+
await this.context.jsPlugins?.run("onSave", makePaneAdapter(this.buffer, this));
|
|
3562
3789
|
await this._saveCursorForBuf(this.buffer);
|
|
3563
3790
|
}
|
|
3564
3791
|
} catch (error) {
|
|
@@ -3715,10 +3942,10 @@ class App {
|
|
|
3715
3942
|
this.tabs.splice(this.activeTabIdx, 1);
|
|
3716
3943
|
this.activeTabIdx = Math.min(this.activeTabIdx, this.tabs.length - 1);
|
|
3717
3944
|
this.message = "";
|
|
3718
|
-
if (this.context.plugins && this.buffer) this.context.plugins.curPaneAdapter = makePaneAdapter(this.buffer);
|
|
3719
|
-
await this.context.plugins?.run("onSetActive", makePaneAdapter(this.buffer));
|
|
3945
|
+
if (this.context.plugins && this.buffer) this.context.plugins.curPaneAdapter = makePaneAdapter(this.buffer, this);
|
|
3946
|
+
await this.context.plugins?.run("onSetActive", makePaneAdapter(this.buffer, this));
|
|
3720
3947
|
await this.context.plugins?.run("onBufferClose", closing);
|
|
3721
|
-
if (this.buffer) this.context.jsPlugins?.run("onSetActive", makePaneAdapter(this.buffer));
|
|
3948
|
+
if (this.buffer) this.context.jsPlugins?.run("onSetActive", makePaneAdapter(this.buffer, this));
|
|
3722
3949
|
await this.context.jsPlugins?.run("onBufferClose", closing);
|
|
3723
3950
|
this.render();
|
|
3724
3951
|
}
|
|
@@ -3891,6 +4118,9 @@ class App {
|
|
|
3891
4118
|
this.message = `colorscheme: ${err.message}`;
|
|
3892
4119
|
}
|
|
3893
4120
|
}
|
|
4121
|
+
if (opt === "clipboard") {
|
|
4122
|
+
await this.reinitializeClipboard(buf.Settings[opt]);
|
|
4123
|
+
}
|
|
3894
4124
|
}
|
|
3895
4125
|
break;
|
|
3896
4126
|
}
|
|
@@ -3937,8 +4167,8 @@ class App {
|
|
|
3937
4167
|
if (answer === "y") {
|
|
3938
4168
|
try {
|
|
3939
4169
|
await buf.save(target);
|
|
3940
|
-
await this.context.plugins?.run("onSave", makePaneAdapter(buf));
|
|
3941
|
-
await this.context.jsPlugins?.run("onSave", makePaneAdapter(buf));
|
|
4170
|
+
await this.context.plugins?.run("onSave", makePaneAdapter(buf, this));
|
|
4171
|
+
await this.context.jsPlugins?.run("onSave", makePaneAdapter(buf, this));
|
|
3942
4172
|
} catch (err) {
|
|
3943
4173
|
this.message = err.message;
|
|
3944
4174
|
}
|
|
@@ -3948,8 +4178,8 @@ class App {
|
|
|
3948
4178
|
} else if (saveArgs.length > 0) {
|
|
3949
4179
|
try {
|
|
3950
4180
|
await buf.save(resolve(expandHome(saveArgs[0])));
|
|
3951
|
-
await this.context.plugins?.run("onSave", makePaneAdapter(buf));
|
|
3952
|
-
await this.context.jsPlugins?.run("onSave", makePaneAdapter(buf));
|
|
4181
|
+
await this.context.plugins?.run("onSave", makePaneAdapter(buf, this));
|
|
4182
|
+
await this.context.jsPlugins?.run("onSave", makePaneAdapter(buf, this));
|
|
3953
4183
|
}
|
|
3954
4184
|
catch (err) { this.message = err.message; }
|
|
3955
4185
|
} else {
|
|
@@ -4006,10 +4236,9 @@ class App {
|
|
|
4006
4236
|
this.toggleComment();
|
|
4007
4237
|
break;
|
|
4008
4238
|
case "goto": {
|
|
4009
|
-
if (cmdArgs.length === 0) { this.message = "Usage: goto <line[:col]>"; break; }
|
|
4239
|
+
if (cmdArgs.length === 0) { this.message = "Usage: goto <line[.subrow][:col]>"; break; }
|
|
4010
4240
|
try {
|
|
4011
|
-
|
|
4012
|
-
buf.gotoLoc(line, col);
|
|
4241
|
+
this.gotoLocation(buf, parseLineCol(cmdArgs[0]), this.pane);
|
|
4013
4242
|
this.pane.selection = null;
|
|
4014
4243
|
} catch (error) {
|
|
4015
4244
|
this.message = String(error.message || error);
|
|
@@ -4203,7 +4432,7 @@ class App {
|
|
|
4203
4432
|
await this.context.plugins?.run("onBufferOpen", buf);
|
|
4204
4433
|
await this.context.jsPlugins?.run("onBufferOpen", buf);
|
|
4205
4434
|
}
|
|
4206
|
-
if (cmd !== "togglelocal" && cfg && opt in cfg.globalSettings) {
|
|
4435
|
+
if (cmd !== "togglelocal" && cfg && opt in cfg.globalSettings && !LOCAL_SETTINGS.has(opt)) {
|
|
4207
4436
|
try { cfg.setGlobalOptionNative(opt, newVal, { modified: true }); await cfg.saveSettings(); } catch {}
|
|
4208
4437
|
}
|
|
4209
4438
|
break;
|
|
@@ -4217,12 +4446,13 @@ class App {
|
|
|
4217
4446
|
const defVal = opt in defaults ? defaults[opt] : true;
|
|
4218
4447
|
try { buf.SetOption(opt, String(defVal)); } catch (err) { this.message = String(err.message || err); break; }
|
|
4219
4448
|
this.message = `${opt} = ${defVal}`;
|
|
4220
|
-
if (cfgR && opt in cfgR.globalSettings) {
|
|
4449
|
+
if (cfgR && opt in cfgR.globalSettings && !LOCAL_SETTINGS.has(opt)) {
|
|
4221
4450
|
try { cfgR.setGlobalOptionNative(opt, defVal, { modified: true }); await cfgR.saveSettings(); } catch {}
|
|
4222
4451
|
}
|
|
4223
4452
|
if (opt === "colorscheme" && this.context?.runtime) {
|
|
4224
4453
|
try { this.context.colorscheme = await new Colorscheme(this.context.runtime).load(String(defVal)); } catch {}
|
|
4225
4454
|
}
|
|
4455
|
+
if (opt === "clipboard") await this.reinitializeClipboard(defVal);
|
|
4226
4456
|
break;
|
|
4227
4457
|
}
|
|
4228
4458
|
case "jump": {
|
|
@@ -4371,11 +4601,66 @@ class App {
|
|
|
4371
4601
|
await this.runAlert(content);
|
|
4372
4602
|
break;
|
|
4373
4603
|
}
|
|
4604
|
+
case "copy": {
|
|
4605
|
+
this._freshClip = false;
|
|
4606
|
+
const sel = this.pane?.selection;
|
|
4607
|
+
const copyText = sel ? getSelectionText(buf, sel) : (buf.currentLineText() + "\n");
|
|
4608
|
+
this.clipboard.write(copyText);
|
|
4609
|
+
this.message = clipboardCopyMsg(this.clipboard, copyText, sel ? "selection" : "line");
|
|
4610
|
+
this._messageClickAction = clipboardAltAction(this.clipboard, copyText);
|
|
4611
|
+
break;
|
|
4612
|
+
}
|
|
4613
|
+
case "cut": {
|
|
4614
|
+
this._freshClip = false;
|
|
4615
|
+
buf.pushUndo();
|
|
4616
|
+
const cutText = this.pane?.selection
|
|
4617
|
+
? deleteSelection(buf, this.pane)
|
|
4618
|
+
: (buf.cutLine() + "\n");
|
|
4619
|
+
const cutKind = this.pane?.selection ? "selection" : "line";
|
|
4620
|
+
this.clipboard.write(cutText);
|
|
4621
|
+
this.message = clipboardCopyMsg(this.clipboard, cutText, cutKind, "Cut");
|
|
4622
|
+
this._messageClickAction = clipboardAltAction(this.clipboard, cutText);
|
|
4623
|
+
break;
|
|
4624
|
+
}
|
|
4625
|
+
case "cutline": {
|
|
4626
|
+
buf.pushUndo();
|
|
4627
|
+
if (this.pane?.selection) {
|
|
4628
|
+
this._freshClip = false;
|
|
4629
|
+
const text = deleteSelection(buf, this.pane);
|
|
4630
|
+
this.clipboard.write(text);
|
|
4631
|
+
this.message = clipboardCopyMsg(this.clipboard, text, "selection", "Cut");
|
|
4632
|
+
this._messageClickAction = clipboardAltAction(this.clipboard, text);
|
|
4633
|
+
} else {
|
|
4634
|
+
const prev = this._freshClip ? (this.clipboard.read() ?? "") : "";
|
|
4635
|
+
const line = buf.cutLine() + "\n";
|
|
4636
|
+
this.clipboard.write(prev + line);
|
|
4637
|
+
this._freshClip = true;
|
|
4638
|
+
const total = (prev + line).split("\n").length - 1;
|
|
4639
|
+
const label = total > 1 ? `${total} lines` : "line";
|
|
4640
|
+
this.message = clipboardCopyMsg(this.clipboard, prev + line, label, "Cut");
|
|
4641
|
+
this._messageClickAction = clipboardAltAction(this.clipboard, prev + line);
|
|
4642
|
+
}
|
|
4643
|
+
break;
|
|
4644
|
+
}
|
|
4645
|
+
case "paste": {
|
|
4646
|
+
this._freshClip = false;
|
|
4647
|
+
const pasted = this.clipboard.read();
|
|
4648
|
+
if (pasted) {
|
|
4649
|
+
buf.pushUndo();
|
|
4650
|
+
if (this.pane?.selection) deleteSelection(buf, this.pane);
|
|
4651
|
+
buf.insert(pasted);
|
|
4652
|
+
this.message = pasteStatusMessage(this.clipboard.readMethodName(), pasted);
|
|
4653
|
+
}
|
|
4654
|
+
break;
|
|
4655
|
+
}
|
|
4656
|
+
case "pasteprimary":
|
|
4657
|
+
await runAction("PastePrimary", this);
|
|
4658
|
+
break;
|
|
4374
4659
|
default: {
|
|
4375
4660
|
const pluginCmd = this.context.plugins?.commands?.get(cmd);
|
|
4376
4661
|
if (pluginCmd) {
|
|
4377
4662
|
try {
|
|
4378
|
-
await pluginCmd(makePaneAdapter(this.buffer), cmdArgs);
|
|
4663
|
+
await pluginCmd(makePaneAdapter(this.buffer, this), cmdArgs);
|
|
4379
4664
|
} catch (e) {
|
|
4380
4665
|
this.message = String(e.message ?? e);
|
|
4381
4666
|
}
|
|
@@ -4700,6 +4985,7 @@ const COMMAND_NAMES = [
|
|
|
4700
4985
|
"cd", "pwd", "tab", "run", "vsplit", "hsplit", "term", "tts", "ttsspeed", "ttspitch", "ttslang", "reopen", "theme", "toggle", "tog",
|
|
4701
4986
|
"togglelocal", "reset", "jump", "tabmove", "tabswitch", "textfilter", "bind", "unbind", "reload", "lintlog", "act", "action", "raw",
|
|
4702
4987
|
"help", "plugin", "showkey", "memusage", "retab", "eval",
|
|
4988
|
+
"copy", "cut", "cutline", "paste", "pasteprimary",
|
|
4703
4989
|
];
|
|
4704
4990
|
|
|
4705
4991
|
const SUPPORTED_ENCODING_LABELS = [
|
|
@@ -4894,10 +5180,40 @@ function isEmptyUntitledBuffer(buffer) {
|
|
|
4894
5180
|
return !buffer.path && !buffer.modified && buffer.lines.length === 1 && buffer.lines[0] === "";
|
|
4895
5181
|
}
|
|
4896
5182
|
|
|
4897
|
-
function
|
|
5183
|
+
function formatAbsoluteCursorLocation(buffer) {
|
|
5184
|
+
if (!buffer) return "+1:1";
|
|
5185
|
+
const y = clamp(buffer.cursor?.y ?? 0, 0, Math.max(0, (buffer.lines?.length ?? 1) - 1));
|
|
5186
|
+
const line = buffer.lines?.[y] ?? "";
|
|
5187
|
+
const x = normalizeCharBoundary(line, buffer.cursor?.x ?? 0);
|
|
5188
|
+
return `+${y + 1}:${x + 1}`;
|
|
5189
|
+
}
|
|
5190
|
+
|
|
5191
|
+
function formatCursorLocation(buffer, pane = null) {
|
|
5192
|
+
if (!buffer) return "+1.0:1";
|
|
5193
|
+
const y = clamp(buffer.cursor?.y ?? 0, 0, Math.max(0, (buffer.lines?.length ?? 1) - 1));
|
|
5194
|
+
const line = buffer.lines?.[y] ?? "";
|
|
5195
|
+
const x = normalizeCharBoundary(line, buffer.cursor?.x ?? 0);
|
|
5196
|
+
let subRow = 0;
|
|
5197
|
+
let col = x + 1;
|
|
5198
|
+
if (pane && (buffer.Settings?.softwrap ?? false)) {
|
|
5199
|
+
const gutterW = editorGutterWidth(buffer);
|
|
5200
|
+
const bufW = Math.max(1, (pane.w ?? process.stdout.columns ?? 80) - gutterW);
|
|
5201
|
+
const wordwrap = buffer.Settings?.wordwrap ?? false;
|
|
5202
|
+
const tabsize = buffer.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
5203
|
+
const breaks = softwrapBreaks(line, bufW, wordwrap, tabsize);
|
|
5204
|
+
subRow = softwrapRowOfCharIdx(breaks, x);
|
|
5205
|
+
const segStart = breaks[subRow] ?? 0;
|
|
5206
|
+
col = displayWidth(line.slice(segStart, x)) + 1;
|
|
5207
|
+
}
|
|
5208
|
+
return `+${y + 1}.${subRow}:${col}`;
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
function makePaneAdapter(buffer, app = null) {
|
|
4898
5212
|
const pane = {
|
|
4899
5213
|
Buf: makeBufferAdapter(buffer),
|
|
4900
5214
|
Cursor: makeCursorAdapter(buffer),
|
|
5215
|
+
CursorLocation: () => formatCursorLocation(buffer, app?.paneForBuffer?.(buffer) ?? app?.pane ?? null),
|
|
5216
|
+
AbsoluteCursorLocation: () => formatAbsoluteCursorLocation(buffer),
|
|
4901
5217
|
Save: () => buffer.save(),
|
|
4902
5218
|
Backspace: () => buffer.backspace(),
|
|
4903
5219
|
Delete: () => buffer.deleteForward(),
|
|
@@ -5206,19 +5522,27 @@ function detectTtsCmd() {
|
|
|
5206
5522
|
return null;
|
|
5207
5523
|
}
|
|
5208
5524
|
|
|
5209
|
-
|
|
5525
|
+
function commandHasStartCursor(command = {}) {
|
|
5526
|
+
return Boolean(command.startCursor && command.startCursor.line >= 1);
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
function commandHasStartupJump(command = {}) {
|
|
5530
|
+
return commandHasStartCursor(command) || Boolean(command.searchRegex);
|
|
5531
|
+
}
|
|
5532
|
+
|
|
5533
|
+
async function loadBufferForPath(pathOrUrl, context, command = {}) {
|
|
5210
5534
|
if (isHttpUrl(pathOrUrl)) {
|
|
5211
5535
|
let encoding = context.config?.globalSettings?.encoding ?? DEFAULT_SETTINGS.encoding;
|
|
5212
5536
|
const decoded = await fetchTextWithEncoding(pathOrUrl, encoding);
|
|
5213
5537
|
const text = decoded.text;
|
|
5214
5538
|
encoding = decoded.encoding;
|
|
5215
5539
|
const urlPath = pathOrUrl.replace(/[?#].*$/, "");
|
|
5216
|
-
const buffer = new BufferModel({ path: pathOrUrl, text, command
|
|
5540
|
+
const buffer = new BufferModel({ path: pathOrUrl, text, command, encoding });
|
|
5217
5541
|
attachSyntax(buffer, context, urlPath, text);
|
|
5218
5542
|
return buffer;
|
|
5219
5543
|
}
|
|
5220
|
-
const buffer = await BufferModel.fromFile(pathOrUrl,
|
|
5221
|
-
if (DEFAULT_SETTINGS.savecursor && context?.cursorStates?.[pathOrUrl]) {
|
|
5544
|
+
const buffer = await BufferModel.fromFile(pathOrUrl, command, context);
|
|
5545
|
+
if (DEFAULT_SETTINGS.savecursor && !commandHasStartupJump(command) && context?.cursorStates?.[pathOrUrl]) {
|
|
5222
5546
|
const saved = context.cursorStates[pathOrUrl];
|
|
5223
5547
|
const y = clamp(saved.y ?? 0, 0, buffer.lines.length - 1);
|
|
5224
5548
|
const x = clamp(saved.x ?? 0, 0, buffer.lines[y]?.length ?? 0);
|
|
@@ -5478,6 +5802,15 @@ function lineNumberText(buf, lineNo, row, gutterW) {
|
|
|
5478
5802
|
return String(lineNo + 1).padStart(Math.max(0, gutterW - 1)) + " ";
|
|
5479
5803
|
}
|
|
5480
5804
|
|
|
5805
|
+
function visualLineNumberText(subRow, gutterW) {
|
|
5806
|
+
const text = `.${subRow}`;
|
|
5807
|
+
const numberW = Math.max(0, gutterW - 1);
|
|
5808
|
+
const padded = text.length >= numberW
|
|
5809
|
+
? text.slice(text.length - numberW)
|
|
5810
|
+
: text.padStart(numberW);
|
|
5811
|
+
return padded + " ";
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5481
5814
|
const BRACE_PAIRS = { "(": ")", "[": "]", "{": "}" };
|
|
5482
5815
|
const BRACE_REVERSE = { ")": "(", "]": "[", "}": "{" };
|
|
5483
5816
|
|
|
@@ -5740,7 +6073,7 @@ async function loadBuffers(files, command) {
|
|
|
5740
6073
|
if (files.length > 0) {
|
|
5741
6074
|
for (const file of files) {
|
|
5742
6075
|
try {
|
|
5743
|
-
buffers.push(await loadBufferForPath(file, loadBuffers.context ?? {}));
|
|
6076
|
+
buffers.push(await loadBufferForPath(file, loadBuffers.context ?? {}, command));
|
|
5744
6077
|
} catch (error) {
|
|
5745
6078
|
console.error(error.message || error);
|
|
5746
6079
|
}
|
|
@@ -5758,6 +6091,11 @@ async function loadBuffers(files, command) {
|
|
|
5758
6091
|
return buffers.length ? buffers : [new BufferModel({ command })];
|
|
5759
6092
|
}
|
|
5760
6093
|
|
|
6094
|
+
async function printReadmeDocs() {
|
|
6095
|
+
const readme = await Bun.file(join(REPO_ROOT, "README.md")).text();
|
|
6096
|
+
process.stdout.write(Bun.markdown.ansi(readme, { hyperlinks: true }));
|
|
6097
|
+
}
|
|
6098
|
+
|
|
5761
6099
|
async function main() {
|
|
5762
6100
|
const { flags, files: rawFiles } = parseArgs(process.argv.slice(2));
|
|
5763
6101
|
if (flags.help) {
|
|
@@ -5765,7 +6103,6 @@ async function main() {
|
|
|
5765
6103
|
return;
|
|
5766
6104
|
}
|
|
5767
6105
|
if (flags.version) {
|
|
5768
|
-
const clipboard = new ClipboardManager();
|
|
5769
6106
|
const ttsCmd = detectTtsCmd();
|
|
5770
6107
|
console.log(pkg.name+":",pkg.description)
|
|
5771
6108
|
console.log(" Rewritten by: Dr. John (醫者小智)")
|
|
@@ -5774,9 +6111,24 @@ async function main() {
|
|
|
5774
6111
|
console.log("Runtime:", `Bun ${Bun.version}`);
|
|
5775
6112
|
console.log("Platform:", platformId());
|
|
5776
6113
|
console.log("Http client:",detectHttpBackend());
|
|
5777
|
-
console.log("Clipboard:", clipboard.methodName());
|
|
5778
6114
|
console.log("TTS:", ttsCmd ? ttsCmd.cmd[0] : "not found");
|
|
5779
6115
|
console.log({SUPPORTED_ENCODING_LABELS})
|
|
6116
|
+
const clipboard = new ClipboardManager();
|
|
6117
|
+
let osc52Available = false;
|
|
6118
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
6119
|
+
process.stdin.setRawMode?.(true);
|
|
6120
|
+
process.stdin.resume();
|
|
6121
|
+
osc52Available = await probeOSC52(process.stdin, process.stdout, 150);
|
|
6122
|
+
process.stdin.setRawMode?.(false);
|
|
6123
|
+
process.stdin.pause();
|
|
6124
|
+
}
|
|
6125
|
+
const externalName = clipboard.methodName();
|
|
6126
|
+
const backends = osc52Available ? `${externalName}, OSC 52` : externalName;
|
|
6127
|
+
console.log("Clipboard:", backends);
|
|
6128
|
+
return;
|
|
6129
|
+
}
|
|
6130
|
+
if (flags.docs) {
|
|
6131
|
+
await printReadmeDocs();
|
|
5780
6132
|
return;
|
|
5781
6133
|
}
|
|
5782
6134
|
if (flags.options) {
|
|
@@ -5797,7 +6149,7 @@ async function main() {
|
|
|
5797
6149
|
const syntaxDefinitions = await loadSyntaxDefinitions(runtime);
|
|
5798
6150
|
|
|
5799
6151
|
if (flags.cat) {
|
|
5800
|
-
await catFiles(rawFiles, colorscheme, syntaxDefinitions);
|
|
6152
|
+
await catFiles(rawFiles, colorscheme, syntaxDefinitions, config.getGlobalOption("encoding"));
|
|
5801
6153
|
return;
|
|
5802
6154
|
}
|
|
5803
6155
|
|
|
@@ -5835,8 +6187,8 @@ async function main() {
|
|
|
5835
6187
|
}
|
|
5836
6188
|
|
|
5837
6189
|
if (flags.clean) {
|
|
5838
|
-
|
|
5839
|
-
|
|
6190
|
+
await cleanConfig(config, plugins);
|
|
6191
|
+
return;
|
|
5840
6192
|
}
|
|
5841
6193
|
|
|
5842
6194
|
const pluginErr = await plugins.loadAll();
|
|
@@ -5878,7 +6230,7 @@ async function main() {
|
|
|
5878
6230
|
}
|
|
5879
6231
|
const app = new App(buffers, context);
|
|
5880
6232
|
jsPlugins.setApp(app);
|
|
5881
|
-
if (plugins && !pluginErr && app.buffer) plugins.curPaneAdapter = makePaneAdapter(app.buffer);
|
|
6233
|
+
if (plugins && !pluginErr && app.buffer) plugins.curPaneAdapter = makePaneAdapter(app.buffer, app);
|
|
5882
6234
|
// Dispatch all JS plugin lifecycle hooks after setApp so TermMessage,
|
|
5883
6235
|
// CurPane, cmd/action proxies, and buffer APIs all work correctly.
|
|
5884
6236
|
await jsPlugins.run("preinit");
|
|
@@ -6014,6 +6366,26 @@ function uncommentText(line, commentType) {
|
|
|
6014
6366
|
}
|
|
6015
6367
|
return indent + rest;
|
|
6016
6368
|
}
|
|
6369
|
+
function clipboardCopyMsg(clipboard, text, kind, verb = "Copied") {
|
|
6370
|
+
const method = clipboard.methodName();
|
|
6371
|
+
const alt = clipboard.altMethodName();
|
|
6372
|
+
const chars = Array.from(String(text).replace(/\n$/, "")).length;
|
|
6373
|
+
const label = typeof kind === "string" && (kind === "line" || kind.endsWith("lines"))
|
|
6374
|
+
? kind : `${chars} chars`;
|
|
6375
|
+
if (alt) return `[Click:Copy>${alt}] ${method}: ${label} ${verb.toLowerCase()}`;
|
|
6376
|
+
return `${verb} ${label} to ${method} clipboard`;
|
|
6377
|
+
}
|
|
6378
|
+
|
|
6379
|
+
function clipboardAltAction(clipboard, text) {
|
|
6380
|
+
const alt = clipboard.altMethodName();
|
|
6381
|
+
if (!alt) return null;
|
|
6382
|
+
return () => {
|
|
6383
|
+
if (!clipboard.writeAlt(text)) return `${alt}: failed`;
|
|
6384
|
+
const chars = Array.from(String(text).replace(/\n$/, "")).length;
|
|
6385
|
+
return `${alt}: ${chars} chars copied`;
|
|
6386
|
+
};
|
|
6387
|
+
}
|
|
6388
|
+
|
|
6017
6389
|
function pasteStatusMessage(method, text) {
|
|
6018
6390
|
const value = String(text);
|
|
6019
6391
|
const lines = value.split("\n").length;
|
|
@@ -6195,14 +6567,16 @@ function segmentSelection(selection, lineNo, start, end) {
|
|
|
6195
6567
|
function parseLineCol(value) {
|
|
6196
6568
|
const input = String(value).trim();
|
|
6197
6569
|
if (!input) throw new Error("Not enough arguments");
|
|
6198
|
-
const
|
|
6199
|
-
if (
|
|
6200
|
-
|
|
6201
|
-
const
|
|
6202
|
-
const col =
|
|
6570
|
+
const match = input.match(/^(-?\d+)(?:\.(\d+))?(?::(-?\d+))?$/);
|
|
6571
|
+
if (!match) throw new Error("Invalid line number");
|
|
6572
|
+
const line = Number(match[1]);
|
|
6573
|
+
const subRow = match[2] == null ? 0 : Number(match[2]);
|
|
6574
|
+
const col = match[3] == null ? 1 : Number(match[3]);
|
|
6203
6575
|
if (!Number.isInteger(line)) throw new Error("Invalid line number");
|
|
6576
|
+
if (!Number.isInteger(subRow)) throw new Error("Invalid visual line number");
|
|
6577
|
+
if (subRow < 0) throw new Error("Invalid visual line number");
|
|
6204
6578
|
if (!Number.isInteger(col)) throw new Error("Invalid column number");
|
|
6205
|
-
return { line, col };
|
|
6579
|
+
return { line, subRow, col };
|
|
6206
6580
|
}
|
|
6207
6581
|
|
|
6208
6582
|
function parseOptionValue(value) {
|
|
@@ -6218,19 +6592,23 @@ function syncEditorSettings(config) {
|
|
|
6218
6592
|
}
|
|
6219
6593
|
}
|
|
6220
6594
|
|
|
6221
|
-
async function catFiles(files, colorscheme, syntaxDefinitions) {
|
|
6595
|
+
async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAULT_SETTINGS.encoding) {
|
|
6222
6596
|
const targets = files.length > 0 ? files.map((f) => ({ path: f, stdin: false })) : [{ path: null, stdin: true }];
|
|
6223
6597
|
for (const { path: filePath, stdin } of targets) {
|
|
6224
6598
|
let content;
|
|
6225
6599
|
let effectivePath = filePath;
|
|
6226
6600
|
if (stdin) {
|
|
6227
|
-
|
|
6601
|
+
const chunks = [];
|
|
6602
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
6603
|
+
content = decodeTextBytesWithEncoding(Buffer.concat(chunks), encoding).text;
|
|
6228
6604
|
} else if (isHttpUrl(filePath)) {
|
|
6229
|
-
|
|
6605
|
+
const decoded = await fetchTextWithEncoding(filePath, encoding);
|
|
6606
|
+
content = decoded.text;
|
|
6230
6607
|
// Use the URL pathname for syntax/md detection (strip query/hash)
|
|
6231
6608
|
try { effectivePath = new URL(filePath).pathname; } catch { effectivePath = filePath; }
|
|
6232
6609
|
} else {
|
|
6233
|
-
|
|
6610
|
+
const decoded = await readTextFileWithEncoding(filePath, encoding);
|
|
6611
|
+
content = decoded.text;
|
|
6234
6612
|
}
|
|
6235
6613
|
if (effectivePath && /\.md$/i.test(effectivePath)) {
|
|
6236
6614
|
process.stdout.write(
|