1+ --- Image pasting functionality from clipboard
2+ --- @see https ://github.com /sst /opencode /blob /45180104fe84e2d0b9d29be0f9f8a5e52d18e102/packages /opencode /src /cli /cmd /tui /util /clipboard.ts
13local context = require (' opencode.context' )
24local state = require (' opencode.state' )
35
46local M = {}
57
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
8+ --- Check if a file exists and has content
9+ --- @param path string
10+ --- @return boolean
911local function is_valid_file (path )
10- return vim .fn .filereadable ( path ) == 1 and vim . fn . getfsize (path ) > 0
12+ return vim .fn .getfsize (path ) > 0
1113end
1214
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 )
15+ --- Run shell command and return success
16+ --- @param cmd string
17+ --- @return boolean
18+ local function run_shell_cmd (cmd )
19+ return vim .system ({ ' sh' , ' -c' , cmd }):wait ().code == 0
3220end
3321
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
22+ --- Save base64 data to file
23+ --- @param data string
24+ --- @param path string
25+ --- @return boolean
26+ local function save_base64 (data , path )
27+ if vim .fn .has (' win32' ) == 1 then
28+ local script =
29+ string.format (' [System.IO.File]::WriteAllBytes("%s", [System.Convert]::FromBase64String("%s"))' , path , data )
30+ return vim .system ({ ' powershell.exe' , ' -command' , ' -' }, { stdin = script }):wait ().code == 0
31+ else
32+ local decode_arg = vim .uv .os_uname ().sysname == ' Darwin' and ' -D' or ' -d'
33+ return vim .system ({ ' sh' , ' -c' , string.format (' base64 %s > "%s"' , decode_arg , path ) }, { stdin = data }):wait ().code
34+ == 0
4035 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 )
4536end
4637
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
38+ --- macOS clipboard image handler using osascript
39+ --- @param path string
40+ --- @return boolean
41+ local function handle_darwin_clipboard ( path )
42+ if vim .fn .executable (' osascript ' ) ~= 1 then
5243 return false
5344 end
45+ local cmd = string.format (
46+ " osascript -e 'set imageData to the clipboard as \" PNGf\" ' -e 'set fileRef to open for access POSIX file \" %s\" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'" ,
47+ path
48+ )
49+ return run_shell_cmd (cmd )
50+ end
5451
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 )
52+ --- Linux clipboard image handler supporting Wayland and X11
53+ --- @param path string
54+ --- @return boolean
55+ local function handle_linux_clipboard (path )
56+ if vim .fn .executable (' wl-paste' ) == 1 and run_shell_cmd (string.format (' wl-paste -t image/png > "%s"' , path )) then
57+ return true
58+ end
59+ return vim .fn .executable (' xclip' ) == 1
60+ and run_shell_cmd (string.format (' xclip -selection clipboard -t image/png -o > "%s"' , path ))
5861end
5962
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 )
63+ --- Windows clipboard image handler using PowerShell
64+ --- @param path string
65+ --- @return boolean
66+ local function handle_windows_clipboard ( path )
6467 if vim .fn .executable (' powershell.exe' ) ~= 1 then
6568 return false
6669 end
70+ local script = string.format (
71+ [[
72+ Add-Type -AssemblyName System.Windows.Forms;
73+ $img = [System.Windows.Forms.Clipboard]::GetImage();
74+ if ($img) {
75+ $img.Save('%s', [System.Drawing.Imaging.ImageFormat]::Png);
76+ } else {
77+ exit 1
78+ }
79+ ]] ,
80+ path
81+ )
82+ return vim .system ({ ' powershell.exe' , ' -command' , ' -' }, { stdin = script }):wait ().code == 0
83+ end
6784
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
85+ local handlers = {
86+ Darwin = handle_darwin_clipboard ,
87+ Linux = handle_linux_clipboard ,
88+ Windows_NT = handle_windows_clipboard ,
89+ }
90+
91+ --- Try to get image from system clipboard
92+ --- @param image_path string
93+ --- @return boolean
94+ local function try_system_clipboard (image_path )
95+ local os_name = vim .uv .os_uname ().sysname
96+ local handler = handlers [os_name ]
97+
98+ -- WSL detection and override
99+ if vim .fn .exists (' $WSL_DISTRO_NAME' ) == 1 then
100+ handler = handlers .Windows_NT
81101 end
82102
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 )
103+ return handler and handler (image_path ) and is_valid_file (image_path ) or false
87104end
88105
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
106+ --- Try to parse base64 image data from clipboard
107+ --- @param temp_dir string
108+ --- @param timestamp string
109+ --- @return boolean , string ?
93110local 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
111+ local content = vim .fn .getreg (' +' )
112+ if not content or content == ' ' then
113+ return false
97114 end
98115
99- local format , base64_data = clipboard_content :match (' ^data:image/([^;]+);base64,(.+)$' )
100- if not format or not base64_data then
101- return nil
116+ local format , data = content :match (' ^data:image/([^;]+);base64,(.+)$' )
117+ if not format or not data then
118+ return false
102119 end
103120
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
121+ local image_path = string.format (' %s/pasted_image_%s.%s' , temp_dir , timestamp , format )
122+ local success = save_base64 (data , image_path ) and is_valid_file (image_path )
113123
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
124+ return success , success and image_path or nil
126125end
127126
128127--- Handle clipboard image data by saving it to a file and adding it to context
@@ -131,39 +130,27 @@ function M.paste_image_from_clipboard()
131130 local temp_dir = vim .fn .tempname ()
132131 vim .fn .mkdir (temp_dir , ' p' )
133132 local timestamp = os.date (' %Y%m%d_%H%M%S' )
134- local image_path = temp_dir .. ' /pasted_image_' .. timestamp .. ' .png '
133+ local image_path = string.format ( ' %s /pasted_image_%s.png ' , temp_dir , timestamp )
135134
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
135+ local success = try_system_clipboard (image_path )
146136
147137 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
138+ local base64_success , base64_path = try_base64_clipboard (temp_dir , timestamp --[[ @as string ]] )
139+ success = base64_success
140+ if base64_path then
141+ image_path = base64_path
152142 end
153143 end
154144
155- if not success then
156- vim .notify (get_clipboard_error_message (os_name ), vim .log .levels .WARN )
157- return false
145+ if success then
146+ context .add_file (image_path )
147+ state .context_updated_at = os.time ()
148+ vim .notify (' Image saved and added to context: ' .. vim .fn .fnamemodify (image_path , ' :t' ), vim .log .levels .INFO )
149+ return true
158150 end
159151
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
152+ vim .notify (' No image found in clipboard.' , vim .log .levels .WARN )
153+ return false
167154end
168155
169156return M
0 commit comments