/ utils / screenshotClipboard.ts
screenshotClipboard.ts
  1  import { mkdir, unlink, writeFile } from 'fs/promises'
  2  import { tmpdir } from 'os'
  3  import { join } from 'path'
  4  import { type AnsiToPngOptions, ansiToPng } from './ansiToPng.js'
  5  import { execFileNoThrowWithCwd } from './execFileNoThrow.js'
  6  import { logError } from './log.js'
  7  import { getPlatform } from './platform.js'
  8  
  9  /**
 10   * Copies an image (from ANSI text) to the system clipboard.
 11   * Supports macOS, Linux (with xclip/xsel), and Windows.
 12   *
 13   * Pure-TS pipeline: ANSI text → bitmap-font render → PNG encode. No WASM,
 14   * no system fonts, so this works in every build (native and JS).
 15   */
 16  export async function copyAnsiToClipboard(
 17    ansiText: string,
 18    options?: AnsiToPngOptions,
 19  ): Promise<{ success: boolean; message: string }> {
 20    try {
 21      const tempDir = join(tmpdir(), 'claude-code-screenshots')
 22      await mkdir(tempDir, { recursive: true })
 23  
 24      const pngPath = join(tempDir, `screenshot-${Date.now()}.png`)
 25      const pngBuffer = ansiToPng(ansiText, options)
 26      await writeFile(pngPath, pngBuffer)
 27  
 28      const result = await copyPngToClipboard(pngPath)
 29  
 30      try {
 31        await unlink(pngPath)
 32      } catch {
 33        // Ignore cleanup errors
 34      }
 35  
 36      return result
 37    } catch (error) {
 38      logError(error)
 39      return {
 40        success: false,
 41        message: `Failed to copy screenshot: ${error instanceof Error ? error.message : 'Unknown error'}`,
 42      }
 43    }
 44  }
 45  
 46  async function copyPngToClipboard(
 47    pngPath: string,
 48  ): Promise<{ success: boolean; message: string }> {
 49    const platform = getPlatform()
 50  
 51    if (platform === 'macos') {
 52      // macOS: Use osascript to copy PNG to clipboard
 53      // Escape backslashes and double quotes for AppleScript string
 54      const escapedPath = pngPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
 55      const script = `set the clipboard to (read (POSIX file "${escapedPath}") as «class PNGf»)`
 56      const result = await execFileNoThrowWithCwd('osascript', ['-e', script], {
 57        timeout: 5000,
 58      })
 59  
 60      if (result.code === 0) {
 61        return { success: true, message: 'Screenshot copied to clipboard' }
 62      }
 63      return {
 64        success: false,
 65        message: `Failed to copy to clipboard: ${result.stderr}`,
 66      }
 67    }
 68  
 69    if (platform === 'linux') {
 70      // Linux: Try xclip first, then xsel
 71      const xclipResult = await execFileNoThrowWithCwd(
 72        'xclip',
 73        ['-selection', 'clipboard', '-t', 'image/png', '-i', pngPath],
 74        { timeout: 5000 },
 75      )
 76  
 77      if (xclipResult.code === 0) {
 78        return { success: true, message: 'Screenshot copied to clipboard' }
 79      }
 80  
 81      // Try xsel as fallback
 82      const xselResult = await execFileNoThrowWithCwd(
 83        'xsel',
 84        ['--clipboard', '--input', '--type', 'image/png'],
 85        { timeout: 5000 },
 86      )
 87  
 88      if (xselResult.code === 0) {
 89        return { success: true, message: 'Screenshot copied to clipboard' }
 90      }
 91  
 92      return {
 93        success: false,
 94        message:
 95          'Failed to copy to clipboard. Please install xclip or xsel: sudo apt install xclip',
 96      }
 97    }
 98  
 99    if (platform === 'windows') {
100      // Windows: Use PowerShell to copy image to clipboard
101      const psScript = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Clipboard]::SetImage([System.Drawing.Image]::FromFile('${pngPath.replace(/'/g, "''")}'))`
102      const result = await execFileNoThrowWithCwd(
103        'powershell',
104        ['-NoProfile', '-Command', psScript],
105        { timeout: 5000 },
106      )
107  
108      if (result.code === 0) {
109        return { success: true, message: 'Screenshot copied to clipboard' }
110      }
111      return {
112        success: false,
113        message: `Failed to copy to clipboard: ${result.stderr}`,
114      }
115    }
116  
117    return {
118      success: false,
119      message: `Screenshot to clipboard is not supported on ${platform}`,
120    }
121  }