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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user