|
| 1 | +local context = require('opencode.context') |
| 2 | +local state = require('opencode.state') |
| 3 | + |
| 4 | +local M = {} |
| 5 | + |
| 6 | +--- Check if a file was successfully created and has content |
| 7 | +--- @param path string File path to check |
| 8 | +--- @return boolean success True if file exists and has content |
| 9 | +local function is_valid_file(path) |
| 10 | + return vim.fn.filereadable(path) == 1 and vim.fn.getfsize(path) > 0 |
| 11 | +end |
| 12 | + |
| 13 | +--- Try to extract image from macOS clipboard |
| 14 | +--- @param image_path string Path where to save the image |
| 15 | +--- @return boolean success True if image was extracted successfully |
| 16 | +local function try_macos_clipboard(image_path) |
| 17 | + if vim.fn.executable('osascript') ~= 1 then |
| 18 | + return false |
| 19 | + end |
| 20 | + |
| 21 | + local osascript_cmd = string.format( |
| 22 | + 'osascript -e \'set imageData to the clipboard as "PNGf"\' ' |
| 23 | + .. '-e \'set fileRef to open for access POSIX file "%s" with write permission\' ' |
| 24 | + .. "-e 'set eof fileRef to 0' " |
| 25 | + .. "-e 'write imageData to fileRef' " |
| 26 | + .. "-e 'close access fileRef'", |
| 27 | + image_path |
| 28 | + ) |
| 29 | + |
| 30 | + local result = vim.system({ 'sh', '-c', osascript_cmd }):wait() |
| 31 | + return result.code == 0 and is_valid_file(image_path) |
| 32 | +end |
| 33 | + |
| 34 | +--- Try to extract image from Linux clipboard (Wayland) |
| 35 | +--- @param image_path string Path where to save the image |
| 36 | +--- @return boolean success True if image was extracted successfully |
| 37 | +local function try_wayland_clipboard(image_path) |
| 38 | + if vim.fn.executable('wl-paste') ~= 1 then |
| 39 | + return false |
| 40 | + end |
| 41 | + |
| 42 | + local cmd = string.format('wl-paste -t image/png > "%s"', image_path) |
| 43 | + local result = vim.system({ 'sh', '-c', cmd }):wait() |
| 44 | + return result.code == 0 and is_valid_file(image_path) |
| 45 | +end |
| 46 | + |
| 47 | +--- Try to extract image from Linux clipboard (X11) |
| 48 | +--- @param image_path string Path where to save the image |
| 49 | +--- @return boolean success True if image was extracted successfully |
| 50 | +local function try_x11_clipboard(image_path) |
| 51 | + if vim.fn.executable('xclip') ~= 1 then |
| 52 | + return false |
| 53 | + end |
| 54 | + |
| 55 | + local cmd = string.format('xclip -selection clipboard -t image/png -o > "%s"', image_path) |
| 56 | + local result = vim.system({ 'sh', '-c', cmd }):wait() |
| 57 | + return result.code == 0 and is_valid_file(image_path) |
| 58 | +end |
| 59 | + |
| 60 | +--- Try to extract image from Windows/WSL clipboard |
| 61 | +--- @param image_path string Path where to save the image |
| 62 | +--- @return boolean success True if image was extracted successfully |
| 63 | +local function try_windows_clipboard(image_path) |
| 64 | + if vim.fn.executable('powershell.exe') ~= 1 then |
| 65 | + return false |
| 66 | + end |
| 67 | + |
| 68 | + local powershell_script = [[ |
| 69 | + Add-Type -AssemblyName System.Windows.Forms; |
| 70 | + $img = [System.Windows.Forms.Clipboard]::GetImage(); |
| 71 | + if ($img) { |
| 72 | + $ms = New-Object System.IO.MemoryStream; |
| 73 | + $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); |
| 74 | + [System.Convert]::ToBase64String($ms.ToArray()) |
| 75 | + } |
| 76 | + ]] |
| 77 | + |
| 78 | + local result = vim.system({ 'powershell.exe', '-command', powershell_script }):wait() |
| 79 | + if result.code ~= 0 or not result.stdout or result.stdout:gsub('%s', '') == '' then |
| 80 | + return false |
| 81 | + end |
| 82 | + |
| 83 | + local base64_data = result.stdout:gsub('%s', '') |
| 84 | + local decode_cmd = string.format('echo "%s" | base64 -d > "%s"', base64_data, image_path) |
| 85 | + local decode_result = vim.system({ 'sh', '-c', decode_cmd }):wait() |
| 86 | + return decode_result.code == 0 and is_valid_file(image_path) |
| 87 | +end |
| 88 | + |
| 89 | +--- Try to extract image from clipboard as base64 text |
| 90 | +--- @param temp_dir string Temporary directory |
| 91 | +--- @param timestamp string Timestamp for filename |
| 92 | +--- @return string|nil image_path Path to extracted image, or nil if failed |
| 93 | +local function try_base64_clipboard(temp_dir, timestamp) |
| 94 | + local clipboard_content = vim.fn.getreg('+') |
| 95 | + if not clipboard_content or not clipboard_content:match('^data:image/[^;]+;base64,') then |
| 96 | + return nil |
| 97 | + end |
| 98 | + |
| 99 | + local format, base64_data = clipboard_content:match('^data:image/([^;]+);base64,(.+)$') |
| 100 | + if not format or not base64_data then |
| 101 | + return nil |
| 102 | + end |
| 103 | + |
| 104 | + local image_path = temp_dir .. '/pasted_image_' .. timestamp .. '.' .. format |
| 105 | + local decode_cmd = string.format('echo "%s" | base64 -d > "%s"', base64_data, image_path) |
| 106 | + local result = vim.system({ 'sh', '-c', decode_cmd }):wait() |
| 107 | + |
| 108 | + if result.code == 0 and is_valid_file(image_path) then |
| 109 | + return image_path |
| 110 | + end |
| 111 | + return nil |
| 112 | +end |
| 113 | + |
| 114 | +--- Get error message for missing clipboard tools |
| 115 | +--- @param os_name string Operating system name |
| 116 | +--- @return string error_message Error message with installation instructions |
| 117 | +local function get_clipboard_error_message(os_name) |
| 118 | + local install_msg = 'No image found in clipboard. Install clipboard tools: ' |
| 119 | + if os_name == 'Linux' then |
| 120 | + return install_msg .. 'xclip (X11) or wl-clipboard (Wayland)' |
| 121 | + elseif os_name == 'Darwin' then |
| 122 | + return install_msg .. 'system clipboard should work natively' |
| 123 | + else |
| 124 | + return install_msg .. 'PowerShell (Windows/WSL)' |
| 125 | + end |
| 126 | +end |
| 127 | + |
| 128 | +--- Handle clipboard image data by saving it to a file and adding it to context |
| 129 | +--- @return boolean success True if image was successfully handled |
| 130 | +function M.paste_image_from_clipboard() |
| 131 | + local temp_dir = vim.fn.tempname() |
| 132 | + vim.fn.mkdir(temp_dir, 'p') |
| 133 | + local timestamp = os.date('%Y%m%d_%H%M%S') |
| 134 | + local image_path = temp_dir .. '/pasted_image_' .. timestamp .. '.png' |
| 135 | + |
| 136 | + local os_name = vim.uv.os_uname().sysname |
| 137 | + local success = false |
| 138 | + |
| 139 | + if os_name == 'Darwin' then |
| 140 | + success = try_macos_clipboard(image_path) |
| 141 | + elseif os_name == 'Windows_NT' or vim.fn.exists('$WSL_DISTRO_NAME') == 1 then |
| 142 | + success = try_windows_clipboard(image_path) |
| 143 | + elseif os_name == 'Linux' then |
| 144 | + success = try_wayland_clipboard(image_path) or try_x11_clipboard(image_path) |
| 145 | + end |
| 146 | + |
| 147 | + if not success then |
| 148 | + local fallback_path = try_base64_clipboard(temp_dir, timestamp) |
| 149 | + if fallback_path then |
| 150 | + image_path = fallback_path |
| 151 | + success = true |
| 152 | + end |
| 153 | + end |
| 154 | + |
| 155 | + if not success then |
| 156 | + vim.notify(get_clipboard_error_message(os_name), vim.log.levels.WARN) |
| 157 | + return false |
| 158 | + end |
| 159 | + |
| 160 | + context.add_file(image_path) |
| 161 | + state.context_updated_at = os.time() |
| 162 | + |
| 163 | + local filename = vim.fn.fnamemodify(image_path, ':t') |
| 164 | + vim.notify(string.format('Image saved and added to context: %s', filename), vim.log.levels.INFO) |
| 165 | + |
| 166 | + return true |
| 167 | +end |
| 168 | + |
| 169 | +return M |
0 commit comments