summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/utils/clipboardUtils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/utils/clipboardUtils.ts')
-rw-r--r--packages/cli/src/ui/utils/clipboardUtils.ts149
1 files changed, 149 insertions, 0 deletions
diff --git a/packages/cli/src/ui/utils/clipboardUtils.ts b/packages/cli/src/ui/utils/clipboardUtils.ts
new file mode 100644
index 00000000..74554495
--- /dev/null
+++ b/packages/cli/src/ui/utils/clipboardUtils.ts
@@ -0,0 +1,149 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+
+const execAsync = promisify(exec);
+
+/**
+ * Checks if the system clipboard contains an image (macOS only for now)
+ * @returns true if clipboard contains an image
+ */
+export async function clipboardHasImage(): Promise<boolean> {
+ if (process.platform !== 'darwin') {
+ return false;
+ }
+
+ try {
+ // Use osascript to check clipboard type
+ const { stdout } = await execAsync(
+ `osascript -e 'clipboard info' 2>/dev/null | grep -qE "«class PNGf»|TIFF picture|JPEG picture|GIF picture|«class JPEG»|«class TIFF»" && echo "true" || echo "false"`,
+ { shell: '/bin/bash' },
+ );
+ return stdout.trim() === 'true';
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Saves the image from clipboard to a temporary file (macOS only for now)
+ * @param targetDir The target directory to create temp files within
+ * @returns The path to the saved image file, or null if no image or error
+ */
+export async function saveClipboardImage(
+ targetDir?: string,
+): Promise<string | null> {
+ if (process.platform !== 'darwin') {
+ return null;
+ }
+
+ try {
+ // 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();
+ const tempDir = path.join(baseDir, '.gemini-clipboard');
+ await fs.mkdir(tempDir, { recursive: true });
+
+ // Generate a unique filename with timestamp
+ const timestamp = new Date().getTime();
+
+ // Try different image formats in order of preference
+ const formats = [
+ { class: 'PNGf', extension: 'png' },
+ { class: 'JPEG', extension: 'jpg' },
+ { class: 'TIFF', extension: 'tiff' },
+ { class: 'GIFf', extension: 'gif' },
+ ];
+
+ 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
+ `;
+
+ const { stdout } = await execAsync(`osascript -e '${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, continue to next format
+ }
+ }
+
+ // Clean up failed attempt
+ try {
+ await fs.unlink(tempFilePath);
+ } catch {
+ // Ignore cleanup errors
+ }
+ }
+
+ // No format worked
+ return null;
+ } catch (error) {
+ console.error('Error saving clipboard image:', error);
+ return null;
+ }
+}
+
+/**
+ * Cleans up old temporary clipboard image files
+ * Removes files older than 1 hour
+ * @param targetDir The target directory where temp files are stored
+ */
+export async function cleanupOldClipboardImages(
+ targetDir?: string,
+): Promise<void> {
+ try {
+ const baseDir = targetDir || process.cwd();
+ const tempDir = path.join(baseDir, '.gemini-clipboard');
+ const files = await fs.readdir(tempDir);
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
+
+ for (const file of files) {
+ if (
+ file.startsWith('clipboard-') &&
+ (file.endsWith('.png') ||
+ file.endsWith('.jpg') ||
+ file.endsWith('.tiff') ||
+ file.endsWith('.gif'))
+ ) {
+ const filePath = path.join(tempDir, file);
+ const stats = await fs.stat(filePath);
+ if (stats.mtimeMs < oneHourAgo) {
+ await fs.unlink(filePath);
+ }
+ }
+ }
+ } catch {
+ // Ignore errors in cleanup
+ }
+}