Files
orca/src/shared/image-path-paste.ts

73 lines
2.5 KiB
TypeScript

import { isWindowsAbsolutePathLike } from './cross-platform-path'
import { IMAGE_FILE_EXTENSIONS } from './file-preview/image-mime-types'
import { shellEscapePath } from './shell/launch/shell-path-quoting'
import { isWslUncPath } from './wsl-paths'
type TerminalTargetShell = 'posix' | 'windows'
// Terminal TUIs recognize bracketed file-path pastes as image attachments, matching clipboard screenshots (#2842).
const IMAGE_DROP_EXTENSIONS = new Set(IMAGE_FILE_EXTENSIONS)
// Why: an ASCII space is unsafe because Codex shlex-splits a raw spaced path into several tokens.
const POSIX_RAW_IMAGE_DROP_UNSAFE_RE = /[ "'`$;&|<>(){}[\]*?!#\\]/
const WINDOWS_RAW_IMAGE_DROP_UNSAFE_RE = /[ "'`$;&|<>(){}[\]*?!#^%]/
/** Match image extensions case-insensitively within POSIX/Windows basenames; dots in parent directories do not count. */
export function isImageDropPath(path: string): boolean {
const lastDot = path.lastIndexOf('.')
if (lastDot === -1) {
return false
}
const lastSeparator = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
if (lastDot < lastSeparator) {
return false
}
return IMAGE_DROP_EXTENSIONS.has(path.slice(lastDot).toLowerCase())
}
export function isTerminalDropWindowsPathLike(path: string): boolean {
if (isWslUncPath(path)) {
return false
}
return isWindowsAbsolutePathLike(path) || path.includes('\\')
}
export function canPasteImageDropPathRaw(path: string, targetShell: TerminalTargetShell): boolean {
if (hasControlByte(path)) {
return false
}
const unsafeRe =
targetShell === 'windows' ? WINDOWS_RAW_IMAGE_DROP_UNSAFE_RE : POSIX_RAW_IMAGE_DROP_UNSAFE_RE
return !unsafeRe.test(path)
}
/** Bracketed-paste text, or null when paste framing would rewrite control bytes and alter the path. */
export function formatImageDropPasteText(
path: string,
targetShell: TerminalTargetShell
): string | null {
if (hasControlByte(path)) {
return null
}
if (canPasteImageDropPathRaw(path, targetShell)) {
return path
}
// Claude strips only one quote pair, so POSIX splice quoting leaks into paths; backslash escapes round-trip across Claude/Codex/shells.
return targetShell === 'windows'
? shellEscapePath(path, 'windows')
: backslashEscapePosixPath(path)
}
function backslashEscapePosixPath(path: string): string {
return path.replace(/[^a-zA-Z0-9_./@:\-\u0080-\uffff]/g, '\\$&')
}
function hasControlByte(path: string): boolean {
for (let i = 0; i < path.length; i += 1) {
const code = path.charCodeAt(i)
if (code < 0x20 || code === 0x7f) {
return true
}
}
return false
}