also looking at this
feat(line-editor): support bracketed paste mode
#55
2 files
+108
-14
| @@ -500,6 +500,13 @@ pub const LineEditor = struct { | ||
| 500 | 500 | try self.terminal.enableRawMode(); |
| 501 | 501 | errdefer self.terminal.disableRawMode() catch {}; |
| 502 | 502 | |
| 503 | // Enable bracketed paste so the terminal wraps pasted text in | |
| 504 | // ESC[200~ ... ESC[201~. The defer disables it on every exit path so we | |
| 505 | // never leave the user's terminal stuck in bracketed-paste mode after | |
| 506 | // Den hands control back. See handlePaste for how the text is consumed. | |
| 507 | self.writeBytes("\x1B[?2004h") catch {}; | |
| 508 | defer self.writeBytes("\x1B[?2004l") catch {}; | |
| 509 | ||
| 503 | 510 | // Reset state |
| 504 | 511 | self.cursor = 0; |
| 505 | 512 | self.length = 0; |
| @@ -1269,6 +1276,70 @@ pub const LineEditor = struct { | ||
| 1269 | 1276 | } |
| 1270 | 1277 | } |
| 1271 | 1278 | |
| 1279 | /// Consume a bracketed paste: after ESC[200~ we read raw bytes until the | |
| 1280 | /// ESC[201~ terminator and insert them as literal text. Embedded newlines are | |
| 1281 | /// inserted (normalized to spaces) rather than submitting the line, so | |
| 1282 | /// pasting a multi-line block never auto-executes — the footgun this prevents. | |
| 1283 | fn handlePaste(self: *LineEditor) !void { | |
| 1284 | self.saveUndoState(); | |
| 1285 | self.clearHistorySearch(); | |
| 1286 | ||
| 1287 | // Terminator state machine for ESC [ 2 0 1 ~ | |
| 1288 | const term = [_]u8{ 0x1B, '[', '2', '0', '1', '~' }; | |
| 1289 | var matched: usize = 0; | |
| 1290 | ||
| 1291 | while (true) { | |
| 1292 | const byte = (try self.terminal.readByte()) orelse { | |
| 1293 | // No byte ready yet — wait briefly; a paste arrives as a burst, | |
| 1294 | // so this only spins at the very end. | |
| 1295 | std.Io.sleep(std.Options.debug_io, std.Io.Duration.fromNanoseconds(@as(i96, 1_000_000)), .awake) catch {}; | |
| 1296 | continue; | |
| 1297 | }; | |
| 1298 | ||
| 1299 | // Track progress toward the paste-end terminator. | |
| 1300 | if (byte == term[matched]) { | |
| 1301 | matched += 1; | |
| 1302 | if (matched == term.len) break; // full terminator seen | |
| 1303 | continue; | |
| 1304 | } | |
| 1305 | ||
| 1306 | // Mismatch: bytes tentatively consumed as a partial terminator were | |
| 1307 | // actually pasted content — flush them, then reconsider this byte. | |
| 1308 | if (matched > 0) { | |
| 1309 | for (term[0..matched]) |pending| { | |
| 1310 | self.insertPasteByte(pending); | |
| 1311 | } | |
| 1312 | matched = 0; | |
| 1313 | if (byte == term[0]) { | |
| 1314 | matched = 1; | |
| 1315 | continue; | |
| 1316 | } | |
| 1317 | } | |
| 1318 | ||
| 1319 | self.insertPasteByte(byte); | |
| 1320 | } | |
| 1321 | ||
| 1322 | try self.redrawLine(); | |
| 1323 | } | |
| 1324 | ||
| 1325 | /// Insert one pasted byte WITHOUT redrawing (handlePaste redraws once at the | |
| 1326 | /// end). Newline/CR/tab become a space so the paste stays on one editable | |
| 1327 | /// line and cannot submit itself. | |
| 1328 | fn insertPasteByte(self: *LineEditor, byte: u8) void { | |
| 1329 | if (self.length >= self.buffer.len) return; | |
| 1330 | const ch: u8 = switch (byte) { | |
| 1331 | '\n', '\r', '\t' => ' ', | |
| 1332 | else => byte, | |
| 1333 | }; | |
| 1334 | var i: usize = self.length; | |
| 1335 | while (i > self.cursor) : (i -= 1) { | |
| 1336 | self.buffer[i] = self.buffer[i - 1]; | |
| 1337 | } | |
| 1338 | self.buffer[self.cursor] = ch; | |
| 1339 | self.cursor += 1; | |
| 1340 | self.length += 1; | |
| 1341 | } | |
| 1342 | ||
| 1272 | 1343 | fn deleteChar(self: *LineEditor) !void { |
| 1273 | 1344 | if (self.cursor >= self.length) return; |
| 1274 | 1345 | |
| @@ -1720,6 +1791,8 @@ pub const LineEditor = struct { | ||
| 1720 | 1791 | } |
| 1721 | 1792 | }, |
| 1722 | 1793 | .delete => try self.deleteChar(), |
| 1794 | .paste_start => try self.handlePaste(), | |
| 1795 | .paste_end => {}, // stray paste-end without a start: ignore | |
| 1723 | 1796 | else => {}, |
| 1724 | 1797 | } |
| 1725 | 1798 | } |