From 13cf9a465333a3565f22cd5b5f0555e7b43ba567 Mon Sep 17 00:00:00 2001 From: Chen Mulong Date: Tue, 4 Aug 2026 11:19:05 +0800 Subject: [PATCH] Add win support --- .gitattributes | 1 + .gitignore | 1 + README.md | 8 ++-- bin/install.ps1 | 29 +++++++++++++ bin/install.sh | 21 +++++++++- lua/richclip/api.lua | 8 +++- lua/richclip/binary.lua | 83 ++++++++++++++++++++++++++------------ lua/richclip/init.lua | 9 +---- tests/spec/binary_spec.lua | 2 +- 9 files changed, 121 insertions(+), 41 deletions(-) create mode 100644 .gitattributes create mode 100644 bin/install.ps1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index b7e1c48..1168974 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ tests/vendor/* bin/richclip +bin/richclip.exe diff --git a/README.md b/README.md index a0b6bb0..294257c 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ with extra features: ## Installing -The plugin requires [richchip](https://github.com/beeender/richclip) command +The plugin requires [richclip](https://github.com/beeender/richclip) command line utility to be installed. The plugin will try to download the utility if -necessary. Or it can be installed separately. -See [richchip#install](https://github.com/beeender/richclip?tab=readme-ov-file#installing). +necessary on Linux, macOS, and Windows. Or it can be installed separately. +See [richclip#install](https://github.com/beeender/richclip?tab=readme-ov-file#installing). ### lazy.nvim @@ -49,7 +49,7 @@ require("richclip").setup({ ### Copy the highlighted code -By default, `richchip.nvim` will set itself as the Neovim clipboard provider, +By default, `richclip.nvim` will set itself as the Neovim clipboard provider, `"+y` (or other hotkeys set by users) will copy the current selection to the clipboard in both `text/plain` and `text/html` format. So the paste client, like a word processor, can choose the preferred format to use. diff --git a/bin/install.ps1 b/bin/install.ps1 new file mode 100644 index 0000000..e4fbe6d --- /dev/null +++ b/bin/install.ps1 @@ -0,0 +1,29 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Version, + + [Parameter(Mandatory = $true)] + [string]$TargetDir +) + +$ErrorActionPreference = "Stop" + +$architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() +$target = switch ($architecture) { + "X64" { "x86_64-pc-windows-msvc" } + "Arm64" { "aarch64-pc-windows-msvc" } + default { throw "Unsupported Windows architecture: '$architecture'" } +} + +$archiveName = "richclip_v${Version}_${target}.zip" +$url = "https://github.com/beeender/richclip/releases/download/v${Version}/${archiveName}" +$archivePath = Join-Path ([System.IO.Path]::GetTempPath()) $archiveName + +New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null +try { + Invoke-WebRequest -Uri $url -OutFile $archivePath + Expand-Archive -LiteralPath $archivePath -DestinationPath $TargetDir -Force +} +finally { + Remove-Item -LiteralPath $archivePath -Force -ErrorAction SilentlyContinue +} diff --git a/bin/install.sh b/bin/install.sh index 0b0dff4..46d9779 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -55,7 +55,26 @@ download() { popd ;; CYGWIN* | MINGW* | MSYS_NT*) - echoerr "Windows build is not available yet." + pushd "$TARGET_DIR" + arch="$(uname -m)" + case "${arch}" in + arm64|aarch64) + target="aarch64-pc-windows-msvc" + ;; + x86_64) + target="x86_64-pc-windows-msvc" + ;; + *) + echoerr "Unsupported Windows architecture: '${arch}'" + exit 1 + ;; + esac + archive="richclip_v${VERSION_STR}_${target}.zip" + curl -fsSLO \ + "https://github.com/beeender/richclip/releases/download/v${VERSION_STR}/${archive}" + tar -xf "$archive" + rm "$archive" + popd ;; *) echoerr "Unknown system '$unameOut'" diff --git a/lua/richclip/api.lua b/lua/richclip/api.lua index e2ab9a4..16d45e9 100644 --- a/lua/richclip/api.lua +++ b/lua/richclip/api.lua @@ -4,6 +4,10 @@ local ser = require("richclip.ser") local utils = require("richclip.utils") local binary = require("richclip.binary") +local function use_primary(is_primary) + return is_primary and vim.fn.has("win32") == 0 +end + ---Copy the selections to the clipboard. ---The {SELECTION} should be a table like: ---{ @@ -15,7 +19,7 @@ local binary = require("richclip.binary") ---@param selections SELECTION[] API.to_clip = function(is_primary, selections) local args = { "copy" } - if is_primary then + if use_primary(is_primary) then table.insert(args, "--primary") end @@ -37,7 +41,7 @@ end ---@return [string] API.from_clip = function(is_primary, mime_type) local args = { "paste" } - if is_primary then + if use_primary(is_primary) then table.insert(args, "--primary") end diff --git a/lua/richclip/binary.lua b/lua/richclip/binary.lua index e26de2d..9da2bb0 100644 --- a/lua/richclip/binary.lua +++ b/lua/richclip/binary.lua @@ -5,20 +5,26 @@ local config = require("richclip.config") local utils = require("richclip.utils") M._major_ver = "0" -M._minor_ver = "3" +M._minor_ver = "4" M._patch_ver = "0" -local current_file_dir = debug.getinfo(1).source:match('@?(.*/)') -local current_file_dir_parts = vim.split(current_file_dir, '/') -local root_dir = table.concat(utils.table_slice(current_file_dir_parts, 1, #current_file_dir_parts - 3), '/') +local current_file = debug.getinfo(1).source:gsub("^@", "") +local root_dir = vim.fs.dirname(vim.fs.dirname(vim.fs.dirname(current_file))) +local installer_dir = vim.fs.joinpath(root_dir, "bin") -- Need to mock in test -M._bin_dir = root_dir .. "/bin" -local install_script_path = M._bin_dir .. "/install.sh" -local richclip_bin_path = M._bin_dir .. "/richclip" +M._bin_dir = installer_dir M._exe_path = nil M.tried_download = false +local function is_windows() + return vim.fn.has("win32") ~= 0 +end + +local function richclip_bin_path() + return vim.fs.joinpath(M._bin_dir, is_windows() and "richclip.exe" or "richclip") +end + local function check_richclip_version(path) if vim.fn['executable'](path) == 0 then return false @@ -37,23 +43,58 @@ local function check_richclip_version(path) return false end - local pattern = "(%d+%.%d+%.[^ ]*)" - local ver_str = string.match(ret.stdout, pattern) - local pattern_parts = "(%d+)%.(%d+)%.(%d+)" - local major, minor, _ = ver_str:match(pattern_parts) - if (M._major_ver > major) or (M._major_ver == major and M._minor_ver > minor) then + local ver_str = ret.stdout:match("(%d+%.%d+%.[^%s]+)") + local major, minor, patch + if ver_str ~= nil then + major, minor, patch = ver_str:match("(%d+)%.(%d+)%.(%d+)") + end + if patch == nil then utils.notify("binary.check_richclip_version", { - msg = string.format("\"%s\" is at version '%s', which is lower than the required version '%s.%s.x'", path, - ver_str, M._major_ver, M._minor_ver), + msg = string.format("Could not determine the version of \"%s\" from:\n%s", path, ret.stdout), level = "WARN" }) + return false + end + + local installed = { tonumber(major), tonumber(minor), tonumber(patch) } + local required = { tonumber(M._major_ver), tonumber(M._minor_ver), tonumber(M._patch_ver) } + local supported = false + for i = 1, 3 do + if installed[i] ~= required[i] then + supported = installed[i] > required[i] + break + end + supported = true + end + if not supported then + utils.notify("binary.check_richclip_version", { + msg = string.format("\"%s\" is at version '%s', which is lower than the required version '%s.%s.%s'", + path, ver_str, M._major_ver, M._minor_ver, M._patch_ver), + level = "WARN" + }) + return false end return true end M.download_richclip_binary = function() local ver_str = string.format("%d.%d.%d", M._major_ver, M._minor_ver, M._patch_ver) - local cmd_line = { install_script_path, ver_str, M._bin_dir } + local cmd_line + if is_windows() then + cmd_line = { + "powershell.exe", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + vim.fs.joinpath(installer_dir, "install.ps1"), + ver_str, + M._bin_dir, + } + else + cmd_line = { vim.fs.joinpath(installer_dir, "install.sh"), ver_str, M._bin_dir } + end -- Set the flag to avoid infinite download & check loop M.tried_download = true @@ -73,14 +114,6 @@ M.download_richclip_binary = function() end M.get_richclip_exe_path = function() - if vim.fn['has']("win32") ~= 0 then - utils.notify("binary.get_richclip_exe_path", { - msg = '"richclip" does not support Windows yet', - level = "ERROR" - }) - return nil - end - if M._exe_path ~= nil then return M._exe_path end @@ -96,8 +129,8 @@ M.get_richclip_exe_path = function() end elseif check_richclip_version("richclip") then M._exe_path = "richclip" - elseif check_richclip_version(richclip_bin_path) then - M._exe_path = richclip_bin_path + elseif check_richclip_version(richclip_bin_path()) then + M._exe_path = richclip_bin_path() end if M._exe_path == nil and (not M.tried_download) then utils.notify("binary.get_richclip_exe_path", { diff --git a/lua/richclip/init.lua b/lua/richclip/init.lua index 82682bc..a9d22a3 100644 --- a/lua/richclip/init.lua +++ b/lua/richclip/init.lua @@ -13,7 +13,7 @@ function M.setup(options) config.with_defaults(options) if config.enable_debug then - vim.env.RICHCLIP_LOG_FILE = "/tmp/richclip.log" + vim.env.RICHCLIP_LOG_FILE = vim.fs.joinpath(vim.fn.stdpath("log"), "richclip.log") end if config.set_g_clipboard then @@ -48,13 +48,6 @@ end ---Takes over the g.clipboard function M.set_g_clipboard() - if vim.fn['has']("win32") ~= 0 then - utils.notify("richclip.set_g_clipboard", { - msg = '"richclip" does not support Windows yet', - level = "WARN" - }) - return - end if vim.g.clipboard ~= nil then utils.notify("richclip.set_g_clipboard", { msg = diff --git a/tests/spec/binary_spec.lua b/tests/spec/binary_spec.lua index ad42ea2..ea55586 100644 --- a/tests/spec/binary_spec.lua +++ b/tests/spec/binary_spec.lua @@ -57,7 +57,7 @@ describe("binary tests", function() it('download_richclip_binary', function() local old_dir = binary._bin_dir - local exe_path = "/tmp/richclip" + local exe_path = "/tmp/richclip" .. (vim.fn.has("win32") ~= 0 and ".exe" or "") vim.fn['delete'](exe_path) binary._bin_dir = "/tmp"