From c80c7e185167e409aae8af144d2e2882e5c2fefe Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 23 Dec 2024 19:36:35 -0700 Subject: [PATCH] refactor: move clipboard tools to search fo into `opts` --- README.md | 103 +++++++++++++++--- lua/universal-clipboard/init.lua | 177 +++++++++++++++++-------------- 2 files changed, 189 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index f44303b..b944827 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,14 @@ clipboard** across multiple platforms. It addresses this issue: > **Platform Differences**: On macOS, Neovim appears to behave nicely with the > system `clipboard`. On others, not so much. External tools can help. For -> example, there’s `wl-copy`/`wl-paste`for Wayland or`xclip`/`xsel` for X11. +> example, there’s `wl-copy`/`wl-paste` for Wayland or `xclip`/`xsel` for X11. With **universal-clipboard.nvim**: - If we detect an external clipboard tool (`wl-clipboard`, `xclip`, `xsel`), - we configure the `clipboard` to use it. -- Otherwise, we simply set it to `unnamedplus` with no further configuration. + we configure Neovim’s `clipboard` to use it. +- Otherwise, we simply set `clipboard` to `unnamedplus` with no further + configuration needed. ## Installation @@ -22,7 +23,7 @@ use({ "swaits/universal-clipboard.nvim", config = function() require("universal-clipboard").setup({ - verbose = false, -- optional, set true to see a small startup message + verbose = false, -- optional: set true to log detection details }) end, }) @@ -34,33 +35,109 @@ Or [**lazy.nvim**](https://github.com/folke/lazy.nvim): { "swaits/universal-clipboard.nvim", opts = { - verbose = true, -- optional, set true to see a small startup message + verbose = true, -- optional: set true to log detection details }, } ``` -> **Note:** If your Neovim lacks `+clipboard`, make sure you have at least one -> of `wl-clipboard`, `xclip`, or `xsel` is installed. +> **Note**: If your Neovim lacks `+clipboard`, make sure you have at least one +> of `wl-clipboard`, `xclip`, or `xsel` installed. Or you may customize the +> tools searched -- see below. -### Default Options +--- + +## Default Options ```lua { - -- If true, prints the chosen clipboard mode at startup + -- If true, prints the chosen clipboard mode/tool at startup verbose = false, + + -- A table of tools to check, in order + tools = { + { + name = "wl-clipboard", + -- Either a string (the executable name) or a function returning boolean + detect = function() + local wayland_display = os.getenv("WAYLAND_DISPLAY") + local wayland_runtime = os.getenv("XDG_RUNTIME_DIR") + local socket_path = (wayland_runtime or "") .. "/" .. (wayland_display or "") + return (vim.fn.executable("wl-copy") == 1) + and (vim.fn.executable("wl-paste") == 1) + and wayland_display ~= "" + and (vim.fn.isdirectory(socket_path) == 1) + end, + commands = { + copy = "wl-copy", + paste = "wl-paste --no-newline", + }, + }, + { + name = "xclip", + detect = "xclip", -- Will check vim.fn.executable("xclip") + commands = { + copy = "xclip -selection clipboard", + paste = "xclip -selection clipboard -o", + }, + }, + { + name = "xsel", + detect = "xsel", + commands = { + copy = "xsel --clipboard --input", + paste = "xsel --clipboard --output", + }, + }, + }, } ``` +### Customizing Tools + +If you want to add or remove tools, you can override the `tools` table in your +`setup()` call. For example: + +```lua +require("universal-clipboard").setup({ + verbose = true, + tools = { + -- If you only want xclip, for instance: + { + name = "xclip-only", + detect = "xclip", + commands = { + copy = "xclip -selection clipboard", + paste = "xclip -selection clipboard -o", + }, + }, + -- Or add more custom entries + }, +}) +``` + +The plugin will iterate over `tools` in order and select the first one that is +“available.” + +--- + ## Usage -Once installed, **no additional steps** are needed. The plugin configures your +Once installed, **no additional steps** are required. The plugin configures your clipboard on startup. Simply copy and paste in Neovim as usual. ### Commands -- `:UniversalClipboardCheck` - Shows the current `clipboard` configuration. -- `:UniversalClipboardReinit` - Re-runs detection. You **do not normally need to - run this.** It's run automatically on startup. +- **`:UniversalClipboardCheck`** + Prints the current `clipboard` configuration to the command line, showing what + `vim.opt.clipboard` and `vim.g.clipboard` are set to. + +- **`:UniversalClipboardReinit`** + Re-runs the detection logic, forcing `verbose` mode for that run to help + troubleshoot. You normally do **not** need this, but it can be helpful if you + install a new system tool _after_ starting Neovim and want to see if it’s + detected. + +--- ## License diff --git a/lua/universal-clipboard/init.lua b/lua/universal-clipboard/init.lua index 5769f68..1a0a4bb 100644 --- a/lua/universal-clipboard/init.lua +++ b/lua/universal-clipboard/init.lua @@ -1,126 +1,147 @@ -- lua/universal-clipboard/init.lua -local M = {} +local M = { + -- Our default options + opts = { + -- Whether to log stuff to the vim console + verbose = false, --- Our default options -local default_opts = { - verbose = false, + -- Copy/paste tools to check for + tools = { + -- Wayland and wl-copy/wl-paste + { + name = "wl-clipboard", + detect = function() + local wayland_display = os.getenv("WAYLAND_DISPLAY") + local wayland_runtime = os.getenv("XDG_RUNTIME_DIR") + local socket_path = (wayland_runtime or "") .. "/" .. (wayland_display or "") + return (vim.fn.executable("wl-copy") == 1) + and (vim.fn.executable("wl-paste") == 1) + and wayland_display ~= "" + and (vim.fn.isdirectory(socket_path) == 1) + end, + commands = { + copy = "wl-copy", + paste = "wl-paste --no-newline", + }, + }, + -- X11 and xclip + { + name = "xclip", + detect = "xclip", -- Just a string, means "check if `xclip` is executable" + commands = { + copy = "xclip -selection clipboard", + paste = "xclip -selection clipboard -o", + }, + }, + -- X11 and xsel + { + name = "xsel", + detect = "xsel", + commands = { + copy = "xsel --clipboard --input", + paste = "xsel --clipboard --output", + }, + }, + }, + }, } --- Pick copy/paste commands for Wayland or X11 if '+clipboard' is missing -local function pick_copy_paste_commands(opts) - if opts.verbose then - vim.notify("[universal-clipboard.nvim] looking for wl-copy/xclip/xsel ...") +-- Utils for logging +local function log(msg, level) + if M.opts.verbose then + vim.notify(msg, level or vim.log.levels.INFO) end +end - -- Check Wayland + wl-copy/wl-paste - local wayland_display = os.getenv("WAYLAND_DISPLAY") - local wayland_runtime = os.getenv("XDG_RUNTIME_DIR") - local wayland_socket_path = (wayland_runtime or "") .. "/" .. (wayland_display or "") +local function info(msg) + log(msg, vim.log.levels.INFO) +end - local has_wlcopy = (vim.fn.executable("wl-copy") == 1) - local has_wlpaste = (vim.fn.executable("wl-paste") == 1) - local have_wayland = wayland_display and wayland_display ~= "" and (vim.fn.isdirectory(wayland_socket_path) == 1) +local function warn(msg) + log(msg, vim.log.levels.WARN) +end - if has_wlcopy and has_wlpaste and have_wayland then - if opts.verbose then - vim.notify("[universal-clipboard.nvim] found wl-copy/wl-paste + Wayland", vim.log.levels.INFO) +-- Pick copy/paste commands from the list in opts.tools +local function pick_copy_paste_tool(opts) + info("[universal-clipboard.nvim] looking for copy/paste tool ...") + + local function check_detect(tool) + if type(tool.detect) == "function" then + return tool.detect() + elseif type(tool.detect) == "string" then + return vim.fn.executable(tool.detect) == 1 + else + vim.notify( + "[universal-clipboard.nvim] tool.detect is not a string or function: " .. vim.inspect(tool), + vim.log.levels.ERROR + ) end - return { - copy = "wl-copy", - paste = "wl-paste --no-newline", - } + -- Fallback for unexpected types + return false end - -- Next, try xclip - if vim.fn.executable("xclip") == 1 then - if opts.verbose then - vim.notify("[universal-clipboard.nvim] found xclip", vim.log.levels.INFO) + for _, tool in ipairs(opts.tools) do + if check_detect(tool) then + info("[universal-clipboard.nvim] found " .. tool.name) + return tool end - return { - copy = "xclip -selection clipboard", - paste = "xclip -selection clipboard -o", - } end - -- Otherwise, try xsel - if vim.fn.executable("xsel") == 1 then - if opts.verbose then - vim.notify("[universal-clipboard.nvim] found xsel", vim.log.levels.INFO) - end - return { - copy = "xsel --clipboard --input", - paste = "xsel --clipboard --output", - } - end - - -- No suitable tool found - if opts.verbose then - vim.notify("[universal-clipboard.nvim] none found.") - end + info("[universal-clipboard.nvim] none found.") return nil end --- 4) Actually configure Neovim's clipboard usage +-- Configure Neovim's clipboard usage local function configure_clipboard(opts) -- Unify normal yanks/pastes to systepm clipboard vim.opt.clipboard = "unnamedplus" -- Search for custom clipboard provider - local commands = pick_copy_paste_commands(opts) - if commands then + local tool = pick_copy_paste_tool(opts) + if tool then vim.g.clipboard = { - name = "UniversalClipboard", + name = "UniversalClipboard(" .. tool.name .. ")", copy = { - ["+"] = commands.copy, - ["*"] = commands.copy, + ["+"] = tool.commands.copy, + ["*"] = tool.commands.copy, }, paste = { - ["+"] = commands.paste, - ["*"] = commands.paste, + ["+"] = tool.commands.paste, + ["*"] = tool.commands.paste, }, cache_enabled = 0, } - return "wl-copy/xclip/xsel" + return tool.name else -- No tool found - if opts.verbose then - vim.notify( - "[universal-clipboard.nvim] No external clipboard tool found (wl-copy, xclip, xsel).", - vim.log.levels.WARN - ) - end - return "system" + warn("[universal-clipboard.nvim] No external clipboard tool found.") + return "(none)" end end -- Setup function (entry point) -function M.setup(user_opts) - -- Merge defaults with user_opts - local opts = vim.tbl_deep_extend("force", default_opts, user_opts or {}) +function M.setup(opts) + -- Merge defaults with opts + M.opts = vim.tbl_deep_extend("force", M.opts, opts or {}) -- Set the clipboard up - local mode = configure_clipboard(opts) - if opts.verbose then - vim.notify(("[universal-clipboard.nvim] Clipboard mode: %s"):format(mode), vim.log.levels.INFO) - end + local tool_name = configure_clipboard(M.opts) + info(("[universal-clipboard.nvim] Clipboard tool: %s"):format(tool_name)) - -- Create user commands for debugging / re-init + -- Command for viewing results vim.api.nvim_create_user_command("UniversalClipboardCheck", function() print("=== UniversalClipboardCheck ===") - local opt_clipboard = vim.opt.clipboard:get() print("vim.opt.clipboard = ", vim.inspect(opt_clipboard)) - - if vim.g.clipboard then - print("vim.g.clipboard = ", vim.inspect(vim.g.clipboard)) - else - print("vim.g.clipboard is not set.") - end + print("vim.g.clipboard = ", vim.inspect(vim.g.clipboard)) end, {}) + -- Command for re-running checks vim.api.nvim_create_user_command("UniversalClipboardReinit", function() - local new_mode = configure_clipboard({ verbose = true }) - print("Reinitialized clipboard mode: " .. new_mode) + print("=== UniversalClipboardReinit ===") + local verbose_opts = vim.tbl_deep_extend("force", M.opts, { verbose = true }) + local new_tool_name = configure_clipboard(verbose_opts) + print("Reinitialized external clipboard tool: " .. new_tool_name) vim.cmd("UniversalClipboardCheck") end, {}) end