summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/utils
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/utils')
-rw-r--r--packages/cli/src/ui/utils/clipboardUtils.test.ts76
-rw-r--r--packages/cli/src/ui/utils/clipboardUtils.ts149
2 files changed, 225 insertions, 0 deletions
diff --git a/packages/cli/src/ui/utils/clipboardUtils.test.ts b/packages/cli/src/ui/utils/clipboardUtils.test.ts
new file mode 100644
index 00000000..30258889
--- /dev/null
+++ b/packages/cli/src/ui/utils/clipboardUtils.test.ts
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ clipboardHasImage,
+ saveClipboardImage,
+ cleanupOldClipboardImages,
+} from './clipboardUtils.js';
+
+describe('clipboardUtils', () => {
+ describe('clipboardHasImage', () => {
+ it('should return false on non-macOS platforms', async () => {
+ if (process.platform !== 'darwin') {
+ const result = await clipboardHasImage();
+ expect(result).toBe(false);
+ } else {
+ // Skip on macOS as it would require actual clipboard state
+ expect(true).toBe(true);
+ }
+ });
+
+ it('should return boolean on macOS', async () => {
+ if (process.platform === 'darwin') {
+ const result = await clipboardHasImage();
+ expect(typeof result).toBe('boolean');
+ } else {
+ // Skip on non-macOS
+ expect(true).toBe(true);
+ }
+ });
+ });
+
+ describe('saveClipboardImage', () => {
+ it('should return null on non-macOS platforms', async () => {
+ if (process.platform !== 'darwin') {
+ const result = await saveClipboardImage();
+ expect(result).toBe(null);
+ } else {
+ // Skip on macOS
+ expect(true).toBe(true);
+ }
+ });
+
+ it('should handle errors gracefully', async () => {
+ // Test with invalid directory (should not throw)
+ const result = await saveClipboardImage(
+ '/invalid/path/that/does/not/exist',
+ );
+
+ if (process.platform === 'darwin') {
+ // On macOS, might return null due to various errors
+ expect(result === null || typeof result === 'string').toBe(true);
+ } else {
+ // On other platforms, should always return null
+ expect(result).toBe(null);
+ }
+ });
+ });
+
+ describe('cleanupOldClipboardImages', () => {
+ it('should not throw errors', async () => {
+ // Should handle missing directories gracefully
+ await expect(
+ cleanupOldClipboardImages('/path/that/does/not/exist'),
+ ).resolves.not.toThrow();
+ });
+
+ it('should complete without errors on valid directory', async () => {
+ await expect(cleanupOldClipboardImages('.')).resolves.not.toThrow();
+ });
+ });
+});
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
+ }
+}