fix(tui): preserved bare png filename pastes

Require bracketed image-path paste detection to see an explicit local path separator or file URI before routing .png-like text to image attachment handling.\n\nFixes #3253
This commit is contained in:
roboomp
2026-06-22 10:46:24 +00:00
parent 320261fca9
commit fe9ffc7912
3 changed files with 36 additions and 1 deletions
@@ -3,6 +3,7 @@ import { $ } from "bun";
import { getEditorTheme, initTheme } from "../theme/theme";
import {
CustomEditor,
extractBracketedImagePastePaths,
SPACE_HOLD_MECHANICAL_RUN,
SPACE_HOLD_RELEASE_MS,
SPACE_REPEAT_MAX_GAP_MS,
@@ -21,6 +22,12 @@ function makeEditor() {
const REPEAT_GAP_MS = 30;
/** A gap above the threshold — looks like a deliberate keypress. */
const TAP_GAP_MS = SPACE_REPEAT_MAX_GAP_MS + 80;
const BRACKETED_PASTE_START = "\x1b[200~";
const BRACKETED_PASTE_END = "\x1b[201~";
function bracketedPaste(text: string): string {
return `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
}
/** Feed `count` spaces `gapMs` apart on the fake clock. The first space of a run has no prior
* space, so its gap is effectively infinite and it always reads as a deliberate tap. */
@@ -66,6 +73,21 @@ describe("CustomEditor placeholder decoration", () => {
});
});
describe("CustomEditor bracketed image-path paste", () => {
it("leaves a pasted bare .png filename on the normal text path", () => {
expect(extractBracketedImagePastePaths(bracketedPaste("icon-photo-default.png"))).toBeUndefined();
});
it("extracts explicit local image paths for attachment", () => {
expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/icon-photo-default.png"))).toEqual([
"/tmp/icon-photo-default.png",
]);
expect(extractBracketedImagePastePaths(bracketedPaste("C:\\Users\\me\\icon-photo-default.png"))).toEqual([
"C:\\Users\\me\\icon-photo-default.png",
]);
});
});
describe("CustomEditor space-hold push-to-talk", () => {
beforeAll(async () => {
await initTheme();
@@ -64,6 +64,9 @@ const BRACKETED_PASTE_END = "\x1b[201~";
const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi;
const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
const URI_SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:/i;
const FILE_URI_REGEX = /^file:\/\//i;
const WINDOWS_DRIVE_PATH_REGEX = /^[a-z]:[\\/]/i;
/** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
* deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
@@ -118,6 +121,12 @@ function normalizePastedImagePath(path: string): string {
return unquoted.replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1");
}
function isExplicitPastedImagePath(path: string): boolean {
if (WINDOWS_DRIVE_PATH_REGEX.test(path) || FILE_URI_REGEX.test(path)) return true;
if (URI_SCHEME_REGEX.test(path)) return false;
return path.includes("/") || path.includes("\\");
}
export function extractBracketedImagePastePaths(data: string): string[] | undefined {
if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
@@ -139,7 +148,7 @@ export function extractBracketedImagePastePaths(data: string): string[] | undefi
if (boundaryEnd === undefined) continue;
const path = normalizePastedImagePath(pasted.slice(segmentStart, boundaryEnd));
if (!path || !BRACKETED_IMAGE_PATH_REGEX.test(path)) return undefined;
if (!path || !BRACKETED_IMAGE_PATH_REGEX.test(path) || !isExplicitPastedImagePath(path)) return undefined;
paths.push(path);
segmentStart = boundaryEnd;