@oh-my-pi/pi-tui 16.0.6 → 16.0.8
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 +6 -0
- package/dist/types/bracketed-paste.d.ts +7 -0
- package/package.json +3 -3
- package/src/bracketed-paste.ts +37 -0
- package/src/components/editor.ts +7 -13
- package/src/components/input.ts +12 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.8] - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed bracketed paste under kitty+tmux leaking `[27;5;106~` escape tails throughout the pasted text (newlines became visible garbage instead of line breaks). tmux's default `extended-keys-format=xterm` re-encodes paste control bytes as `modifyOtherKeys` sequences (`ESC[27;5;<code>~`), which the paste sanitizer did not decode — only the sibling `csi-u` form (`ESC[<code>;5u`) was handled. Both forms are now decoded back to their literal control byte (Ctrl+J → "\n") before control-character stripping, and the decoder is shared by the multi-line editor and the single-line modal input.
|
|
10
|
+
|
|
5
11
|
## [16.0.5] - 2026-06-17
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -5,6 +5,13 @@ export type PasteResult = {
|
|
|
5
5
|
pasteContent?: string;
|
|
6
6
|
remaining: string;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Decode tmux's re-encoded control bytes (both `extended-keys-format` variants) inside a
|
|
10
|
+
* bracketed-paste payload back to their literal byte (e.g. Ctrl+J → "\n"). Leaves the rest of
|
|
11
|
+
* the text untouched. Call before any control-character stripping so newlines/tabs survive
|
|
12
|
+
* instead of leaking the printable escape tail into the buffer.
|
|
13
|
+
*/
|
|
14
|
+
export declare function decodeReencodedPasteControls(text: string): string;
|
|
8
15
|
/**
|
|
9
16
|
* Handles bracketed paste mode buffering for terminal input components.
|
|
10
17
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.8",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "16.0.
|
|
41
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.0.8",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.0.8",
|
|
42
42
|
"lru-cache": "11.5.1",
|
|
43
43
|
"marked": "^18.0.5"
|
|
44
44
|
},
|
package/src/bracketed-paste.ts
CHANGED
|
@@ -3,6 +3,43 @@ const PASTE_END = "\x1b[201~";
|
|
|
3
3
|
|
|
4
4
|
export type PasteResult = { handled: false } | { handled: true; pasteContent?: string; remaining: string };
|
|
5
5
|
|
|
6
|
+
// Some terminals re-encode the control bytes inside a bracketed paste as key-event
|
|
7
|
+
// escape sequences (observed with tmux extended-keys passthrough under kitty). tmux
|
|
8
|
+
// emits one of two formats depending on `extended-keys-format`:
|
|
9
|
+
// - csi-u: ESC [ <codepoint> ; 5 u (Ctrl+J → ESC [ 106 ; 5 u)
|
|
10
|
+
// - xterm: ESC [ 27 ; 5 ; <codepoint> ~ (Ctrl+J → ESC [ 27 ; 5 ; 106 ~)
|
|
11
|
+
// Callers must decode these back to the literal control byte (Ctrl+J → "\n") before
|
|
12
|
+
// stripping control chars; otherwise ESC is dropped and the printable tail
|
|
13
|
+
// ("[106;5u" / "[27;5;106~") leaks into the editor.
|
|
14
|
+
//
|
|
15
|
+
// Only Ctrl+<letter> is decoded (codepoint a-z/A-Z → 0x01..0x1A). That is the set tmux
|
|
16
|
+
// actually re-encodes from paste content in practice — TAB (Ctrl+I), LF (Ctrl+J), CR
|
|
17
|
+
// (Ctrl+M), VT (Ctrl+K), FF (Ctrl+L), … Non-letter Ctrl combos (NUL, ESC, FS-US, DEL)
|
|
18
|
+
// never appear as re-encoded paste bytes, so they are left untouched rather than
|
|
19
|
+
// synthesized into raw control bytes. Callers still strip leftover control characters
|
|
20
|
+
// after decoding (the editor keeps "\n"; the single-line input strips all of them).
|
|
21
|
+
const REENCODED_CTRL_CSI_U = /\x1b\[(\d+);5u/g;
|
|
22
|
+
const REENCODED_CTRL_XTERM = /\x1b\[27;5;(\d+)~/g;
|
|
23
|
+
|
|
24
|
+
function decodeReencodedCtrlByte(match: string, code: string): string {
|
|
25
|
+
const cp = Number(code);
|
|
26
|
+
if (cp >= 97 && cp <= 122) return String.fromCharCode(cp - 96); // a-z → Ctrl+A..Ctrl+Z
|
|
27
|
+
if (cp >= 65 && cp <= 90) return String.fromCharCode(cp - 64); // A-Z → Ctrl+A..Ctrl+Z
|
|
28
|
+
return match;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decode tmux's re-encoded control bytes (both `extended-keys-format` variants) inside a
|
|
33
|
+
* bracketed-paste payload back to their literal byte (e.g. Ctrl+J → "\n"). Leaves the rest of
|
|
34
|
+
* the text untouched. Call before any control-character stripping so newlines/tabs survive
|
|
35
|
+
* instead of leaking the printable escape tail into the buffer.
|
|
36
|
+
*/
|
|
37
|
+
export function decodeReencodedPasteControls(text: string): string {
|
|
38
|
+
return text
|
|
39
|
+
.replace(REENCODED_CTRL_CSI_U, decodeReencodedCtrlByte)
|
|
40
|
+
.replace(REENCODED_CTRL_XTERM, decodeReencodedCtrlByte);
|
|
41
|
+
}
|
|
42
|
+
|
|
6
43
|
/**
|
|
7
44
|
* Handles bracketed paste mode buffering for terminal input components.
|
|
8
45
|
*
|
package/src/components/editor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete";
|
|
3
|
-
import { BracketedPasteHandler } from "../bracketed-paste";
|
|
3
|
+
import { BracketedPasteHandler, decodeReencodedPasteControls } from "../bracketed-paste";
|
|
4
4
|
import { getKeybindings, type KeybindingsManager } from "../keybindings";
|
|
5
5
|
import { extractPrintableText, matchesKey } from "../keys";
|
|
6
6
|
import { KillRing } from "../kill-ring";
|
|
@@ -1843,20 +1843,14 @@ export class Editor implements Component, Focusable {
|
|
|
1843
1843
|
});
|
|
1844
1844
|
}
|
|
1845
1845
|
|
|
1846
|
-
/** Normalize raw pasted text: decode tmux
|
|
1846
|
+
/** Normalize raw pasted text: decode tmux re-encoded control bytes (both extended-keys formats),
|
|
1847
|
+
* normalize CRLF and
|
|
1847
1848
|
* NFC (macOS NFD filename drag-drops), expand tabs, and strip control characters except newline. */
|
|
1848
1849
|
#sanitizePastedText(pastedText: string): string {
|
|
1849
|
-
//
|
|
1850
|
-
//
|
|
1851
|
-
//
|
|
1852
|
-
|
|
1853
|
-
// leaking the printable tail (e.g. "[106;5u") into the editor.
|
|
1854
|
-
const decodedText = pastedText.replace(/\x1b\[(\d+);5u/g, (match, code) => {
|
|
1855
|
-
const cp = Number(code);
|
|
1856
|
-
if (cp >= 97 && cp <= 122) return String.fromCharCode(cp - 96);
|
|
1857
|
-
if (cp >= 65 && cp <= 90) return String.fromCharCode(cp - 64);
|
|
1858
|
-
return match;
|
|
1859
|
-
});
|
|
1850
|
+
// Decode tmux's re-encoded control bytes (both extended-keys formats) back to
|
|
1851
|
+
// their literal byte so the per-char filter below preserves newlines instead of
|
|
1852
|
+
// stripping ESC and leaking the printable tail into the editor. See the decoder.
|
|
1853
|
+
const decodedText = decodeReencodedPasteControls(pastedText);
|
|
1860
1854
|
|
|
1861
1855
|
// Clean the pasted text. NFC-normalize so macOS Finder drag-drops of
|
|
1862
1856
|
// Korean filenames (which arrive as NFD: e.g. `ᄒ`+`ᅪ` instead of `화`)
|
package/src/components/input.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BracketedPasteHandler } from "../bracketed-paste";
|
|
1
|
+
import { BracketedPasteHandler, decodeReencodedPasteControls } from "../bracketed-paste";
|
|
2
2
|
import { getKeybindings } from "../keybindings";
|
|
3
3
|
import { extractPrintableText } from "../keys";
|
|
4
4
|
import { KillRing } from "../kill-ring";
|
|
@@ -375,8 +375,12 @@ export class Input implements Component, Focusable {
|
|
|
375
375
|
this.#lastAction = null;
|
|
376
376
|
this.#pushUndo();
|
|
377
377
|
|
|
378
|
-
// Clean the pasted text —
|
|
379
|
-
//
|
|
378
|
+
// Clean the pasted text — decode tmux's re-encoded control bytes (both
|
|
379
|
+
// extended-keys formats, e.g. Ctrl+J → "\n") back to literal bytes so the escape
|
|
380
|
+
// tail does not leak in, remove newlines/carriage returns, expand tabs, NFC-normalize,
|
|
381
|
+
// then strip any remaining control bytes. The decoder can synthesize Ctrl+A..Ctrl+Z
|
|
382
|
+
// (0x01..0x1A) from a paste, and a single-line value must hold none of them — newlines
|
|
383
|
+
// are already gone and tabs are already spaces by the time the C0/DEL strip runs.
|
|
380
384
|
//
|
|
381
385
|
// NFC normalization rationale: macOS Finder drag-drops file paths in NFD
|
|
382
386
|
// (Conjoining Jamo, U+1100..U+11FF). `Bun.stringWidth` counts each
|
|
@@ -387,9 +391,11 @@ export class Input implements Component, Focusable {
|
|
|
387
391
|
// as cursor drift past the visible filename — N×~1.5 cells for a path
|
|
388
392
|
// with N Korean syllables. NFC normalization at paste time stores the
|
|
389
393
|
// value in the same form everything else in the codebase assumes.
|
|
390
|
-
const cleanText = replaceTabs(
|
|
391
|
-
"
|
|
392
|
-
)
|
|
394
|
+
const cleanText = replaceTabs(
|
|
395
|
+
decodeReencodedPasteControls(pastedText).replace(/\r\n/g, "").replace(/\r/g, "").replace(/\n/g, ""),
|
|
396
|
+
)
|
|
397
|
+
.normalize("NFC")
|
|
398
|
+
.replace(/[\x00-\x1F\x7F]/g, "");
|
|
393
399
|
|
|
394
400
|
// Insert at cursor position
|
|
395
401
|
this.#value = this.#value.slice(0, this.#cursor) + cleanText + this.#value.slice(this.#cursor);
|