mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
fix paste image on windows
This commit is contained in:
parent
b28e5c4c0f
commit
1050163804
9 changed files with 169 additions and 402 deletions
|
|
@ -40,6 +40,7 @@
|
|||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@qwen-code/qwen-code-core": "file:../core",
|
||||
"@teddyzhu/clipboard": "^0.0.5",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"ansi-regex": "^6.2.2",
|
||||
"command-exists": "^1.2.9",
|
||||
|
|
@ -80,12 +81,12 @@
|
|||
"@types/diff": "^7.0.2",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"archiver": "^7.0.1",
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export interface KeyBinding {
|
|||
command?: boolean;
|
||||
/** Paste operation requirement: true=must be paste, false=must not be paste, undefined=ignore */
|
||||
paste?: boolean;
|
||||
meta?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -153,10 +154,16 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
|||
{ key: 'x', ctrl: true },
|
||||
{ sequence: '\x18', ctrl: true },
|
||||
],
|
||||
[Command.PASTE_CLIPBOARD_IMAGE]: [
|
||||
{ key: 'v', ctrl: true },
|
||||
{ key: 'v', command: true },
|
||||
],
|
||||
[Command.PASTE_CLIPBOARD_IMAGE]:
|
||||
process.platform === 'win32'
|
||||
? [
|
||||
{ key: 'v', command: true },
|
||||
{ key: 'v', meta: true },
|
||||
]
|
||||
: [
|
||||
{ key: 'v', ctrl: true },
|
||||
{ key: 'v', command: true },
|
||||
],
|
||||
|
||||
// App level bindings
|
||||
[Command.SHOW_ERROR_DETAILS]: [{ key: 'o', ctrl: true }],
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ interface Shortcut {
|
|||
// Platform-specific key mappings
|
||||
const getNewlineKey = () =>
|
||||
process.platform === 'win32' ? 'ctrl+enter' : 'ctrl+j';
|
||||
const getPasteKey = () => (process.platform === 'darwin' ? 'cmd+v' : 'ctrl+v');
|
||||
const getPasteKey = () => {
|
||||
if (process.platform === 'win32') return 'alt+v';
|
||||
return process.platform === 'darwin' ? 'cmd+v' : 'ctrl+v';
|
||||
};
|
||||
const getExternalEditorKey = () =>
|
||||
process.platform === 'darwin' ? 'ctrl+x' : 'ctrl+x';
|
||||
|
||||
|
|
|
|||
|
|
@ -735,31 +735,6 @@ export function KeypressProvider({
|
|||
};
|
||||
|
||||
let rl: readline.Interface;
|
||||
let stdinRl: readline.Interface | null = null;
|
||||
|
||||
// On Windows, when pasting an image (not text), the terminal may not send
|
||||
// any data to stdin, so the 'data' event won't fire. We need to also
|
||||
// listen for keypress events directly on stdin to capture Ctrl+V.
|
||||
// This handler only processes Ctrl+V to avoid duplicate events for other keys.
|
||||
const handleStdinKeypress = async (_: unknown, key: Key) => {
|
||||
// Only handle Ctrl+V (sequence '\x16') that might not come through data event
|
||||
// Other keys will come through the data -> keypressStream -> keypress path
|
||||
if (key && key.sequence === '\x16') {
|
||||
// Check if this is a potential image paste by checking clipboard
|
||||
const hasImage = await clipboardHasImage();
|
||||
if (hasImage) {
|
||||
broadcast({
|
||||
name: '',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: true,
|
||||
pasteImage: true,
|
||||
sequence: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (usePassthrough) {
|
||||
rl = readline.createInterface({
|
||||
|
|
@ -769,14 +744,6 @@ export function KeypressProvider({
|
|||
readline.emitKeypressEvents(keypressStream, rl);
|
||||
keypressStream.on('keypress', handleKeypress);
|
||||
stdin.on('data', handleRawKeypress);
|
||||
|
||||
// Also listen for keypress on stdin to capture Ctrl+V for image paste
|
||||
stdinRl = readline.createInterface({
|
||||
input: stdin,
|
||||
escapeCodeTimeout: 0,
|
||||
});
|
||||
readline.emitKeypressEvents(stdin, stdinRl);
|
||||
stdin.on('keypress', handleStdinKeypress);
|
||||
} else {
|
||||
rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 0 });
|
||||
readline.emitKeypressEvents(stdin, rl);
|
||||
|
|
@ -787,8 +754,6 @@ export function KeypressProvider({
|
|||
if (usePassthrough) {
|
||||
keypressStream.removeListener('keypress', handleKeypress);
|
||||
stdin.removeListener('data', handleRawKeypress);
|
||||
stdin.removeListener('keypress', handleStdinKeypress);
|
||||
stdinRl?.close();
|
||||
} else {
|
||||
stdin.removeListener('keypress', handleKeypress);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { defaultKeyBindings } from '../config/keyBindings.js';
|
|||
import type { Key } from './hooks/useKeypress.js';
|
||||
|
||||
describe('keyMatchers', () => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const createKey = (name: string, mods: Partial<Key> = {}): Key => ({
|
||||
name,
|
||||
ctrl: false,
|
||||
|
|
@ -50,7 +51,7 @@ describe('keyMatchers', () => {
|
|||
[Command.OPEN_EXTERNAL_EDITOR]: (key: Key) =>
|
||||
key.ctrl && (key.name === 'x' || key.sequence === '\x18'),
|
||||
[Command.PASTE_CLIPBOARD_IMAGE]: (key: Key) =>
|
||||
(key.ctrl || key.meta) && key.name === 'v',
|
||||
(isWindows ? key.meta : key.ctrl || key.meta) && key.name === 'v',
|
||||
[Command.SHOW_ERROR_DETAILS]: (key: Key) => key.ctrl && key.name === 'o',
|
||||
[Command.TOGGLE_TOOL_DESCRIPTIONS]: (key: Key) =>
|
||||
key.ctrl && key.name === 't',
|
||||
|
|
@ -218,11 +219,12 @@ describe('keyMatchers', () => {
|
|||
},
|
||||
{
|
||||
command: Command.PASTE_CLIPBOARD_IMAGE,
|
||||
positive: [
|
||||
createKey('v', { ctrl: true }),
|
||||
createKey('v', { meta: true }),
|
||||
],
|
||||
negative: [createKey('v'), createKey('c', { ctrl: true })],
|
||||
positive: isWindows
|
||||
? [createKey('v', { meta: true })]
|
||||
: [createKey('v', { ctrl: true }), createKey('v', { meta: true })],
|
||||
negative: isWindows
|
||||
? [createKey('v', { ctrl: true }), createKey('v')]
|
||||
: [createKey('v'), createKey('c', { ctrl: true })],
|
||||
},
|
||||
|
||||
// App level bindings
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ describe('clipboardUtils', () => {
|
|||
|
||||
it('should return true when clipboard contains image', async () => {
|
||||
mockExecCommand.mockResolvedValue({
|
||||
stdout: 'True',
|
||||
stdout: 'true',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
|
|
@ -132,17 +132,17 @@ describe('clipboardUtils', () => {
|
|||
const result = await clipboardHasImage();
|
||||
expect(result).toBe(true);
|
||||
expect(mockExecCommand).toHaveBeenCalledWith(
|
||||
'powershell',
|
||||
'powershell.exe',
|
||||
expect.arrayContaining([
|
||||
'-command',
|
||||
'Add-Type -Assembly System.Windows.Forms; [System.Windows.Forms.Clipboard]::ContainsImage()',
|
||||
expect.stringContaining('Get-Clipboard'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when clipboard does not contain image', async () => {
|
||||
mockExecCommand.mockResolvedValue({
|
||||
stdout: 'False',
|
||||
stdout: 'false',
|
||||
stderr: '',
|
||||
code: 0,
|
||||
});
|
||||
|
|
@ -151,11 +151,15 @@ describe('clipboardUtils', () => {
|
|||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when PowerShell fails', async () => {
|
||||
mockExecCommand.mockRejectedValue(new Error('PowerShell not found'));
|
||||
it('should return false when all PowerShell hosts fail', async () => {
|
||||
mockExecCommand
|
||||
.mockRejectedValueOnce(new Error('PowerShell not found'))
|
||||
.mockRejectedValueOnce(new Error('PowerShell not found'))
|
||||
.mockRejectedValueOnce(new Error('PowerShell not found'));
|
||||
|
||||
const result = await clipboardHasImage();
|
||||
expect(result).toBe(false);
|
||||
expect(mockExecCommand).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@
|
|||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { execCommand } from '@qwen-code/qwen-code-core';
|
||||
|
||||
const MACOS_CLIPBOARD_TIMEOUT_MS = 1500;
|
||||
import { ClipboardManager } from '@teddyzhu/clipboard';
|
||||
|
||||
/**
|
||||
* Checks if the system clipboard contains an image
|
||||
|
|
@ -16,100 +14,9 @@ const MACOS_CLIPBOARD_TIMEOUT_MS = 1500;
|
|||
*/
|
||||
export async function clipboardHasImage(): Promise<boolean> {
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
// Use osascript to check clipboard type
|
||||
const { stdout } = await execCommand(
|
||||
'osascript',
|
||||
['-e', 'clipboard info'],
|
||||
{
|
||||
timeout: MACOS_CLIPBOARD_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
// Support common image formats: PNG, JPEG, TIFF, GIF, WebP, BMP, HEIC/HEIF
|
||||
const imageRegex =
|
||||
/«class PNGf»|«class JPEG»|«class JPEGffffff»|«class TIFF»|«class GIFf»|«class WEBP»|«class BMPf»|«class heic»|«class heif»|TIFF picture|JPEG picture|GIF picture|PNG picture|public.heic|public.heif/;
|
||||
return imageRegex.test(stdout);
|
||||
} else if (process.platform === 'win32') {
|
||||
// On Windows, use System.Windows.Forms.Clipboard (more reliable than PresentationCore)
|
||||
try {
|
||||
const { stdout } = await execCommand('powershell', [
|
||||
'-noprofile',
|
||||
'-noninteractive',
|
||||
'-nologo',
|
||||
'-sta',
|
||||
'-executionpolicy',
|
||||
'unrestricted',
|
||||
'-windowstyle',
|
||||
'hidden',
|
||||
'-command',
|
||||
'Add-Type -Assembly System.Windows.Forms; [System.Windows.Forms.Clipboard]::ContainsImage()',
|
||||
]);
|
||||
return stdout.trim() === 'True';
|
||||
} catch {
|
||||
// If PowerShell or .NET Forms is not available, return false
|
||||
return false;
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
// On Linux, check if xclip or wl-clipboard is available and has image data
|
||||
try {
|
||||
// Try xclip first (X11) - check for multiple image formats
|
||||
await execCommand('which', ['xclip']);
|
||||
const imageFormats = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/bmp',
|
||||
'image/webp',
|
||||
'image/tiff',
|
||||
];
|
||||
for (const format of imageFormats) {
|
||||
try {
|
||||
const { stdout: xclipOut } = await execCommand('xclip', [
|
||||
'-selection',
|
||||
'clipboard',
|
||||
'-t',
|
||||
format,
|
||||
'-o',
|
||||
]);
|
||||
if (xclipOut.length > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// This format is not available, try next
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
try {
|
||||
// Try xsel as fallback (X11) - check TARGETS to see if image data exists
|
||||
await execCommand('which', ['xsel']);
|
||||
try {
|
||||
// Check available clipboard targets
|
||||
const { stdout: targets } = await execCommand('xsel', ['-b', '-t']);
|
||||
// Check if any image MIME type is in the targets
|
||||
return /image\/(png|jpeg|jpg|gif|bmp|webp|tiff)/i.test(targets);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
// Try wl-clipboard as fallback (Wayland)
|
||||
await execCommand('which', ['wl-paste']);
|
||||
const { stdout: wlOut } = await execCommand('wl-paste', [
|
||||
'--list-types',
|
||||
]);
|
||||
// Check for image MIME types (must start with image/)
|
||||
return /^image\//m.test(wlOut);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
const clipboard = new ClipboardManager();
|
||||
return clipboard.hasFormat('image');
|
||||
} catch (error) {
|
||||
// Log error for debugging but don't throw
|
||||
if (process.env['DEBUG']) {
|
||||
console.error('Error checking clipboard for image:', error);
|
||||
}
|
||||
|
|
@ -126,6 +33,12 @@ export async function saveClipboardImage(
|
|||
targetDir?: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const clipboard = new ClipboardManager();
|
||||
|
||||
if (!clipboard.hasFormat('image')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a temporary directory for clipboard images within the target directory
|
||||
// This avoids security restrictions on paths outside the target directory
|
||||
const baseDir = targetDir || process.cwd();
|
||||
|
|
@ -134,16 +47,19 @@ export async function saveClipboardImage(
|
|||
|
||||
// Generate a unique filename with timestamp
|
||||
const timestamp = new Date().getTime();
|
||||
const tempFilePath = path.join(tempDir, `clipboard-${timestamp}.png`);
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
return await saveMacOSClipboardImage(tempDir, timestamp);
|
||||
} else if (process.platform === 'win32') {
|
||||
return await saveWindowsClipboardImage(tempDir, timestamp);
|
||||
} else if (process.platform === 'linux') {
|
||||
return await saveLinuxClipboardImage(tempDir, timestamp);
|
||||
const imageData = clipboard.getImageData();
|
||||
// Use data buffer from the API
|
||||
const buffer = imageData.data;
|
||||
|
||||
if (!buffer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
await fs.writeFile(tempFilePath, buffer);
|
||||
|
||||
return tempFilePath;
|
||||
} catch (error) {
|
||||
if (process.env['DEBUG']) {
|
||||
console.error('Error saving clipboard image:', error);
|
||||
|
|
@ -152,251 +68,6 @@ export async function saveClipboardImage(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves clipboard image on macOS using osascript
|
||||
*/
|
||||
async function saveMacOSClipboardImage(
|
||||
tempDir: string,
|
||||
timestamp: number,
|
||||
): Promise<string | null> {
|
||||
// Try different image formats in order of preference
|
||||
const formats = [
|
||||
{ class: 'PNGf', extension: 'png' },
|
||||
{ class: 'JPEG', extension: 'jpg' },
|
||||
{ class: 'WEBP', extension: 'webp' },
|
||||
{ class: 'heic', extension: 'heic' },
|
||||
{ class: 'heif', extension: 'heif' },
|
||||
{ class: 'TIFF', extension: 'tiff' },
|
||||
{ class: 'GIFf', extension: 'gif' },
|
||||
{ class: 'BMPf', extension: 'bmp' },
|
||||
];
|
||||
|
||||
for (const format of formats) {
|
||||
const tempFilePath = path.join(
|
||||
tempDir,
|
||||
`clipboard-${timestamp}.${format.extension}`,
|
||||
);
|
||||
|
||||
// Try to save clipboard as this format
|
||||
const script = `
|
||||
try
|
||||
set imageData to the clipboard as «class ${format.class}»
|
||||
set fileRef to open for access POSIX file "${tempFilePath}" with write permission
|
||||
write imageData to fileRef
|
||||
close access fileRef
|
||||
return "success"
|
||||
on error errMsg
|
||||
try
|
||||
close access POSIX file "${tempFilePath}"
|
||||
end try
|
||||
return "error"
|
||||
end try
|
||||
`;
|
||||
|
||||
try {
|
||||
const { stdout } = await execCommand('osascript', ['-e', script], {
|
||||
timeout: MACOS_CLIPBOARD_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
if (stdout.trim() === 'success') {
|
||||
// Verify the file was created and has content
|
||||
try {
|
||||
const stats = await fs.stat(tempFilePath);
|
||||
if (stats.size > 0) {
|
||||
return tempFilePath;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist, continue to next format
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// This format failed, try next
|
||||
}
|
||||
|
||||
// Clean up failed attempt
|
||||
try {
|
||||
await fs.unlink(tempFilePath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves clipboard image on Windows using PowerShell
|
||||
*/
|
||||
async function saveWindowsClipboardImage(
|
||||
tempDir: string,
|
||||
timestamp: number,
|
||||
): Promise<string | null> {
|
||||
const tempFilePath = path.join(tempDir, `clipboard-${timestamp}.png`);
|
||||
|
||||
try {
|
||||
// Use PowerShell to save clipboard image as PNG
|
||||
const script = `
|
||||
Add-Type -Assembly System.Windows.Forms
|
||||
Add-Type -Assembly System.Drawing
|
||||
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
||||
if ($img -ne $null) {
|
||||
$img.Save('${tempFilePath.replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
Write-Output 'success'
|
||||
} else {
|
||||
Write-Output 'no-image'
|
||||
}
|
||||
`;
|
||||
|
||||
const { stdout } = await execCommand('powershell', [
|
||||
'-noprofile',
|
||||
'-noninteractive',
|
||||
'-nologo',
|
||||
'-sta',
|
||||
'-executionpolicy',
|
||||
'unrestricted',
|
||||
'-windowstyle',
|
||||
'hidden',
|
||||
'-command',
|
||||
script,
|
||||
]);
|
||||
|
||||
if (stdout.trim() === 'success') {
|
||||
// Verify the file was created and has content
|
||||
try {
|
||||
const stats = await fs.stat(tempFilePath);
|
||||
if (stats.size > 0) {
|
||||
return tempFilePath;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up failed attempt
|
||||
try {
|
||||
await fs.unlink(tempFilePath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
} catch (error) {
|
||||
// PowerShell failed, log in DEBUG mode and re-throw
|
||||
if (process.env['DEBUG']) {
|
||||
console.error('Error in saveWindowsClipboardImage:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves clipboard image on Linux using xclip or wl-paste
|
||||
*/
|
||||
async function saveLinuxClipboardImage(
|
||||
tempDir: string,
|
||||
timestamp: number,
|
||||
): Promise<string | null> {
|
||||
// Try xclip first (X11)
|
||||
try {
|
||||
await execCommand('which', ['xclip']);
|
||||
|
||||
// Try different image formats
|
||||
const formats = [
|
||||
{ mime: 'image/png', extension: 'png' },
|
||||
{ mime: 'image/jpeg', extension: 'jpg' },
|
||||
{ mime: 'image/gif', extension: 'gif' },
|
||||
{ mime: 'image/bmp', extension: 'bmp' },
|
||||
{ mime: 'image/webp', extension: 'webp' },
|
||||
{ mime: 'image/tiff', extension: 'tiff' },
|
||||
];
|
||||
|
||||
for (const format of formats) {
|
||||
const tempFilePath = path.join(
|
||||
tempDir,
|
||||
`clipboard-${timestamp}.${format.extension}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Use shell redirection to save binary data
|
||||
await execCommand('sh', [
|
||||
'-c',
|
||||
`xclip -selection clipboard -t ${format.mime} -o > "${tempFilePath}"`,
|
||||
]);
|
||||
|
||||
// Verify the file was created and has content
|
||||
try {
|
||||
const stats = await fs.stat(tempFilePath);
|
||||
if (stats.size > 0) {
|
||||
return tempFilePath;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or is empty
|
||||
}
|
||||
|
||||
// Clean up empty file
|
||||
try {
|
||||
await fs.unlink(tempFilePath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
} catch {
|
||||
// This format not available, try next
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// xclip not available, try wl-paste (Wayland)
|
||||
try {
|
||||
await execCommand('which', ['wl-paste']);
|
||||
|
||||
// Get list of available types
|
||||
const { stdout: types } = await execCommand('wl-paste', ['--list-types']);
|
||||
|
||||
// Find first image type
|
||||
const imageTypeMatch = types.match(/^(image\/\w+)$/m);
|
||||
if (imageTypeMatch) {
|
||||
const mimeType = imageTypeMatch[1];
|
||||
const extension = mimeType.split('/')[1] || 'png';
|
||||
const tempFilePath = path.join(
|
||||
tempDir,
|
||||
`clipboard-${timestamp}.${extension}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Use shell redirection to save binary data
|
||||
await execCommand('sh', [
|
||||
'-c',
|
||||
`wl-paste --type ${mimeType} > "${tempFilePath}"`,
|
||||
]);
|
||||
|
||||
// Verify the file was created and has content
|
||||
try {
|
||||
const stats = await fs.stat(tempFilePath);
|
||||
if (stats.size > 0) {
|
||||
return tempFilePath;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or is empty
|
||||
}
|
||||
|
||||
// Clean up empty file
|
||||
try {
|
||||
await fs.unlink(tempFilePath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
} catch {
|
||||
// Failed to save image
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// wl-paste not available
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up old temporary clipboard image files using LRU strategy
|
||||
* Keeps maximum 100 images, when exceeding removes 50 oldest files to reduce cleanup frequency
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue