├── .gitignore ├── LICENSE ├── README.md ├── doc └── neoclip.txt ├── lua └── neoclip │ ├── health.lua │ └── init.lua ├── plugin └── neoclip.lua └── src ├── CMakeLists.txt ├── extra └── wlr-data-control-unstable-v1.xml ├── meson.build ├── neo_common.c ├── neo_wayland.c ├── neo_wayland_uv.c ├── neo_x11.c ├── neo_x11_uv.c ├── neoclip.h ├── neoclip_mac.m ├── neoclip_nix.c ├── neoclip_nix.h └── neoclip_w32.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.a 2 | *.dll 3 | *.so 4 | build/ 5 | tags 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | This is [Neovim][1] clipboard provider. It allows to access system clipboard without any 4 | extra tools, such as `win32yank`, `xclip`, `xsel`, `pbcopy`, `pbpaste` and so on. 5 | 6 | Read `:h provider-clipboard` for more information on Neovim clipboard integration. 7 | 8 | ### Installation 9 | 10 | #### From source code 11 | 12 | First, fetch the plugin using any plugin manager you like, or simply clone it with `git` 13 | under your packages directory tree, see `:h packages`. 14 | 15 | An example for [minpac][2] 16 | 17 | 1. Add to your `init.vim` 18 | ``` 19 | call minpac#init() 20 | call minpac#add('matveyt/neoclip', #{type: 'opt'}) 21 | "... more plugins to follow 22 | 23 | if has('nvim') 24 | packadd! neoclip 25 | endif 26 | ``` 27 | 28 | 2. Save the file and reload configuration 29 | ``` 30 | :update | source % 31 | ``` 32 | 33 | 3. Update the plugin from network repository 34 | ``` 35 | :call minpac#update('neoclip') 36 | ``` 37 | 38 | Next, drop to your shell and compile platform-dependent module from source. 39 | 40 | 4. Compiling 41 | ``` 42 | $ cd ~/.config/nvim/pack/minpac/start/neoclip/src 43 | 44 | $ # by CMake 45 | $ cmake -B build 46 | $ cmake --build build 47 | $ cmake --install build --strip 48 | 49 | $ # ..or by Meson 50 | $ meson setup build 51 | $ meson install -C build --strip 52 | ``` 53 | 54 | 5. Run Neovim again and see if it's all right 55 | ``` 56 | :checkhealth neoclip 57 | ``` 58 | 59 | #### With Homebrew 60 | 61 | There is a formula for `Homebrew` in [homebrew-neoclip][3] repository. 62 | 63 | #### As a Nix Flake 64 | 65 | Please, refer to [neoclip-flake][4] repository: 66 | - an [example][5] which uses an overlay 67 | - an [example][6] which uses a package directly 68 | 69 | ### Compatibility and other troubles 70 | 71 | Neoclip should run on Windows, macOS and all the various \*nix'es (with X11 and/or 72 | Wayland display server). See `:h neoclip-build` to get more information on build 73 | dependencies. See `:h neoclip-issues` for a list of known issues. 74 | 75 | [1]: https://neovim.io 76 | [2]: https://github.com/k-takata/minpac 77 | [3]: https://github.com/neoclip-nvim/homebrew-neoclip 78 | [4]: https://github.com/neoclip-nvim/neoclip-flake 79 | [5]: https://github.com/neoclip-nvim/neoclip-flake/blob/master/examples/with-overlay/flake.nix 80 | [6]: https://github.com/neoclip-nvim/neoclip-flake/blob/master/examples/with-package/flake.nix 81 | -------------------------------------------------------------------------------- /doc/neoclip.txt: -------------------------------------------------------------------------------- 1 | *neoclip.txt* Neovim clipboard provider 2 | 3 | Type |gO| to see the table of contents. 4 | 5 | ============================================================================== 6 | OVERVIEW *neoclip* 7 | 8 | |Neoclip| is a clipboard provider for Neovim |provider-clipboard|. It allows 9 | to access system clipboard without any external tools, such as `win32yank`, 10 | `xclip`, `xsel`, `pbcopy`, `pbpaste` etc. 11 | 12 | Supported platforms are Windows, macOS and *nix (with X11 or Wayland). 13 | 14 | ============================================================================== 15 | AUTOLOAD *neoclip-autoload* 16 | 17 | |Neoclip| is loaded by default. You don't need to add anything to your 18 | |config|. Yet if you wish to skip it then configure another 19 | |provider-clipboard|. E.g., `:runtime autoload/provider/clipboard.vim` to use 20 | Neovim's default instead. 21 | 22 | If you share same |config| between Vim and Neovim then it is advisable to 23 | install |neoclip| under `opt/` tree and use |:packadd| command > 24 | 25 | if has('nvim') 26 | packadd! neoclip 27 | endif 28 | < 29 | ============================================================================== 30 | BUILDING *neoclip-build* 31 | 32 | |Neoclip| must be built from source. So you'll need C (or Objective C for 33 | macOS) compiler and tools installed on your machine. 34 | 35 | You also need a project build tool. Both CMake https://cmake.org and Meson 36 | https://mesonbuild.com are supported. 37 | 38 | The common build pre-requisite for all platforms is LuaJIT https://luajit.org. 39 | This is because we're going to make a Lua binary extension module for Neovim's 40 | builtin engine. The exact package name and installation method depend on your 41 | OS/distro. For example, on Debian GNU/Linux it could be > 42 | 43 | $sudo apt install libluajit-5.1-dev 44 | < 45 | Hopefully, it's enough for both Windows and macOS. But on *nix we need also 46 | threads, X11 and Wayland client libraries. Some of them may already be 47 | installed, while some are not. An example suitable for Debian GNU/Linux > 48 | 49 | $sudo apt install libx11-dev libwayland-dev 50 | < 51 | And the final point. CMake doesn't support Wayland libraries out-of-the-box. 52 | So building the project with CMake may require installing ECM (aka Extra CMake 53 | Modules) package as well. Otherwise, neoclip/Wayland module would be quietly 54 | skipped. > 55 | 56 | $sudo apt install extra-cmake-modules 57 | < 58 | So now we are able to build our project > 59 | 60 | $ cd ~/.config/nvim/pack/bundle/opt/neoclip/src 61 | 62 | $ # by CMake 63 | $ cmake -B build 64 | $ cmake --build build 65 | $ cmake --install build --strip 66 | 67 | $ # ..or by Meson 68 | $ meson setup build 69 | $ meson install -C build --strip 70 | < 71 | Then run Neovim and type > 72 | 73 | :checkhealth neoclip 74 | < 75 | to see if everything went okay. 76 | 77 | ============================================================================== 78 | FUNCTIONS *neoclip-functions* 79 | 80 | |Neoclip| returns a module table that has the following fields. 81 | 82 | |neoclip.driver| 83 | This is a binary module doing real job. You can also call it directly if you 84 | wish so. The methods are > 85 | 86 | neoclip.driver.id() -> string 87 | neoclip.driver.start() -> nil or error 88 | neoclip.driver.stop() -> nil 89 | neoclip.driver.status() -> boolean 90 | neoclip.driver.get(reg) -> {string_array, type} 91 | neoclip.driver.set(reg, string_array, type) -> boolean 92 | < 93 | NOTE: start/stop/status are only functional under *nix OS. In Windows and 94 | macOS they are doing nothing. 95 | 96 | |neoclip.require()| 97 | This method loads binary module into |neoclip.driver| variable. You seldom 98 | need it as |neoclip.setup()| calls it for you. > 99 | 100 | -- force loading of X11 driver only 101 | local neoclip = require"neoclip" 102 | neoclip.require"neoclip.x11-driver" 103 | neoclip.register() 104 | < 105 | |neoclip.register()| 106 | This method sets or resets |g:clipboard| variable activating the plugin. 107 | Usually, you want to call |neoclip.setup()| only. > 108 | 109 | -- set Neovim default clipboard-tool 110 | require"neoclip".register(false) 111 | 112 | -- get neoclip.driver back 113 | require"neoclip".register() 114 | < 115 | |neoclip.setup()| 116 | This method performs module initialization. Basically, it is an equivalent 117 | of |neoclip.require()| followed by |neoclip.register()|. Optionally, it also 118 | accepts driver name to pass to |neoclip.require()|. > 119 | 120 | -- load and register default driver 121 | require"neoclip".setup() 122 | < 123 | ============================================================================== 124 | HEALTH *neoclip-health* 125 | 126 | |Neoclip| implements Neovim |health| API. Run |:checkhealth| command to monitor its 127 | current status. 128 | 129 | ============================================================================== 130 | KNOWN ISSUES *neoclip-issues* 131 | 132 | *neoclip-gnome* *neoclip-weston* 133 | neoclip/Wayland module requires wlr-data-control protocol 134 | https://wayland.app/protocols/wlr-data-control-unstable-v1. If your Wayland 135 | compositor does not provide it then neoclip/X11 will be loaded instead. 136 | 137 | *neoclip-luv* 138 | There are two new driver versions for *nix: `neoclip.wluv-driver` and 139 | `neoclip.x11uv-driver`. They are still considered experimental and not used by 140 | default. The main difference is that they depend on |luv-event-loop| not 141 | requiring an extra thread. If you want to try it then pass the driver name 142 | directly > 143 | 144 | require"neoclip".setup"neoclip.wluv-driver" 145 | < 146 | *neoclip-wsl* 147 | There was a longstanding bug that made |neoclip| to fail on WSL. Now it's no 148 | more! Still to remember that WSLg is based on Weston composer, so 149 | |neoclip-weston| issue applies. 150 | 151 | ============================================================================== 152 | vim:tw=78:ts=8:noet:ft=help:norl: 153 | -------------------------------------------------------------------------------- /lua/neoclip/health.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | neoclip - Neovim clipboard provider 3 | Last Change: 2025 Mar 17 4 | License: https://unlicense.org 5 | URL: https://github.com/matveyt/neoclip 6 | --]] 7 | 8 | 9 | local function neo_check() 10 | local h = vim.health or { 11 | start = require"health".report_start, 12 | info = require"health".report_info, 13 | ok = require"health".report_ok, 14 | warn = require"health".report_warn, 15 | error = require"health".report_error, 16 | } 17 | h.start"neoclip" 18 | 19 | if vim.fn.has"clipboard" == 0 then 20 | h.error("Clipboard provider is disabled", { 21 | "Registers + and * will not work", 22 | }) 23 | return 24 | end 25 | 26 | local neoclip = _G.package.loaded.neoclip 27 | if not neoclip then 28 | h.error("Not found", { 29 | "Do you |require()| it?", 30 | "Refer to |neoclip-build| on how to build from source code", 31 | "OS Windows, macOS and *nix (with X11 or Wayland) are supported", 32 | }) 33 | return 34 | end 35 | 36 | local driver = neoclip.driver 37 | if not driver then 38 | h.error("No driver module loaded", neoclip.issues or { 39 | "Have you run |neoclip.setup()|?", 40 | }) 41 | return 42 | end 43 | 44 | local clipboard_id = vim.fn["provider#clipboard#Executable"]() 45 | local driver_id = driver.id() 46 | if clipboard_id == driver_id then 47 | h.ok(string.format("*%s* driver is in use", driver_id)) 48 | 49 | local display = nil 50 | if vim.endswith(driver_id, "Wayland") then 51 | display = vim.env.WAYLAND_DISPLAY 52 | elseif vim.endswith(driver_id, "X11") then 53 | display = vim.env.DISPLAY 54 | end 55 | if display then 56 | h.info(string.format("Running on display `%s`", display)) 57 | end 58 | else 59 | h.warn(string.format("*%s* driver is loaded but not properly registered", 60 | driver_id), { 61 | "Do not install another clipboard provider", 62 | "Check list of issues", 63 | "Try |neoclip.register()|", 64 | }) 65 | end 66 | 67 | if not driver.status() then 68 | h.warn("Driver module stopped", { 69 | "|neoclip.driver.start()| to restart", 70 | }) 71 | end 72 | 73 | if neoclip.issues then 74 | h.warn("Found issues", neoclip.issues) 75 | end 76 | 77 | local reg_plus, reg_star = vim.fn.getreginfo"+", vim.fn.getreginfo"*" 78 | local line_plus, line_star = "На дворе трава", "На траве дрова" 79 | local uv = vim.uv or vim.loop 80 | local now = tostring(uv.now()) 81 | 82 | if not driver.set("+", { now, line_plus }, "b") then 83 | h.warn"Driver failed to set register +" 84 | end 85 | if not driver.set("*", { now, line_star }, "b") then 86 | h.warn"Driver failed to set register *" 87 | end 88 | 89 | -- for "cache_enabled" provider 90 | uv.sleep(200) 91 | 92 | local test_plus = vim.fn.getreginfo"+" 93 | vim.fn.setreg("+", reg_plus) 94 | vim.fn.setreg("*", reg_star) 95 | 96 | if #test_plus.regcontents == 2 and test_plus.regcontents[1] == now and 97 | (test_plus.regcontents[2] == line_plus or test_plus.regcontents[2] == line_star) 98 | then 99 | h.ok"Clipboard test passed" 100 | 101 | if test_plus.regcontents[2] == line_star then 102 | h.info"NOTE registers + and * are always equal" 103 | end 104 | 105 | assert(#line_plus == #line_star and #line_plus >= #now) 106 | if test_plus.regtype ~= "\22" .. #line_plus then 107 | h.warn(string.format("Block type has been changed to %q", test_plus.regtype), 108 | { 109 | string.format("It looks like %s does not support Vim's native blocks", 110 | clipboard_id), 111 | }) 112 | end 113 | else 114 | h.error("Clipboard test failed", { 115 | "Sometimes, this happens because of `cache_enabled` setting", 116 | "Repeat |:checkhealth| again before reporting a bug", 117 | }) 118 | end 119 | end 120 | 121 | 122 | return { check = neo_check } 123 | -------------------------------------------------------------------------------- /lua/neoclip/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | neoclip - Neovim clipboard provider 3 | Last Change: 2025 Mar 26 4 | License: https://unlicense.org 5 | URL: https://github.com/matveyt/neoclip 6 | --]] 7 | 8 | 9 | local neoclip = { 10 | -- driver = require"neoclip.XYZ" 11 | -- issues = {"array", "of", "strings"} 12 | -- 13 | -- issue(fmt, ...) 14 | -- require(driver) 15 | -- register([clipboard]) 16 | -- setup([driver]) 17 | } 18 | 19 | 20 | function neoclip.issue(fmt, ...) 21 | neoclip.issues = neoclip.issues or {nil} -- pre-allocate slot 22 | neoclip.issues[#neoclip.issues + 1] = fmt:format(...) 23 | end 24 | 25 | function neoclip.require(driver) 26 | local status, result1, result2 27 | 28 | status, result1 = pcall(require, driver) 29 | if status then 30 | status, result2 = pcall(result1.start) 31 | if status then 32 | neoclip.driver = result1 33 | else 34 | neoclip.issue("'%s' failed to start", driver) 35 | neoclip.issue("%s", result2) 36 | _G.package.loaded[driver] = nil 37 | end 38 | else 39 | neoclip.issue("'%s' failed to load", driver) 40 | neoclip.issue("%s", result1) 41 | end 42 | 43 | return status 44 | end 45 | 46 | function neoclip.register(clipboard) 47 | if clipboard == nil then 48 | -- catch driver load failure 49 | assert(neoclip.driver, "neoclip driver error (:checkhealth for info)") 50 | -- setup g:clipboard 51 | vim.g.clipboard = { 52 | name = neoclip.driver.id(), 53 | copy = { 54 | ["+"] = function(...) return neoclip.driver.set("+", ...) end, 55 | ["*"] = function(...) return neoclip.driver.set("*", ...) end, 56 | }, 57 | paste = { 58 | ["+"] = function() return neoclip.driver.get"+" end, 59 | ["*"] = function() return neoclip.driver.get"*" end, 60 | }, 61 | cache_enabled = false, 62 | } 63 | -- create autocmds 64 | if vim.api.nvim_create_augroup then 65 | local group = vim.api.nvim_create_augroup("neoclip", { clear=true }) 66 | vim.api.nvim_create_autocmd("VimSuspend", { group=group, 67 | callback=neoclip.driver.stop }) 68 | vim.api.nvim_create_autocmd("VimResume", { group=group, 69 | callback=neoclip.driver.start }) 70 | else 71 | vim.cmd[[ 72 | augroup neoclip | au! 73 | autocmd VimSuspend * lua require"neoclip".driver.stop() 74 | autocmd VimResume * lua require"neoclip".driver.start() 75 | augroup end 76 | ]] 77 | end 78 | else 79 | vim.g.clipboard = clipboard 80 | vim.cmd[[ 81 | if exists("#neoclip") 82 | autocmd! neoclip 83 | augroup! neoclip 84 | endif 85 | ]] 86 | end 87 | 88 | -- :h provider-reload 89 | vim.g.loaded_clipboard_provider = nil 90 | vim.cmd"runtime autoload/provider/clipboard.vim" 91 | end 92 | 93 | function neoclip.setup(arg1, arg2) 94 | -- local helper function 95 | local has = function(feat) return vim.fn.has(feat) == 1 end 96 | 97 | -- compat: accept both neoclip:setup() and neoclip.setup() 98 | local driver = (arg1 ~= neoclip) and arg1 or arg2 99 | 100 | -- (re-)init self 101 | if neoclip.driver then 102 | neoclip.driver.stop() 103 | neoclip.driver = nil 104 | end 105 | neoclip.issues = nil 106 | 107 | -- load driver 108 | if driver then 109 | neoclip.require(driver) 110 | elseif has"win32" then 111 | neoclip.require"neoclip.w32-driver" 112 | elseif has"mac" then 113 | neoclip.require"neoclip.mac-driver" 114 | elseif has"unix" then 115 | -- Wayland first, fallback to X11 116 | local _ = vim.env.WAYLAND_DISPLAY 117 | and (neoclip.require"neoclip.wl-driver" 118 | or neoclip.require"neoclip.wluv-driver") 119 | or (neoclip.require"neoclip.x11-driver" 120 | or neoclip.require"neoclip.x11uv-driver") 121 | else 122 | neoclip.issue"Unsupported platform" 123 | end 124 | 125 | -- warn if &clipboard is unnamed[plus] 126 | if vim.go.clipboard ~= "" then 127 | neoclip.issue("'clipboard' option is set to *%s*", vim.go.clipboard) 128 | end 129 | 130 | neoclip.register() 131 | end 132 | 133 | 134 | return neoclip 135 | -------------------------------------------------------------------------------- /plugin/neoclip.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | neoclip - Neovim clipboard provider 3 | Last Change: 2025 Mar 17 4 | License: https://unlicense.org 5 | URL: https://github.com/matveyt/neoclip 6 | --]] 7 | 8 | 9 | -- load default driver 10 | if not vim.g.loaded_clipboard_provider then 11 | require"neoclip".setup() 12 | end 13 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #[[ 2 | neoclip - Neovim clipboard provider 3 | Last Change: 2024 Aug 21 4 | License: https://unlicense.org 5 | URL: https://github.com/matveyt/neoclip 6 | #]] 7 | 8 | 9 | cmake_minimum_required(VERSION 3.16) 10 | project(neoclip DESCRIPTION "Neovim clipboard provider" LANGUAGES C) 11 | 12 | 13 | # selectively enable module build 14 | set(w32_target "ON") 15 | set(mac_target "ON") 16 | set(x11_target "ON") 17 | set(x11uv_target "ON") 18 | set(wl_target "ON") 19 | set(wluv_target "ON") 20 | 21 | 22 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 23 | set(CMAKE_BUILD_TYPE "Release") 24 | endif() 25 | set(CMAKE_C_EXTENSIONS "OFF") 26 | set(CMAKE_C_STANDARD "99") 27 | set(CMAKE_C_STANDARD_REQUIRED "ON") 28 | set(CMAKE_C_VISIBILITY_PRESET "internal") 29 | set(CMAKE_INSTALL_PREFIX "${PROJECT_SOURCE_DIR}/..") 30 | set(CMAKE_SHARED_MODULE_PREFIX "") 31 | 32 | set(gnu_like_compilers "GNU;Clang;AppleClang") 33 | if(CMAKE_C_COMPILER_ID IN_LIST gnu_like_compilers) 34 | add_compile_options(-Wall -Wextra -Wpedantic -Werror) 35 | endif() 36 | 37 | 38 | # pkg-config required 39 | find_package(PkgConfig REQUIRED) 40 | # Lua(JIT) is always required 41 | pkg_check_modules(PC_LUAJIT REQUIRED luajit) 42 | find_library(LUA_LIBRARIES PATHS "${PC_LUAJIT_LIBDIR}" 43 | NAMES luajit libluajit luajit-5.1 libluajit-5.1) 44 | set(LUA_DEFINITIONS "${PC_LUAJIT_CFLAGS_OTHER}") 45 | find_path(LUA_INCLUDE_DIRS luajit.h PATHS "${PC_LUAJIT_INCLUDEDIR}" 46 | PATH_SUFFIXES luajit luajit-2.0 luajit-2.1) 47 | 48 | 49 | # create/install target with Lua dependency 50 | function(neo_module name) 51 | cmake_parse_arguments(NEO "" "" "SOURCES;LIBRARIES;INCLUDE_DIRS" ${ARGN}) 52 | add_library(${name} MODULE ${NEO_SOURCES}) 53 | target_link_libraries(${name} ${LUA_LIBRARIES} ${NEO_LIBRARIES}) 54 | target_compile_definitions(${name} PRIVATE ${LUA_DEFINITIONS}) 55 | target_include_directories(${name} PRIVATE ${LUA_INCLUDE_DIRS} ${NEO_INCLUDE_DIRS}) 56 | install(TARGETS ${name} DESTINATION "lua/neoclip" PERMISSIONS OWNER_READ OWNER_WRITE) 57 | endfunction() 58 | 59 | 60 | if(WIN32) 61 | # w32-driver 62 | set(w32_sources "neoclip_w32.c" "neo_common.c") 63 | 64 | elseif(APPLE) 65 | enable_language(OBJC) 66 | set(CMAKE_OBJC_VISIBILITY_PRESET "internal") 67 | find_library(APPKIT_LIBRARIES AppKit REQUIRED) 68 | 69 | # mac-driver 70 | set(mac_sources "neoclip_mac.m" "neo_common.c") 71 | set(mac_libraries "${APPKIT_LIBRARIES}") 72 | 73 | elseif(UNIX) 74 | find_library(X11_LIBRARIES X11) 75 | find_package(Threads) 76 | # Extra CMake Modules 77 | find_package(ECM) 78 | if(ECM_FOUND) 79 | list(APPEND CMAKE_MODULE_PATH "${ECM_MODULE_PATH}") 80 | find_package(Wayland) 81 | find_package(WaylandScanner) 82 | endif() 83 | if(WaylandScanner_FOUND) 84 | ecm_add_wayland_client_protocol(wlr_data_control BASENAME "wlr-data-control" 85 | PROTOCOL "extra/wlr-data-control-unstable-v1.xml") 86 | endif() 87 | 88 | # x11-driver 89 | if(X11_LIBRARIES AND Threads_FOUND) 90 | set(x11_sources "neoclip_nix.c" "neo_x11.c" "neo_common.c") 91 | set(x11_libraries "${X11_LIBRARIES}" Threads::Threads) 92 | endif() 93 | 94 | # x11uv-driver 95 | if(X11_LIBRARIES) 96 | set(x11uv_sources "neoclip_nix.c" "neo_x11_uv.c" "neo_common.c") 97 | set(x11uv_libraries "${X11_LIBRARIES}") 98 | endif() 99 | 100 | # wl-driver 101 | if(Wayland_FOUND AND wlr_data_control AND Threads_FOUND) 102 | set(wl_sources "neoclip_nix.c" "neo_wayland.c" "neo_common.c" 103 | "${wlr_data_control}") 104 | set(wl_libraries "${Wayland_LIBRARIES}" Threads::Threads) 105 | set(wl_include_dirs "${CMAKE_CURRENT_BINARY_DIR}") 106 | endif() 107 | 108 | # wluv-driver 109 | if(Wayland_FOUND AND wlr_data_control) 110 | set(wluv_sources "neoclip_nix.c" "neo_wayland_uv.c" "neo_common.c" 111 | "${wlr_data_control}") 112 | set(wluv_libraries "${Wayland_LIBRARIES}") 113 | set(wluv_include_dirs "${CMAKE_CURRENT_BINARY_DIR}") 114 | endif() 115 | endif() 116 | 117 | foreach(t w32 mac x11 x11uv wl wluv) 118 | if(${t}_target AND ${t}_sources) 119 | message("Building `${t}-driver'") 120 | neo_module(${t}-driver SOURCES ${${t}_sources} LIBRARIES ${${t}_libraries} 121 | INCLUDE_DIRS ${${t}_include_dirs}) 122 | endif() 123 | endforeach() 124 | -------------------------------------------------------------------------------- /src/extra/wlr-data-control-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Simon Ser 5 | Copyright © 2019 Ivan Molodetskikh 6 | 7 | Permission to use, copy, modify, distribute, and sell this 8 | software and its documentation for any purpose is hereby granted 9 | without fee, provided that the above copyright notice appear in 10 | all copies and that both that copyright notice and this permission 11 | notice appear in supporting documentation, and that the name of 12 | the copyright holders not be used in advertising or publicity 13 | pertaining to distribution of the software without specific, 14 | written prior permission. The copyright holders make no 15 | representations about the suitability of this software for any 16 | purpose. It is provided "as is" without express or implied 17 | warranty. 18 | 19 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 20 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 21 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 23 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 24 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 25 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 26 | THIS SOFTWARE. 27 | 28 | 29 | 30 | This protocol allows a privileged client to control data devices. In 31 | particular, the client will be able to manage the current selection and take 32 | the role of a clipboard manager. 33 | 34 | Warning! The protocol described in this file is experimental and 35 | backward incompatible changes may be made. Backward compatible changes 36 | may be added together with the corresponding interface version bump. 37 | Backward incompatible changes are done by bumping the version number in 38 | the protocol and interface names and resetting the interface version. 39 | Once the protocol is to be declared stable, the 'z' prefix and the 40 | version number in the protocol and interface names are removed and the 41 | interface version number is reset. 42 | 43 | 44 | 45 | 46 | This interface is a manager that allows creating per-seat data device 47 | controls. 48 | 49 | 50 | 51 | 52 | Create a new data source. 53 | 54 | 56 | 57 | 58 | 59 | 60 | Create a data device that can be used to manage a seat's selection. 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | All objects created by the manager will still remain valid, until their 69 | appropriate destroy request has been called. 70 | 71 | 72 | 73 | 74 | 75 | 76 | This interface allows a client to manage a seat's selection. 77 | 78 | When the seat is destroyed, this object becomes inert. 79 | 80 | 81 | 82 | 83 | This request asks the compositor to set the selection to the data from 84 | the source on behalf of the client. 85 | 86 | The given source may not be used in any further set_selection or 87 | set_primary_selection requests. Attempting to use a previously used 88 | source is a protocol error. 89 | 90 | To unset the selection, set the source to NULL. 91 | 92 | 94 | 95 | 96 | 97 | 98 | Destroys the data device object. 99 | 100 | 101 | 102 | 103 | 104 | The data_offer event introduces a new wlr_data_control_offer object, 105 | which will subsequently be used in either the 106 | wlr_data_control_device.selection event (for the regular clipboard 107 | selections) or the wlr_data_control_device.primary_selection event (for 108 | the primary clipboard selections). Immediately following the 109 | wlr_data_control_device.data_offer event, the new data_offer object 110 | will send out wlr_data_control_offer.offer events to describe the MIME 111 | types it offers. 112 | 113 | 114 | 115 | 116 | 117 | 118 | The selection event is sent out to notify the client of a new 119 | wlr_data_control_offer for the selection for this device. The 120 | wlr_data_control_device.data_offer and the wlr_data_control_offer.offer 121 | events are sent out immediately before this event to introduce the data 122 | offer object. The selection event is sent to a client when a new 123 | selection is set. The wlr_data_control_offer is valid until a new 124 | wlr_data_control_offer or NULL is received. The client must destroy the 125 | previous selection wlr_data_control_offer, if any, upon receiving this 126 | event. 127 | 128 | The first selection event is sent upon binding the 129 | wlr_data_control_device object. 130 | 131 | 133 | 134 | 135 | 136 | 137 | This data control object is no longer valid and should be destroyed by 138 | the client. 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | The primary_selection event is sent out to notify the client of a new 147 | wlr_data_control_offer for the primary selection for this device. The 148 | wlr_data_control_device.data_offer and the wlr_data_control_offer.offer 149 | events are sent out immediately before this event to introduce the data 150 | offer object. The primary_selection event is sent to a client when a 151 | new primary selection is set. The wlr_data_control_offer is valid until 152 | a new wlr_data_control_offer or NULL is received. The client must 153 | destroy the previous primary selection wlr_data_control_offer, if any, 154 | upon receiving this event. 155 | 156 | If the compositor supports primary selection, the first 157 | primary_selection event is sent upon binding the 158 | wlr_data_control_device object. 159 | 160 | 162 | 163 | 164 | 165 | 166 | This request asks the compositor to set the primary selection to the 167 | data from the source on behalf of the client. 168 | 169 | The given source may not be used in any further set_selection or 170 | set_primary_selection requests. Attempting to use a previously used 171 | source is a protocol error. 172 | 173 | To unset the primary selection, set the source to NULL. 174 | 175 | The compositor will ignore this request if it does not support primary 176 | selection. 177 | 178 | 180 | 181 | 182 | 183 | 185 | 186 | 187 | 188 | 189 | 190 | The wlr_data_control_source object is the source side of a 191 | wlr_data_control_offer. It is created by the source client in a data 192 | transfer and provides a way to describe the offered data and a way to 193 | respond to requests to transfer the data. 194 | 195 | 196 | 197 | 199 | 200 | 201 | 202 | 203 | This request adds a MIME type to the set of MIME types advertised to 204 | targets. Can be called several times to offer multiple types. 205 | 206 | Calling this after wlr_data_control_device.set_selection is a protocol 207 | error. 208 | 209 | 211 | 212 | 213 | 214 | 215 | Destroys the data source object. 216 | 217 | 218 | 219 | 220 | 221 | Request for data from the client. Send the data as the specified MIME 222 | type over the passed file descriptor, then close it. 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | This data source is no longer valid. The data source has been replaced 231 | by another data source. 232 | 233 | The client should clean up and destroy this data source. 234 | 235 | 236 | 237 | 238 | 239 | 240 | A wlr_data_control_offer represents a piece of data offered for transfer 241 | by another client (the source client). The offer describes the different 242 | MIME types that the data can be converted to and provides the mechanism 243 | for transferring the data directly from the source client. 244 | 245 | 246 | 247 | 248 | To transfer the offered data, the client issues this request and 249 | indicates the MIME type it wants to receive. The transfer happens 250 | through the passed file descriptor (typically created with the pipe 251 | system call). The source client writes the data in the MIME type 252 | representation requested and then closes the file descriptor. 253 | 254 | The receiving client reads from the read end of the pipe until EOF and 255 | then closes its end, at which point the transfer is complete. 256 | 257 | This request may happen multiple times for different MIME types. 258 | 259 | 261 | 262 | 263 | 264 | 265 | 266 | Destroys the data offer object. 267 | 268 | 269 | 270 | 271 | 272 | Sent immediately after creating the wlr_data_control_offer object. 273 | One event per offered MIME type. 274 | 275 | 276 | 277 | 278 | 279 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | # 2 | # neoclip - Neovim clipboard provider 3 | # Last Change: 2024 Aug 23 4 | # License: https://unlicense.org 5 | # URL: https://github.com/matveyt/neoclip 6 | # 7 | 8 | 9 | project('neoclip', 'c', license : 'Unlicense', meson_version : '>=0.56.0', 10 | default_options : ['buildtype=release', 'c_std=c99', 'install_umask=0177', 11 | 'prefix=' + meson.project_source_root() / '..', 'strip=true', 'warning_level=3', 12 | 'werror=true']) 13 | 14 | 15 | # selectively enable module build 16 | w32_target = true 17 | mac_target = true 18 | x11_target = true 19 | x11uv_target = true 20 | wl_target = true 21 | wluv_target = true 22 | 23 | 24 | # Lua(JIT) is always required 25 | lua = dependency('luajit') 26 | 27 | if host_machine.system() == 'windows' 28 | # w32-driver 29 | w32_sources = ['neoclip_w32.c', 'neo_common.c'] 30 | 31 | elif host_machine.system() == 'darwin' 32 | add_languages('objc') 33 | appkit = dependency('AppKit', method : 'extraframework') 34 | 35 | # mac-driver 36 | mac_sources = ['neoclip_mac.m', 'neo_common.c'] 37 | mac_deps = [appkit] 38 | 39 | else # *nix 40 | x11 = dependency('X11', required : false) 41 | threads = dependency('threads', required : false) 42 | wl_client = dependency('wayland-client', required : false) 43 | wl_scanner = find_program('wayland-scanner', required : false, native : true) 44 | if wl_scanner.found() 45 | wlr_data_control_xml = 'extra/wlr-data-control-unstable-v1.xml' 46 | wlr_data_control = [ 47 | custom_target('wlr-data-control-c', input : wlr_data_control_xml, 48 | output : 'wayland-wlr-data-control-protocol.c', 49 | command : [wl_scanner, 'private-code', '@INPUT@', '@OUTPUT@']), 50 | custom_target('wlr-data-control-h', input : wlr_data_control_xml, 51 | output : 'wayland-wlr-data-control-client-protocol.h', 52 | command : [wl_scanner, 'client-header', '@INPUT@', '@OUTPUT@']), 53 | ] 54 | endif 55 | 56 | # x11-driver 57 | if x11.found() and threads.found() 58 | x11_sources = ['neoclip_nix.c', 'neo_x11.c', 'neo_common.c'] 59 | x11_deps = [x11, threads] 60 | endif 61 | 62 | # x11uv-driver 63 | if x11.found() 64 | x11uv_sources = ['neoclip_nix.c', 'neo_x11_uv.c', 'neo_common.c'] 65 | x11uv_deps = [x11] 66 | endif 67 | 68 | # wl-driver 69 | if wl_client.found() and is_variable('wlr_data_control') and threads.found() 70 | wl_sources = ['neoclip_nix.c', 'neo_wayland.c', 'neo_common.c', wlr_data_control] 71 | wl_deps = [wl_client, threads] 72 | endif 73 | 74 | # wluv-driver 75 | if wl_client.found() and is_variable('wlr_data_control') 76 | wluv_sources = ['neoclip_nix.c', 'neo_wayland_uv.c', 'neo_common.c', 77 | wlr_data_control] 78 | wluv_deps = [wl_client] 79 | endif 80 | endif 81 | 82 | foreach t : ['w32', 'mac', 'x11', 'x11uv', 'wl', 'wluv'] 83 | name = t + '-driver' 84 | sources = get_variable(t + '_sources', []) 85 | deps = get_variable(t + '_deps', []) + [lua] 86 | if get_variable(t + '_target', false) and sources != [] 87 | message('Building `@0@\''.format(name)) 88 | shared_module(name, sources, dependencies : deps, 89 | gnu_symbol_visibility : 'internal', install : true, install_dir : 'lua/neoclip', 90 | name_prefix : '', name_suffix : host_machine.system() == 'darwin' ? 'so' : []) 91 | endif 92 | endforeach 93 | -------------------------------------------------------------------------------- /src/neo_common.c: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 23 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #include "neoclip.h" 10 | 11 | 12 | // lua_CFunction(uv_module) => string 13 | int neo_id(lua_State* L) 14 | { 15 | // name = string.match(module_name, "(%w+)-") 16 | lua_getglobal(L, "string"); 17 | lua_getfield(L, -1, "match"); 18 | lua_pushvalue(L, uv_module); 19 | lua_pushliteral(L, "(%w+)-"); 20 | lua_call(L, 2, 1); 21 | 22 | // return "neoclip/" .. name 23 | lua_pushliteral(L, "neoclip/"); 24 | const char* name = lua_tostring(L, -2); 25 | if (name == NULL) 26 | lua_pushliteral(L, "Unknown"); 27 | else if (strcmp(name, "w32") == 0) 28 | lua_pushliteral(L, "WinAPI"); 29 | else if (strcmp(name, "mac") == 0) 30 | lua_pushliteral(L, "AppKit"); 31 | else if (strcmp(name, "wl") == 0) 32 | lua_pushliteral(L, "Wayland+pthreads"); 33 | else if (strcmp(name, "wluv") == 0) 34 | lua_pushliteral(L, "Wayland+luv"); 35 | else if (strcmp(name, "x11") == 0) 36 | lua_pushliteral(L, "X11+pthreads"); 37 | else if (strcmp(name, "x11uv") == 0) 38 | lua_pushliteral(L, "X11+luv"); 39 | else 40 | lua_pushvalue(L, -2); 41 | lua_concat(L, 2); 42 | return 1; 43 | } 44 | 45 | 46 | // table concatenation (numeric indices only) 47 | // returns string on Lua stack 48 | void neo_join(lua_State* L, int ix, const char* sep) 49 | { 50 | luaL_Buffer b; 51 | luaL_buffinit(L, &b); 52 | 53 | int n = lua_objlen(L, ix); 54 | if (n > 0) { 55 | for (int i = 1; i < n; ++i) { 56 | lua_rawgeti(L, ix, i); 57 | luaL_addvalue(&b); 58 | luaL_addstring(&b, sep); 59 | } 60 | lua_rawgeti(L, ix, n); 61 | luaL_addvalue(&b); 62 | } 63 | 64 | luaL_pushresult(&b); 65 | } 66 | 67 | 68 | // split UTF-8 string into lines (LF or CRLF) and save in table [lines, regtype] 69 | // chop invalid data, e.g. trailing zero in Windows Clipboard 70 | void neo_split(lua_State* L, int ix, const void* data, size_t cb, int type) 71 | { 72 | // accept negative index too 73 | ix = neo_absindex(L, ix); 74 | 75 | // validate input 76 | luaL_checktype(L, ix, LUA_TTABLE); 77 | if (data == NULL || cb < 1) 78 | return; 79 | 80 | // pb points to start of line 81 | const uint8_t* pb = data; 82 | // off + rest = size of remaining text 83 | size_t off = 0, rest = cb; 84 | // i is Lua table index (one-based) 85 | int i = 1; 86 | // state: -1 after CR; 0 normal; 1, 2, 3 skip continuation octets 87 | int state = 0; 88 | 89 | // lines table 90 | lua_newtable(L); 91 | 92 | do { 93 | int c = pb[off]; // get next octet 94 | 95 | if (state > 0) { // skip continuation octet(s) 96 | if (c < 0x80 || c >= 0xc0) 97 | break; // non-continuation octet 98 | --state; 99 | } else if (c == 0) { // NUL 100 | break; 101 | } else if (c == 10) { // LF or CRLF 102 | // push current line 103 | lua_pushlstring(L, (const char*)pb, off - (state < 0)); 104 | lua_rawseti(L, -2, i++); 105 | // adjust pb and off 106 | pb += off + 1; 107 | off = state = 0; 108 | continue; // don't increment off 109 | } else if (c == 13) { // have CR 110 | state = -1; 111 | } else if (c < 0x80) { // 7 bits code 112 | state = 0; 113 | } else if (c < 0xc0) { // unexpected continuation octet 114 | break; 115 | } else if (c < 0xe0) { // 11 bits code 116 | state = 1; 117 | } else if (c < 0xf0) { // 16 bits code 118 | state = 2; 119 | } else if (c < 0xf8) { // 21 bits code 120 | state = 3; 121 | } else // bad octet 122 | break; 123 | 124 | ++off; 125 | } while (--rest); 126 | 127 | // push last string w/o invalid rest 128 | lua_pushlstring(L, (const char*)pb, off - (state < 0)); 129 | lua_rawseti(L, -2, i); 130 | 131 | // save result 132 | lua_rawseti(L, ix, 1); 133 | lua_pushlstring(L, type == MCHAR ? "v" : type == MLINE ? "V" : 134 | type == MBLOCK ? "\026" : (state >= 0 && off > 0) ? "v" : "V", sizeof(char)); 135 | lua_rawseti(L, ix, 2); 136 | } 137 | 138 | 139 | #if 0 140 | // debug helpers 141 | // (L == NULL) => use previous lua_State 142 | static lua_State* LL = NULL; 143 | #define LL_SET(L) if ((L) != NULL) \ 144 | LL = (L); \ 145 | else if (LL != NULL) \ 146 | (L) = LL; \ 147 | else \ 148 | return 149 | 150 | void neo_inspect(lua_State* L, int ix) 151 | { 152 | LL_SET(L); 153 | ix = neo_absindex(L, ix); 154 | 155 | lua_getglobal(L, "print"); 156 | lua_getglobal(L, "vim"); 157 | lua_getfield(L, -1, "inspect"); 158 | lua_replace(L, -2); 159 | lua_pushvalue(L, ix); 160 | lua_call(L, 1, 1); 161 | lua_call(L, 1, 0); 162 | } 163 | 164 | 165 | void neo_printf(lua_State* L, const char* fmt, ...) 166 | { 167 | LL_SET(L); 168 | 169 | va_list argp; 170 | va_start(argp, fmt); 171 | 172 | lua_getglobal(L, "print"); 173 | lua_pushvfstring(L, fmt, argp); 174 | lua_call(L, 1, 0); 175 | 176 | va_end(argp); 177 | } 178 | #endif 179 | -------------------------------------------------------------------------------- /src/neo_wayland.c: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 23 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #include "neoclip_nix.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | 19 | // our listeners 20 | static void registry_global(void* X, struct wl_registry* registry, uint32_t name, 21 | const char* interface, uint32_t version); 22 | static void data_control_device_data_offer(void* X, 23 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer); 24 | static void data_control_device_primary_selection(void* X, 25 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer); 26 | static void data_control_device_selection(void* X, 27 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer); 28 | static void data_control_device_finished(void* X, 29 | struct zwlr_data_control_device_v1* dcd); 30 | static void data_control_offer_offer(void* X, struct zwlr_data_control_offer_v1* offer, 31 | const char* mime_type); 32 | static void data_control_source_send_prim(void* X, 33 | struct zwlr_data_control_source_v1* dcs, const char* mime_type, int fd); 34 | static void data_control_source_send_clip(void* X, 35 | struct zwlr_data_control_source_v1* dcs, const char* mime_type, int fd); 36 | static void data_control_source_cancelled(void* X, 37 | struct zwlr_data_control_source_v1* dcs); 38 | 39 | 40 | // simplify Wayland listeners declaration 41 | #define COUNTOF(o) (int)(sizeof(o) / sizeof(o[0])) 42 | #define INDEX(ix) INDEX_##ix 43 | #define LISTEN \ 44 | struct { \ 45 | const int count; \ 46 | void* pimpl; \ 47 | } _listen[] = 48 | #define OBJECT(o, ix) [ix] = { \ 49 | .count = sizeof(struct o##_listener) / sizeof(LISTENER_FUNC), \ 50 | .pimpl = &(struct o##_listener) 51 | 52 | 53 | enum { 54 | INDEX(registry), 55 | INDEX(device), 56 | INDEX(offer), 57 | INDEX(source_prim), 58 | INDEX(source_clip), 59 | }; 60 | typedef void (*LISTENER_FUNC)(void); 61 | 62 | 63 | static LISTEN { 64 | OBJECT(wl_registry, INDEX(registry)) { 65 | .global = registry_global, 66 | }}, 67 | OBJECT(zwlr_data_control_device_v1, INDEX(device)) { 68 | .data_offer = data_control_device_data_offer, 69 | .primary_selection = data_control_device_primary_selection, 70 | .selection = data_control_device_selection, 71 | .finished = data_control_device_finished, 72 | }}, 73 | OBJECT(zwlr_data_control_offer_v1, INDEX(offer)) { 74 | .offer = data_control_offer_offer, 75 | }}, 76 | OBJECT(zwlr_data_control_source_v1, INDEX(source_prim)) { 77 | .send = data_control_source_send_prim, 78 | .cancelled = data_control_source_cancelled, 79 | }}, 80 | OBJECT(zwlr_data_control_source_v1, INDEX(source_clip)) { 81 | .send = data_control_source_send_clip, 82 | .cancelled = data_control_source_cancelled, 83 | }}, 84 | }; 85 | 86 | 87 | // As Wayland segfaults on NULL listener we must provide stubs. Dirt! 88 | static void _nop(void) {} 89 | static inline void listen_init(void) 90 | { 91 | for (int i = 0; i < COUNTOF(_listen); ++i) 92 | for (int j = 0; j < _listen[i].count; ++j) 93 | if (((LISTENER_FUNC*)_listen[i].pimpl)[j] == NULL) 94 | ((LISTENER_FUNC*)_listen[i].pimpl)[j] = _nop; 95 | } 96 | 97 | 98 | static inline int listen_to(void* object, int i, void* data) 99 | { 100 | return wl_proxy_add_listener(object, _listen[i].pimpl, data); 101 | } 102 | 103 | 104 | // driver state 105 | struct neo_X { 106 | struct wl_display* d; // Wayland display 107 | struct wl_seat* seat; // Wayland seat 108 | struct zwlr_data_control_manager_v1* dcm; // wlroots data control manager 109 | struct zwlr_data_control_device_v1* dcd; // wlroots data control device 110 | uint8_t* data[sel_total]; // Selection: _VIMENC_TEXT 111 | size_t cb[sel_total]; // Selection: text size only 112 | pthread_mutex_t lock; // Mutex lock 113 | pthread_t tid; // Thread ID 114 | }; 115 | 116 | 117 | // supported mime types (from best to worst) 118 | static const char* mime[] = { 119 | [0] = "_VIMENC_TEXT", 120 | [1] = "_VIM_TEXT", 121 | "text/plain;charset=utf-8", 122 | "text/plain", 123 | "UTF8_STRING", 124 | "STRING", 125 | "TEXT", 126 | }; 127 | 128 | 129 | // forward prototypes 130 | static int neo__gc(lua_State* L); 131 | static bool neo_lock(neo_X* x); 132 | static bool neo_unlock(neo_X* x); 133 | static void* thread_main(void* X); 134 | static size_t alloc_data(neo_X* x, int sel, size_t cb); 135 | static void sel_read(neo_X* x, int sel, struct zwlr_data_control_offer_v1* offer); 136 | static void sel_write(neo_X* x, int sel, const char* mime_type, int fd); 137 | static void* offer_read(neo_X* x, struct zwlr_data_control_offer_v1* offer, 138 | const char* mime, size_t* pcb); 139 | 140 | 141 | // init state and start thread 142 | int neo_start(lua_State* L) 143 | { 144 | neo_X* x = neo_x(L); 145 | if (x == NULL) { 146 | // create new state 147 | x = lua_newuserdata(L, sizeof(neo_X)); 148 | 149 | // try to open display 150 | x->d = wl_display_connect(NULL); 151 | if (x->d == NULL) { 152 | lua_pushliteral(L, "wl_display_connect failed"); 153 | return lua_error(L); 154 | } 155 | listen_init(); 156 | 157 | // read globals from Wayland registry 158 | struct wl_registry* registry = wl_display_get_registry(x->d); 159 | listen_to(registry, INDEX(registry), x); 160 | x->seat = NULL, x->dcm = NULL; 161 | wl_display_roundtrip(x->d); 162 | wl_registry_destroy(registry); 163 | if (x->dcm == NULL) { 164 | wl_seat_release(x->seat); 165 | wl_display_disconnect(x->d); 166 | lua_pushliteral(L, "no support for wlr-data-control protocol"); 167 | return lua_error(L); 168 | } 169 | 170 | // listen for new offers on our data device 171 | x->dcd = zwlr_data_control_manager_v1_get_data_device(x->dcm, x->seat); 172 | listen_to(x->dcd, INDEX(device), x); 173 | 174 | // clear data 175 | for (int i = 0; i < sel_total; ++i) { 176 | x->data[i] = NULL; 177 | x->cb[i] = 0; 178 | } 179 | 180 | // metatable for state 181 | luaL_newmetatable(L, lua_tostring(L, uv_module)); 182 | neo_pushcfunction(L, neo__gc); 183 | lua_setfield(L, -2, "__gc"); 184 | lua_setmetatable(L, -2); 185 | 186 | // uv_share.x = x 187 | lua_setfield(L, uv_share, "x"); 188 | 189 | // start thread 190 | pthread_mutex_init(&x->lock, NULL); 191 | pthread_create(&x->tid, NULL, thread_main, x); 192 | } 193 | 194 | lua_pushnil(L); 195 | return 1; 196 | } 197 | 198 | 199 | // destroy state 200 | static int neo__gc(lua_State* L) 201 | { 202 | neo_X* x = (neo_X*)neo_checkud(L, 1); 203 | 204 | // clear data 205 | pthread_kill(x->tid, SIGTERM); 206 | pthread_join(x->tid, NULL); 207 | pthread_mutex_destroy(&x->lock); 208 | for (int i = 0; i < sel_total; ++i) 209 | free(x->data[i]); 210 | zwlr_data_control_device_v1_destroy(x->dcd); 211 | zwlr_data_control_manager_v1_destroy(x->dcm); 212 | wl_seat_release(x->seat); 213 | wl_display_disconnect(x->d); 214 | 215 | return 0; 216 | } 217 | 218 | 219 | // fetch new selection 220 | void neo_fetch(lua_State* L, int ix, int sel) 221 | { 222 | neo_X* x = neo_x(L); 223 | if (x != NULL && neo_lock(x)) { 224 | // wlr_data_control_device should've informed us of a new selection 225 | if (x->cb[sel] > 0) 226 | neo_split(L, ix, x->data[sel] + 1 + sizeof("utf-8"), x->cb[sel], 227 | x->data[sel][0]); 228 | 229 | // release lock 230 | neo_unlock(x); 231 | } 232 | } 233 | 234 | 235 | // own new selection 236 | // (cb == 0) => empty selection 237 | void neo_own(neo_X* x, bool offer, int sel, const void* ptr, size_t cb, int type) 238 | { 239 | if (neo_lock(x)) { 240 | // _VIMENC_TEXT: type 'encoding' NUL text 241 | cb = alloc_data(x, sel, cb); 242 | if (cb > 0) { 243 | x->data[sel][0] = type; 244 | memcpy(x->data[sel] + 1, "utf-8", sizeof("utf-8")); 245 | memcpy(x->data[sel] + 1 + sizeof("utf-8"), ptr, cb); 246 | } 247 | 248 | if (offer) { 249 | // offer our selection 250 | struct zwlr_data_control_source_v1* dcs = 251 | zwlr_data_control_manager_v1_create_data_source(x->dcm); 252 | for (int i = 0; i < COUNTOF(mime); ++i) 253 | zwlr_data_control_source_v1_offer(dcs, mime[i]); 254 | switch (sel) { 255 | case sel_prim: 256 | listen_to(dcs, INDEX(source_prim), x); 257 | zwlr_data_control_device_v1_set_primary_selection(x->dcd, dcs); 258 | break; 259 | case sel_clip: 260 | listen_to(dcs, INDEX(source_clip), x); 261 | zwlr_data_control_device_v1_set_selection(x->dcd, dcs); 262 | break; 263 | } 264 | wl_display_flush(x->d); 265 | // dispatch in the polling thread only! 266 | //wl_display_roundtrip(x->d); 267 | } 268 | 269 | neo_unlock(x); 270 | } 271 | } 272 | 273 | 274 | // lock selection data 275 | static inline bool neo_lock(neo_X* x) 276 | { 277 | return (pthread_mutex_lock(&x->lock) == 0); 278 | } 279 | 280 | 281 | // unlock selection data 282 | static inline bool neo_unlock(neo_X* x) 283 | { 284 | return (pthread_mutex_unlock(&x->lock) == 0); 285 | } 286 | 287 | 288 | // thread entry point 289 | static void* thread_main(void* X) 290 | { 291 | neo_X* x = (neo_X*)X; 292 | 293 | sigset_t mask; 294 | sigemptyset(&mask); 295 | sigaddset(&mask, SIGINT); 296 | sigaddset(&mask, SIGTERM); 297 | pthread_sigmask(SIG_BLOCK, &mask, NULL); 298 | 299 | struct pollfd fds[] = { 300 | { .fd = signalfd(-1, &mask, 0), .events = POLLIN, }, 301 | { .fd = wl_display_get_fd(x->d), .events = POLLIN, }, 302 | }; 303 | 304 | for (;;) { 305 | while (wl_display_prepare_read(x->d) != 0) 306 | wl_display_dispatch_pending(x->d); 307 | wl_display_flush(x->d); 308 | 309 | if (poll(fds, COUNTOF(fds), -1) < 0) 310 | break; 311 | 312 | if (fds[0].revents & POLLIN) { 313 | struct signalfd_siginfo ssi; 314 | size_t cb = read(fds[0].fd, &ssi, sizeof(ssi)); 315 | if (cb != sizeof(ssi) || ssi.ssi_signo == SIGINT || ssi.ssi_signo == SIGTERM) 316 | break; 317 | } 318 | 319 | if (fds[1].revents & POLLIN) 320 | wl_display_read_events(x->d); 321 | else 322 | wl_display_cancel_read(x->d); 323 | wl_display_dispatch_pending(x->d); 324 | } 325 | 326 | close(fds[0].fd); 327 | wl_display_cancel_read(x->d); 328 | return NULL; 329 | } 330 | 331 | 332 | // wl_registry::global 333 | static void registry_global(void* X, struct wl_registry* registry, uint32_t name, 334 | const char* interface, uint32_t version) 335 | { 336 | neo_X* x = (neo_X*)X; 337 | struct { 338 | void** pobject; 339 | const struct wl_interface* iface; 340 | } globl[] = { 341 | { (void**)&x->seat, &wl_seat_interface, }, 342 | { (void**)&x->dcm, &zwlr_data_control_manager_v1_interface, }, 343 | }; 344 | 345 | for (int i = 0; i < COUNTOF(globl); ++i) { 346 | if (strcmp(interface, globl[i].iface->name) == 0) { 347 | if (*globl[i].pobject == NULL) 348 | *globl[i].pobject = wl_registry_bind(registry, name, globl[i].iface, 349 | version); 350 | break; 351 | } 352 | } 353 | } 354 | 355 | 356 | // [z]wlr_data_control_device_[v1]::data_offer 357 | static void data_control_device_data_offer(void* X, 358 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer) 359 | { 360 | (void)X; // unused 361 | (void)dcd; // unused 362 | 363 | listen_to(offer, INDEX(offer), (void*)(intptr_t)COUNTOF(mime)); 364 | } 365 | 366 | 367 | // [z]wlr_data_control_device_[v1]::primary_selection 368 | static void data_control_device_primary_selection(void* X, 369 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer) 370 | { 371 | (void)dcd; // unused 372 | sel_read((neo_X*)X, sel_prim, offer); 373 | } 374 | 375 | 376 | // [z]wlr_data_control_device_[v1]::selection 377 | static void data_control_device_selection(void* X, 378 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer) 379 | { 380 | (void)dcd; // unused 381 | sel_read((neo_X*)X, sel_clip, offer); 382 | } 383 | 384 | 385 | // [z]wlr_data_control_device_[v1]::finished 386 | static void data_control_device_finished(void* X, 387 | struct zwlr_data_control_device_v1* dcd) 388 | { 389 | neo_X* x = (neo_X*)X; 390 | 391 | if (x->dcd == dcd) { 392 | x->dcd = zwlr_data_control_manager_v1_get_data_device(x->dcm, x->seat); 393 | listen_to(x->dcd, INDEX(device), x); 394 | } 395 | zwlr_data_control_device_v1_destroy(dcd); 396 | } 397 | 398 | 399 | // [z]wlr_data_control_offer_[v1]::offer 400 | static void data_control_offer_offer(void* X, struct zwlr_data_control_offer_v1* offer, 401 | const char* mime_type) 402 | { 403 | (void)X; // unused 404 | 405 | int best = (intptr_t)wl_proxy_get_user_data((struct wl_proxy*)offer); 406 | for (int i = 0; i < best; ++i) { 407 | if (strcmp(mime_type, mime[i]) == 0) { 408 | best = i; 409 | break; 410 | } 411 | } 412 | wl_proxy_set_user_data((struct wl_proxy*)offer, (void*)(intptr_t)best); 413 | } 414 | 415 | 416 | // [z]wlr_data_control_source_[v1]::send 417 | static void data_control_source_send_prim(void* X, 418 | struct zwlr_data_control_source_v1* dcs, const char* mime_type, int fd) 419 | { 420 | (void)dcs; // unused 421 | sel_write((neo_X*)X, sel_prim, mime_type, fd); 422 | } 423 | 424 | 425 | // [z]wlr_data_control_source_[v1]::send 426 | static void data_control_source_send_clip(void* X, 427 | struct zwlr_data_control_source_v1* dcs, const char* mime_type, int fd) 428 | { 429 | (void)dcs; // unused 430 | sel_write((neo_X*)X, sel_clip, mime_type, fd); 431 | } 432 | 433 | 434 | // [z]wlr_data_control_source_[v1]::cancelled 435 | static void data_control_source_cancelled(void* X, 436 | struct zwlr_data_control_source_v1* dcs) 437 | { 438 | (void)X; // unused 439 | zwlr_data_control_source_v1_destroy(dcs); 440 | } 441 | 442 | 443 | // (re-)allocate data buffer for selection 444 | // Note: caller must acquire neo_lock() first 445 | static size_t alloc_data(neo_X* x, int sel, size_t cb) 446 | { 447 | if (cb > 0) { 448 | void* ptr = realloc(x->data[sel], 1 + sizeof("utf-8") + cb); 449 | if (ptr != NULL) { 450 | x->data[sel] = ptr; 451 | x->cb[sel] = cb; 452 | } 453 | } else { 454 | free(x->data[sel]); 455 | x->data[sel] = NULL; 456 | x->cb[sel] = 0; 457 | } 458 | 459 | return x->cb[sel]; 460 | } 461 | 462 | 463 | // read selection data from offer 464 | static void sel_read(neo_X* x, int sel, struct zwlr_data_control_offer_v1* offer) 465 | { 466 | if (offer == NULL) { 467 | neo_own(x, false, sel, NULL, 0, 0); 468 | return; 469 | } 470 | 471 | int best_mime = (intptr_t)wl_proxy_get_user_data((struct wl_proxy*)offer); 472 | if (best_mime >= 0 && best_mime < COUNTOF(mime)) { 473 | size_t cb; 474 | uint8_t* ptr = offer_read(x, offer, mime[best_mime], &cb); 475 | int type = (cb > 0 && best_mime <= 1) ? ptr[0] : MAUTO; 476 | 477 | void* data = ptr; 478 | if (cb == 0) { 479 | // nothing to do 480 | } else if (best_mime == 0) { 481 | // _VIMENC_TEXT 482 | if (cb >= 1 + sizeof("utf-8") 483 | && memcmp(ptr + 1, "utf-8", sizeof("utf-8")) == 0) { 484 | // this is UTF-8 485 | data = ptr + 1 + sizeof("utf-8"); 486 | cb -= 1 + sizeof("utf-8"); 487 | } else { 488 | // Vim must have UTF8_STRING 489 | free(ptr); 490 | data = ptr = offer_read(x, offer, "UTF8_STRING", &cb); 491 | } 492 | } else if (best_mime == 1) { 493 | // _VIM_TEXT 494 | data = ptr + 1; 495 | --cb; 496 | } 497 | 498 | neo_own(x, false, sel, data, cb, type); 499 | free(ptr); 500 | } 501 | 502 | zwlr_data_control_offer_v1_destroy(offer); 503 | } 504 | 505 | 506 | // write selection data to file descriptor 507 | static void sel_write(neo_X* x, int sel, const char* mime_type, int fd) 508 | { 509 | if (neo_lock(x)) { 510 | // assume _VIMENC_TEXT 511 | uint8_t* buf = x->data[sel]; 512 | size_t cb = 1 + sizeof("utf-8") + x->cb[sel]; 513 | ssize_t n = 1; 514 | 515 | // not _VIMENC_TEXT? 516 | if (strcmp(mime_type, mime[0]) != 0) { 517 | // _VIM_TEXT: output type 518 | if (strcmp(mime_type, mime[1]) == 0) 519 | n = write(fd, buf, 1); 520 | 521 | // skip over header 522 | buf += 1 + sizeof("utf-8"); 523 | cb -= 1 + sizeof("utf-8"); 524 | } 525 | 526 | // output selection 527 | if (n > 0) 528 | n = write(fd, buf, cb); 529 | neo_unlock(x); 530 | } 531 | 532 | close(fd); 533 | } 534 | 535 | 536 | // read specific mime type from offer 537 | static void* offer_read(neo_X* x, struct zwlr_data_control_offer_v1* offer, 538 | const char* mime, size_t* pcb) 539 | { 540 | uint8_t* ptr = NULL; 541 | size_t total = 0; 542 | int fds[2]; 543 | 544 | if (pipe(fds) == 0) { 545 | zwlr_data_control_offer_v1_receive(offer, mime, fds[1]); 546 | wl_display_roundtrip(x->d); 547 | close(fds[1]); 548 | 549 | void* buf = malloc(64 * 1024); 550 | if (buf != NULL) { 551 | ssize_t part; 552 | while ((part = read(fds[0], buf, 64 * 1024)) > 0) { 553 | void* ptr2 = realloc(ptr, total + part); 554 | if (ptr2 == NULL) 555 | break; 556 | ptr = ptr2; 557 | memcpy(ptr + total, buf, part); 558 | total += part; 559 | } 560 | free(buf); 561 | } 562 | close(fds[0]); 563 | } 564 | 565 | *pcb = total; 566 | return ptr; 567 | } 568 | -------------------------------------------------------------------------------- /src/neo_wayland_uv.c: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 23 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #include "neoclip_nix.h" 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | // our listeners 16 | static void registry_global(void* X, struct wl_registry* registry, uint32_t name, 17 | const char* interface, uint32_t version); 18 | static void data_control_device_data_offer(void* X, 19 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer); 20 | static void data_control_device_primary_selection(void* X, 21 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer); 22 | static void data_control_device_selection(void* X, 23 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer); 24 | static void data_control_device_finished(void* X, 25 | struct zwlr_data_control_device_v1* dcd); 26 | static void data_control_offer_offer(void* X, struct zwlr_data_control_offer_v1* offer, 27 | const char* mime_type); 28 | static void data_control_source_send_prim(void* X, 29 | struct zwlr_data_control_source_v1* dcs, const char* mime_type, int fd); 30 | static void data_control_source_send_clip(void* X, 31 | struct zwlr_data_control_source_v1* dcs, const char* mime_type, int fd); 32 | static void data_control_source_cancelled(void* X, 33 | struct zwlr_data_control_source_v1* dcs); 34 | 35 | 36 | // simplify Wayland listeners declaration 37 | #define COUNTOF(o) (int)(sizeof(o) / sizeof(o[0])) 38 | #define INDEX(ix) INDEX_##ix 39 | #define LISTEN \ 40 | struct { \ 41 | const int count; \ 42 | void* pimpl; \ 43 | } _listen[] = 44 | #define OBJECT(o, ix) [ix] = { \ 45 | .count = sizeof(struct o##_listener) / sizeof(LISTENER_FUNC), \ 46 | .pimpl = &(struct o##_listener) 47 | 48 | 49 | enum { 50 | INDEX(registry), 51 | INDEX(device), 52 | INDEX(offer), 53 | INDEX(source_prim), 54 | INDEX(source_clip), 55 | }; 56 | typedef void (*LISTENER_FUNC)(void); 57 | 58 | 59 | static LISTEN { 60 | OBJECT(wl_registry, INDEX(registry)) { 61 | .global = registry_global, 62 | }}, 63 | OBJECT(zwlr_data_control_device_v1, INDEX(device)) { 64 | .data_offer = data_control_device_data_offer, 65 | .primary_selection = data_control_device_primary_selection, 66 | .selection = data_control_device_selection, 67 | .finished = data_control_device_finished, 68 | }}, 69 | OBJECT(zwlr_data_control_offer_v1, INDEX(offer)) { 70 | .offer = data_control_offer_offer, 71 | }}, 72 | OBJECT(zwlr_data_control_source_v1, INDEX(source_prim)) { 73 | .send = data_control_source_send_prim, 74 | .cancelled = data_control_source_cancelled, 75 | }}, 76 | OBJECT(zwlr_data_control_source_v1, INDEX(source_clip)) { 77 | .send = data_control_source_send_clip, 78 | .cancelled = data_control_source_cancelled, 79 | }}, 80 | }; 81 | 82 | 83 | // As Wayland segfaults on NULL listener we must provide stubs. Dirt! 84 | static void _nop(void) {} 85 | static inline void listen_init(void) 86 | { 87 | for (int i = 0; i < COUNTOF(_listen); ++i) 88 | for (int j = 0; j < _listen[i].count; ++j) 89 | if (((LISTENER_FUNC*)_listen[i].pimpl)[j] == NULL) 90 | ((LISTENER_FUNC*)_listen[i].pimpl)[j] = _nop; 91 | } 92 | 93 | 94 | static inline int listen_to(void* object, int i, void* data) 95 | { 96 | return wl_proxy_add_listener(object, _listen[i].pimpl, data); 97 | } 98 | 99 | 100 | // driver state 101 | struct neo_X { 102 | struct wl_display* d; // Wayland display 103 | struct wl_seat* seat; // Wayland seat 104 | struct zwlr_data_control_manager_v1* dcm; // wlroots data control manager 105 | struct zwlr_data_control_device_v1* dcd; // wlroots data control device 106 | uint8_t* data[sel_total]; // Selection: _VIMENC_TEXT 107 | size_t cb[sel_total]; // Selection: text size only 108 | }; 109 | 110 | 111 | // supported mime types (from best to worst) 112 | static const char* mime[] = { 113 | [0] = "_VIMENC_TEXT", 114 | [1] = "_VIM_TEXT", 115 | "text/plain;charset=utf-8", 116 | "text/plain", 117 | "UTF8_STRING", 118 | "STRING", 119 | "TEXT", 120 | }; 121 | 122 | 123 | // forward prototypes 124 | static int neo__gc(lua_State* L); 125 | static int cb_prepare(lua_State* L); 126 | static int cb_poll(lua_State* L); 127 | static size_t alloc_data(neo_X* x, int sel, size_t cb); 128 | static void sel_read(neo_X* x, int sel, struct zwlr_data_control_offer_v1* offer); 129 | static void sel_write(neo_X* x, int sel, const char* mime_type, int fd); 130 | static void* offer_read(neo_X* x, struct zwlr_data_control_offer_v1* offer, 131 | const char* mime, size_t* pcb); 132 | 133 | 134 | // init state and start thread 135 | int neo_start(lua_State* L) 136 | { 137 | neo_X* x = neo_x(L); 138 | if (x == NULL) { 139 | // create new state 140 | x = lua_newuserdata(L, sizeof(neo_X)); 141 | 142 | // try to open display 143 | x->d = wl_display_connect(NULL); 144 | if (x->d == NULL) { 145 | lua_pushliteral(L, "wl_display_connect failed"); 146 | return lua_error(L); 147 | } 148 | listen_init(); 149 | 150 | // read globals from Wayland registry 151 | struct wl_registry* registry = wl_display_get_registry(x->d); 152 | listen_to(registry, INDEX(registry), x); 153 | x->seat = NULL, x->dcm = NULL; 154 | wl_display_roundtrip(x->d); 155 | wl_registry_destroy(registry); 156 | if (x->dcm == NULL) { 157 | wl_seat_release(x->seat); 158 | wl_display_disconnect(x->d); 159 | lua_pushliteral(L, "no support for wlr-data-control protocol"); 160 | return lua_error(L); 161 | } 162 | 163 | // listen for new offers on our data device 164 | x->dcd = zwlr_data_control_manager_v1_get_data_device(x->dcm, x->seat); 165 | listen_to(x->dcd, INDEX(device), x); 166 | 167 | // clear data 168 | for (int i = 0; i < sel_total; ++i) { 169 | x->data[i] = NULL; 170 | x->cb[i] = 0; 171 | } 172 | 173 | // metatable for state 174 | luaL_newmetatable(L, lua_tostring(L, uv_module)); 175 | neo_pushcfunction(L, neo__gc); 176 | lua_setfield(L, -2, "__gc"); 177 | lua_setmetatable(L, -2); 178 | 179 | // uv_share.x = x 180 | lua_setfield(L, uv_share, "x"); 181 | 182 | // start polling the display 183 | lua_getglobal(L, "vim"); // vim.uv or vim.loop => stack 184 | lua_getfield(L, -1, "uv"); 185 | if (lua_isnil(L, -1)) { 186 | lua_pop(L, 1); 187 | lua_getfield(L, -1, "loop"); 188 | } 189 | lua_replace(L, -2); 190 | 191 | // local prepare = uv.new_prepare() 192 | lua_getfield(L, -1, "new_prepare"); 193 | lua_call(L, 0, 1); // prepare => stack 194 | // uv.prepare_start(prepare, cb_prepare) 195 | lua_getfield(L, -2, "prepare_start"); 196 | lua_pushvalue(L, -2); 197 | neo_pushcfunction(L, cb_prepare); 198 | lua_call(L, 2, 0); 199 | 200 | // local poll = uv.new_poll(wl_display_get_fd(x->d)) 201 | lua_getfield(L, -2, "new_poll"); 202 | lua_pushinteger(L, wl_display_get_fd(x->d)); 203 | lua_call(L, 1, 1); // poll => stack 204 | // uv.poll_start(poll, "rw", cb_poll) 205 | lua_getfield(L, -3, "poll_start"); 206 | lua_pushvalue(L, -2); 207 | lua_pushliteral(L, "rw"); 208 | neo_pushcfunction(L, cb_poll); 209 | lua_call(L, 3, 0); 210 | 211 | // uv_share.poll = poll 212 | lua_setfield(L, uv_share, "poll"); // poll <= stack 213 | // uv_share.prepare = prepare 214 | lua_setfield(L, uv_share, "prepare"); // prepare <= stack 215 | // uv_share.uv = vim.uv or vim.loop 216 | lua_setfield(L, uv_share, "uv"); // vim.uv or vim.loop <= stack 217 | } 218 | 219 | lua_pushnil(L); 220 | return 1; 221 | } 222 | 223 | 224 | // destroy state 225 | static int neo__gc(lua_State* L) 226 | { 227 | neo_X* x = (neo_X*)neo_checkud(L, 1); 228 | 229 | lua_getfield(L, uv_share, "uv"); // uv or loop => stack 230 | // uv.poll_stop(uv_share.poll) 231 | lua_getfield(L, -1, "poll_stop"); 232 | lua_getfield(L, uv_share, "poll"); 233 | if (lua_isnil(L, -1)) { 234 | lua_pop(L, 2); 235 | } else { 236 | lua_call(L, 1, 0); 237 | // uv.close(poll) 238 | lua_getfield(L, -1, "close"); 239 | lua_getfield(L, uv_share, "poll"); 240 | lua_call(L, 1, 0); 241 | // uv_share.poll = nil 242 | lua_pushnil(L); 243 | lua_setfield(L, uv_share, "poll"); 244 | } 245 | 246 | // uv.prepare_stop(prepare) 247 | lua_getfield(L, -1, "prepare_stop"); 248 | lua_getfield(L, uv_share, "prepare"); 249 | if (lua_isnil(L, -1)) { 250 | lua_pop(L, 2); 251 | } else { 252 | lua_call(L, 1, 0); 253 | // uv.close(prepare) 254 | lua_getfield(L, -1, "close"); 255 | lua_getfield(L, uv_share, "prepare"); 256 | lua_call(L, 1, 0); 257 | // uv_share.prepare = nil 258 | lua_pushnil(L); 259 | lua_setfield(L, uv_share, "prepare"); 260 | } 261 | 262 | // clear data 263 | for (int i = 0; i < sel_total; ++i) 264 | free(x->data[i]); 265 | zwlr_data_control_device_v1_destroy(x->dcd); 266 | zwlr_data_control_manager_v1_destroy(x->dcm); 267 | wl_seat_release(x->seat); 268 | wl_display_disconnect(x->d); 269 | 270 | return 0; 271 | } 272 | 273 | 274 | // fetch new selection 275 | void neo_fetch(lua_State* L, int ix, int sel) 276 | { 277 | neo_X* x = neo_x(L); 278 | if (x != NULL) { 279 | // wlr_data_control_device should've informed us of a new selection 280 | if (x->cb[sel] > 0) 281 | neo_split(L, ix, x->data[sel] + 1 + sizeof("utf-8"), x->cb[sel], 282 | x->data[sel][0]); 283 | } 284 | } 285 | 286 | 287 | // own new selection 288 | // (cb == 0) => empty selection 289 | void neo_own(neo_X* x, bool offer, int sel, const void* ptr, size_t cb, int type) 290 | { 291 | // _VIMENC_TEXT: type 'encoding' NUL text 292 | cb = alloc_data(x, sel, cb); 293 | if (cb > 0) { 294 | x->data[sel][0] = type; 295 | memcpy(x->data[sel] + 1, "utf-8", sizeof("utf-8")); 296 | memcpy(x->data[sel] + 1 + sizeof("utf-8"), ptr, cb); 297 | } 298 | 299 | if (offer) { 300 | // offer our selection 301 | struct zwlr_data_control_source_v1* dcs = 302 | zwlr_data_control_manager_v1_create_data_source(x->dcm); 303 | for (int i = 0; i < COUNTOF(mime); ++i) 304 | zwlr_data_control_source_v1_offer(dcs, mime[i]); 305 | switch (sel) { 306 | case sel_prim: 307 | listen_to(dcs, INDEX(source_prim), x); 308 | zwlr_data_control_device_v1_set_primary_selection(x->dcd, dcs); 309 | break; 310 | case sel_clip: 311 | listen_to(dcs, INDEX(source_clip), x); 312 | zwlr_data_control_device_v1_set_selection(x->dcd, dcs); 313 | break; 314 | } 315 | wl_display_flush(x->d); 316 | wl_display_roundtrip(x->d); 317 | } 318 | } 319 | 320 | 321 | // uv_prepare_t callback 322 | static int cb_prepare(lua_State* L) 323 | { 324 | neo_X* x = neo_x(L); 325 | if (x != NULL) { 326 | while (wl_display_prepare_read(x->d) != 0) 327 | wl_display_dispatch_pending(x->d); 328 | wl_display_flush(x->d); 329 | } 330 | 331 | return 0; 332 | } 333 | 334 | 335 | // uv_poll_t callback 336 | static int cb_poll(lua_State* L) 337 | { 338 | neo_X* x = neo_x(L); 339 | if (x != NULL) { 340 | if (lua_isnil(L, 1) && strchr(lua_tostring(L, 2), 'r') != NULL) 341 | wl_display_read_events(x->d); 342 | else 343 | wl_display_cancel_read(x->d); 344 | wl_display_dispatch_pending(x->d); 345 | } 346 | 347 | return 0; 348 | } 349 | 350 | 351 | // wl_registry::global 352 | static void registry_global(void* X, struct wl_registry* registry, uint32_t name, 353 | const char* interface, uint32_t version) 354 | { 355 | neo_X* x = (neo_X*)X; 356 | struct { 357 | void** pobject; 358 | const struct wl_interface* iface; 359 | } globl[] = { 360 | { (void**)&x->seat, &wl_seat_interface, }, 361 | { (void**)&x->dcm, &zwlr_data_control_manager_v1_interface, }, 362 | }; 363 | 364 | for (int i = 0; i < COUNTOF(globl); ++i) { 365 | if (strcmp(interface, globl[i].iface->name) == 0) { 366 | if (*globl[i].pobject == NULL) 367 | *globl[i].pobject = wl_registry_bind(registry, name, globl[i].iface, 368 | version); 369 | break; 370 | } 371 | } 372 | } 373 | 374 | 375 | // [z]wlr_data_control_device_[v1]::data_offer 376 | static void data_control_device_data_offer(void* X, 377 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer) 378 | { 379 | (void)X; // unused 380 | (void)dcd; // unused 381 | 382 | listen_to(offer, INDEX(offer), (void*)(intptr_t)COUNTOF(mime)); 383 | } 384 | 385 | 386 | // [z]wlr_data_control_device_[v1]::primary_selection 387 | static void data_control_device_primary_selection(void* X, 388 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer) 389 | { 390 | (void)dcd; // unused 391 | sel_read((neo_X*)X, sel_prim, offer); 392 | } 393 | 394 | 395 | // [z]wlr_data_control_device_[v1]::selection 396 | static void data_control_device_selection(void* X, 397 | struct zwlr_data_control_device_v1* dcd, struct zwlr_data_control_offer_v1* offer) 398 | { 399 | (void)dcd; // unused 400 | sel_read((neo_X*)X, sel_clip, offer); 401 | } 402 | 403 | 404 | // [z]wlr_data_control_device_[v1]::finished 405 | static void data_control_device_finished(void* X, 406 | struct zwlr_data_control_device_v1* dcd) 407 | { 408 | neo_X* x = (neo_X*)X; 409 | 410 | if (x->dcd == dcd) { 411 | x->dcd = zwlr_data_control_manager_v1_get_data_device(x->dcm, x->seat); 412 | listen_to(x->dcd, INDEX(device), x); 413 | } 414 | zwlr_data_control_device_v1_destroy(dcd); 415 | } 416 | 417 | 418 | // [z]wlr_data_control_offer_[v1]::offer 419 | static void data_control_offer_offer(void* X, struct zwlr_data_control_offer_v1* offer, 420 | const char* mime_type) 421 | { 422 | (void)X; // unused 423 | 424 | int best = (intptr_t)wl_proxy_get_user_data((struct wl_proxy*)offer); 425 | for (int i = 0; i < best; ++i) { 426 | if (strcmp(mime_type, mime[i]) == 0) { 427 | best = i; 428 | break; 429 | } 430 | } 431 | wl_proxy_set_user_data((struct wl_proxy*)offer, (void*)(intptr_t)best); 432 | } 433 | 434 | 435 | // [z]wlr_data_control_source_[v1]::send 436 | static void data_control_source_send_prim(void* X, 437 | struct zwlr_data_control_source_v1* dcs, const char* mime_type, int fd) 438 | { 439 | (void)dcs; // unused 440 | sel_write((neo_X*)X, sel_prim, mime_type, fd); 441 | } 442 | 443 | 444 | // [z]wlr_data_control_source_[v1]::send 445 | static void data_control_source_send_clip(void* X, 446 | struct zwlr_data_control_source_v1* dcs, const char* mime_type, int fd) 447 | { 448 | (void)dcs; // unused 449 | sel_write((neo_X*)X, sel_clip, mime_type, fd); 450 | } 451 | 452 | 453 | // [z]wlr_data_control_source_[v1]::cancelled 454 | static void data_control_source_cancelled(void* X, 455 | struct zwlr_data_control_source_v1* dcs) 456 | { 457 | (void)X; // unused 458 | zwlr_data_control_source_v1_destroy(dcs); 459 | } 460 | 461 | 462 | // (re-)allocate data buffer for selection 463 | static size_t alloc_data(neo_X* x, int sel, size_t cb) 464 | { 465 | if (cb > 0) { 466 | void* ptr = realloc(x->data[sel], 1 + sizeof("utf-8") + cb); 467 | if (ptr != NULL) { 468 | x->data[sel] = ptr; 469 | x->cb[sel] = cb; 470 | } 471 | } else { 472 | free(x->data[sel]); 473 | x->data[sel] = NULL; 474 | x->cb[sel] = 0; 475 | } 476 | 477 | return x->cb[sel]; 478 | } 479 | 480 | 481 | // read selection data from offer 482 | static void sel_read(neo_X* x, int sel, struct zwlr_data_control_offer_v1* offer) 483 | { 484 | if (offer == NULL) { 485 | neo_own(x, false, sel, NULL, 0, 0); 486 | return; 487 | } 488 | 489 | int best_mime = (intptr_t)wl_proxy_get_user_data((struct wl_proxy*)offer); 490 | if (best_mime >= 0 && best_mime < COUNTOF(mime)) { 491 | size_t cb; 492 | uint8_t* ptr = offer_read(x, offer, mime[best_mime], &cb); 493 | int type = (cb > 0 && best_mime <= 1) ? ptr[0] : MAUTO; 494 | 495 | void* data = ptr; 496 | if (cb == 0) { 497 | // nothing to do 498 | } else if (best_mime == 0) { 499 | // _VIMENC_TEXT 500 | if (cb >= 1 + sizeof("utf-8") 501 | && memcmp(ptr + 1, "utf-8", sizeof("utf-8")) == 0) { 502 | // this is UTF-8 503 | data = ptr + 1 + sizeof("utf-8"); 504 | cb -= 1 + sizeof("utf-8"); 505 | } else { 506 | // Vim must have UTF8_STRING 507 | free(ptr); 508 | data = ptr = offer_read(x, offer, "UTF8_STRING", &cb); 509 | } 510 | } else if (best_mime == 1) { 511 | // _VIM_TEXT 512 | data = ptr + 1; 513 | --cb; 514 | } 515 | 516 | neo_own(x, false, sel, data, cb, type); 517 | free(ptr); 518 | } 519 | 520 | zwlr_data_control_offer_v1_destroy(offer); 521 | } 522 | 523 | 524 | // write selection data to file descriptor 525 | static void sel_write(neo_X* x, int sel, const char* mime_type, int fd) 526 | { 527 | // assume _VIMENC_TEXT 528 | uint8_t* buf = x->data[sel]; 529 | size_t cb = 1 + sizeof("utf-8") + x->cb[sel]; 530 | ssize_t n = 1; 531 | 532 | // not _VIMENC_TEXT? 533 | if (strcmp(mime_type, mime[0]) != 0) { 534 | // _VIM_TEXT: output type 535 | if (strcmp(mime_type, mime[1]) == 0) 536 | n = write(fd, buf, 1); 537 | 538 | // skip over header 539 | buf += 1 + sizeof("utf-8"); 540 | cb -= 1 + sizeof("utf-8"); 541 | } 542 | 543 | // output selection 544 | if (n > 0) 545 | n = write(fd, buf, cb); 546 | 547 | close(fd); 548 | } 549 | 550 | 551 | // read specific mime type from offer 552 | static void* offer_read(neo_X* x, struct zwlr_data_control_offer_v1* offer, 553 | const char* mime, size_t* pcb) 554 | { 555 | uint8_t* ptr = NULL; 556 | size_t total = 0; 557 | int fds[2]; 558 | 559 | if (pipe(fds) == 0) { 560 | zwlr_data_control_offer_v1_receive(offer, mime, fds[1]); 561 | wl_display_roundtrip(x->d); 562 | close(fds[1]); 563 | 564 | void* buf = malloc(64 * 1024); 565 | if (buf != NULL) { 566 | ssize_t part; 567 | while ((part = read(fds[0], buf, 64 * 1024)) > 0) { 568 | void* ptr2 = realloc(ptr, total + part); 569 | if (ptr2 == NULL) 570 | break; 571 | ptr = ptr2; 572 | memcpy(ptr + total, buf, part); 573 | total += part; 574 | } 575 | free(buf); 576 | } 577 | close(fds[0]); 578 | } 579 | 580 | *pcb = total; 581 | return ptr; 582 | } 583 | -------------------------------------------------------------------------------- /src/neo_x11.c: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 23 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #include "neoclip_nix.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | // X11 atoms 18 | enum { 19 | // sel_prim, // PRIMARY 20 | // sel_sec, // SECONDARY 21 | // sel_clip, // CLIPBOARD 22 | atom = sel_total, // ATOM 23 | atom_pair, // ATOM_PAIR 24 | clipman, // CLIPBOARD_MANAGER 25 | incr, // INCR 26 | integer, // INTEGER 27 | null, // NULL 28 | wm_proto, // WM_PROTOCOLS 29 | wm_dele, // WM_DELETE_WINDOW 30 | neo_ready, // NEO_READY 31 | neo_offer, // NEO_OFFER 32 | // supported targets 33 | targets, // TARGETS 34 | dele, // DELETE 35 | multi, // MULTIPLE 36 | save, // SAVE_TARGETS 37 | timestamp, // TIMESTAMP 38 | // encodings from best to worst 39 | vimenc, // _VIMENC_TEXT 40 | vimtext, // _VIM_TEXT 41 | plain_utf8, // text/plain;charset=utf-8 42 | utf8_string, // UTF8_STRING 43 | plain, // text/plain 44 | compound, // COMPOUND_TEXT 45 | string, // STRING 46 | text, // TEXT 47 | // total count 48 | total 49 | }; 50 | 51 | 52 | // driver state 53 | struct neo_X { 54 | Display* d; // X Display 55 | Window w; // X Window 56 | Time delta; // X server startup time (ms from Unix epoch) 57 | Atom atom[total]; // X Atoms list 58 | uint8_t* data[sel_total]; // Selection: _VIMENC_TEXT 59 | size_t cb[sel_total]; // Selection: text size only 60 | Time stamp[sel_total]; // Selection: time stamp 61 | pthread_cond_t c_rdy[sel_total]; // Selection: "ready" condition 62 | bool f_rdy[sel_total]; // Selection: "ready" flag 63 | pthread_mutex_t lock; // Mutex lock 64 | pthread_t tid; // Thread ID 65 | }; 66 | 67 | 68 | // forward prototypes 69 | static int neo__gc(lua_State* L); 70 | static bool neo_lock(neo_X* x); 71 | static bool neo_unlock(neo_X* x); 72 | static bool neo_signal(neo_X* x, int sel); 73 | static void* thread_main(void* X); 74 | static bool on_sel_notify(neo_X* x, XSelectionEvent* xse); 75 | static void on_sel_request(neo_X* x, XSelectionRequestEvent* xsre); 76 | static bool on_client_message(neo_X* X, XClientMessageEvent* xcme); 77 | static size_t alloc_data(neo_X* x, int sel, size_t cb); 78 | static int atom2sel(neo_X* x, Atom atom); 79 | static Atom best_target(neo_X* x, Atom* atom, int count); 80 | static void client_message(neo_X* x, int message, int param); 81 | static Bool is_incr_notify(Display* d, XEvent* xe, XPointer arg); 82 | static Time time_diff(Time ref); 83 | static void to_multiple(neo_X* x, int sel, XSelectionEvent* xse); 84 | static void to_property(neo_X* x, int sel, Window w, Atom property, Atom type); 85 | 86 | 87 | // init state and start thread 88 | int neo_start(lua_State* L) 89 | { 90 | neo_X* x = neo_x(L); 91 | if (x == NULL) { 92 | // initialize X threads (required for xcb) 93 | if (!neo_did(L, "XInitThreads")) { 94 | if (XInitThreads() == False) { 95 | lua_pushliteral(L, "XInitThreads failed"); 96 | return lua_error(L); 97 | } 98 | } 99 | 100 | // create new state 101 | x = lua_newuserdata(L, sizeof(neo_X)); 102 | 103 | // try to open display 104 | x->d = XOpenDisplay(NULL); 105 | if (x->d == NULL) { 106 | lua_pushliteral(L, "XOpenDisplay failed"); 107 | return lua_error(L); 108 | } 109 | 110 | // atom names 111 | static /*const*/ char* atom_name[total] = { 112 | [sel_prim] = "PRIMARY", 113 | [sel_sec] = "SECONDARY", 114 | [sel_clip] = "CLIPBOARD", 115 | [atom] = "ATOM", 116 | [atom_pair] = "ATOM_PAIR", 117 | [clipman] = "CLIPBOARD_MANAGER", 118 | [incr] = "INCR", 119 | [integer] = "INTEGER", 120 | [null] = "NULL", 121 | [wm_proto] = "WM_PROTOCOLS", 122 | [wm_dele] = "WM_DELETE_WINDOW", 123 | [neo_ready] = "NEO_READY", 124 | [neo_offer] = "NEO_OFFER", 125 | [targets] = "TARGETS", 126 | [dele] = "DELETE", 127 | [multi] = "MULTIPLE", 128 | [save] = "SAVE_TARGETS", 129 | [timestamp] = "TIMESTAMP", 130 | [vimenc] = "_VIMENC_TEXT", 131 | [vimtext] = "_VIM_TEXT", 132 | [plain_utf8] = "text/plain;charset=utf-8", 133 | [utf8_string] = "UTF8_STRING", 134 | [plain] = "text/plain", 135 | [compound] = "COMPOUND_TEXT", 136 | [string] = "STRING", 137 | [text] = "TEXT", 138 | }; 139 | 140 | // init state 141 | x->w = XCreateSimpleWindow(x->d, XDefaultRootWindow(x->d), 0, 0, 1, 1, 0, 0, 0); 142 | x->delta = CurrentTime; 143 | XInternAtoms(x->d, atom_name, total, False, x->atom); 144 | XSetWMProtocols(x->d, x->w, &x->atom[wm_dele], 1); 145 | for (int i = 0; i < sel_total; ++i) { 146 | x->data[i] = NULL; 147 | x->cb[i] = 0; 148 | x->stamp[i] = CurrentTime; 149 | pthread_cond_init(&x->c_rdy[i], NULL); 150 | x->f_rdy[i] = false; 151 | } 152 | 153 | // metatable for state 154 | luaL_newmetatable(L, lua_tostring(L, uv_module)); 155 | neo_pushcfunction(L, neo__gc); 156 | lua_setfield(L, -2, "__gc"); 157 | lua_setmetatable(L, -2); 158 | 159 | // uv_share.x = x 160 | lua_setfield(L, uv_share, "x"); 161 | 162 | // start thread 163 | pthread_mutex_init(&x->lock, NULL); 164 | pthread_create(&x->tid, NULL, thread_main, x); 165 | } 166 | 167 | lua_pushnil(L); 168 | return 1; 169 | } 170 | 171 | 172 | // destroy state 173 | static int neo__gc(lua_State* L) 174 | { 175 | neo_X* x = (neo_X*)neo_checkud(L, 1); 176 | 177 | // clear data 178 | client_message(x, wm_proto, wm_dele); 179 | pthread_join(x->tid, NULL); 180 | pthread_mutex_destroy(&x->lock); 181 | for (int i = 0; i < sel_total; ++i) { 182 | pthread_cond_destroy(&x->c_rdy[i]); 183 | free(x->data[i]); 184 | } 185 | XDestroyWindow(x->d, x->w); 186 | XCloseDisplay(x->d); 187 | 188 | return 0; 189 | } 190 | 191 | 192 | // fetch new selection 193 | void neo_fetch(lua_State* L, int ix, int sel) 194 | { 195 | neo_X* x = neo_x(L); 196 | if (x != NULL && neo_lock(x)) { 197 | // send request 198 | x->f_rdy[sel] = false; 199 | client_message(x, neo_ready, sel); 200 | 201 | // wait upto 1 second 202 | struct timespec t; 203 | if (clock_gettime(CLOCK_REALTIME, &t) == 0) { 204 | ++t.tv_sec; 205 | while (!x->f_rdy[sel] 206 | && pthread_cond_timedwait(&x->c_rdy[sel], &x->lock, &t) == 0) 207 | /*nothing*/; 208 | } 209 | 210 | // split selection into t[ix] 211 | if (x->f_rdy[sel] && x->cb[sel] > 0) 212 | neo_split(L, ix, x->data[sel] + 1 + sizeof("utf-8"), x->cb[sel], 213 | x->data[sel][0]); 214 | 215 | // release lock 216 | neo_unlock(x); 217 | } 218 | } 219 | 220 | 221 | // own new selection 222 | // (cb == 0) => empty selection 223 | void neo_own(neo_X* x, bool offer, int sel, const void* ptr, size_t cb, int type) 224 | { 225 | if (neo_lock(x)) { 226 | // _VIMENC_TEXT: type 'encoding' NUL text 227 | cb = alloc_data(x, sel, cb); 228 | if (cb > 0) { 229 | x->data[sel][0] = type; 230 | memcpy(x->data[sel] + 1, "utf-8", sizeof("utf-8")); 231 | memcpy(x->data[sel] + 1 + sizeof("utf-8"), ptr, cb); 232 | } 233 | x->stamp[sel] = time_diff(x->delta); 234 | 235 | if (offer) 236 | client_message(x, neo_offer, sel); 237 | else 238 | neo_signal(x, sel); 239 | 240 | neo_unlock(x); 241 | } 242 | } 243 | 244 | 245 | // lock selection data 246 | static inline bool neo_lock(neo_X* x) 247 | { 248 | return (pthread_mutex_lock(&x->lock) == 0); 249 | } 250 | 251 | 252 | // unlock selection data 253 | static inline bool neo_unlock(neo_X* x) 254 | { 255 | return (pthread_mutex_unlock(&x->lock) == 0); 256 | } 257 | 258 | 259 | // signal data ready 260 | static inline bool neo_signal(neo_X* x, int sel) 261 | { 262 | x->f_rdy[sel] = true; 263 | return (pthread_cond_signal(&x->c_rdy[sel]) == 0); 264 | } 265 | 266 | 267 | // thread entry point 268 | static void* thread_main(void* X) 269 | { 270 | neo_X* x = (neo_X*)X; 271 | 272 | // force property change to get timestamp from X server 273 | XSelectInput(x->d, x->w, PropertyChangeMask); 274 | XChangeProperty(x->d, x->w, x->atom[timestamp], x->atom[timestamp], 32, 275 | PropModeAppend, NULL, 0); 276 | 277 | bool stop = false; 278 | do { 279 | XEvent xe; 280 | XNextEvent(x->d, &xe); 281 | switch (xe.type) { 282 | case PropertyNotify: 283 | if (xe.xproperty.atom == x->atom[timestamp]) { 284 | x->delta = time_diff(xe.xproperty.time); 285 | XSelectInput(x->d, x->w, NoEventMask); 286 | } 287 | break; 288 | case SelectionClear: 289 | if (xe.xselectionclear.window == x->w && neo_lock(x)) { 290 | alloc_data(x, atom2sel(x, xe.xselectionclear.selection), 0); 291 | neo_unlock(x); 292 | } 293 | break; 294 | case SelectionNotify: 295 | stop = on_sel_notify(x, &xe.xselection); 296 | break; 297 | case SelectionRequest: 298 | on_sel_request(x, &xe.xselectionrequest); 299 | break; 300 | case ClientMessage: 301 | stop = on_client_message(x, &xe.xclient); 302 | break; 303 | } 304 | } while (!stop); 305 | 306 | return NULL; 307 | } 308 | 309 | 310 | // SelectionNotify event handler 311 | static bool on_sel_notify(neo_X* x, XSelectionEvent* xse) 312 | { 313 | int sel = atom2sel(x, xse->selection); 314 | 315 | if (xse->property == x->atom[neo_ready]) { 316 | // read our property 317 | Atom type = None; 318 | uint8_t* ptr = NULL; 319 | unsigned char* xptr = NULL; 320 | unsigned long cxptr = 0; 321 | XGetWindowProperty(x->d, x->w, x->atom[neo_ready], 0, LONG_MAX, True, 322 | AnyPropertyType, &type, &(int){0}, &cxptr, &(unsigned long){0}, &xptr); 323 | 324 | do { 325 | uint8_t* buf = (uint8_t*)xptr; 326 | size_t cb = cxptr; 327 | 328 | if (type == x->atom[incr]) { 329 | // INCR 330 | for (cb = 0; ; cb += cxptr) { 331 | XFree(xptr); 332 | XIfEvent(x->d, &(XEvent){0}, is_incr_notify, (XPointer)xse); 333 | XGetWindowProperty(x->d, x->w, x->atom[neo_ready], 0, LONG_MAX, 334 | True, AnyPropertyType, &(Atom){None}, &(int){0}, &cxptr, 335 | &(unsigned long){0}, &xptr); 336 | void* ptr2 = cxptr ? realloc(ptr, cb + cxptr) : NULL; 337 | if (ptr2 == NULL) 338 | break; 339 | ptr = ptr2; 340 | memcpy(ptr + cb, xptr, cxptr); 341 | } 342 | type = xse->target; 343 | buf = ptr; 344 | } 345 | 346 | if (cb == 0) { 347 | // nothing to do 348 | } else if (type == x->atom[atom] || type == x->atom[targets]) { 349 | // TARGETS 350 | Atom target = best_target(x, (Atom*)buf, (int)cb); 351 | if (target != None) { 352 | XConvertSelection(x->d, xse->selection, target, x->atom[neo_ready], 353 | x->w, xse->time); 354 | break; 355 | } 356 | } else if (type == x->atom[vimenc]) { 357 | // _VIMENC_TEXT 358 | if (cb >= 1 + sizeof("utf-8") 359 | && memcmp(buf + 1, "utf-8", sizeof("utf-8")) == 0) { 360 | // this is UTF-8 361 | neo_own(x, false, sel, buf + 1 + sizeof("utf-8"), 362 | cb - 1 - sizeof("utf-8"), buf[0]); 363 | } else { 364 | // no UTF-8; ask then for UTF8_STRING 365 | XConvertSelection(x->d, xse->selection, x->atom[utf8_string], 366 | x->atom[neo_ready], x->w, xse->time); 367 | } 368 | break; 369 | } else if (type == x->atom[vimtext]) { 370 | // _VIM_TEXT: assume UTF-8 371 | neo_own(x, false, sel, buf + 1, cb - 1, buf[0]); 372 | break; 373 | } else if (type == x->atom[plain_utf8] || type == x->atom[utf8_string] 374 | || type == x->atom[plain]) { 375 | // no conversion 376 | neo_own(x, false, sel, buf, cb, MAUTO); 377 | break; 378 | } else if (type == x->atom[compound] || type == x->atom[string] 379 | || type == x->atom[text]) { 380 | // COMPOUND_TEXT, STRING or TEXT: attempt to convert to UTF-8 381 | XTextProperty xtp = { 382 | .value = buf, 383 | .encoding = type, 384 | .format = 8, 385 | .nitems = cb, 386 | }; 387 | char** list; 388 | if (Xutf8TextPropertyToTextList(x->d, &xtp, &list, &(int){0}) 389 | == Success) { 390 | neo_own(x, false, sel, list[0], strlen(list[0]), MAUTO); 391 | XFreeStringList(list); 392 | break; 393 | } 394 | } 395 | 396 | // conversion failed 397 | neo_own(x, false, sel, NULL, 0, 0); 398 | } while (0); 399 | 400 | free(ptr); 401 | if (xptr != NULL) 402 | XFree(xptr); 403 | } else if (xse->property == None) { 404 | // exit upon SAVE_TARGETS: anyone supporting this? 405 | if (xse->target == x->atom[save]) 406 | return true; 407 | // peer error 408 | neo_own(x, false, sel, NULL, 0, 0); 409 | } 410 | 411 | return false; 412 | } 413 | 414 | 415 | // SelectionRequest event handler 416 | static void on_sel_request(neo_X* x, XSelectionRequestEvent* xsre) 417 | { 418 | // prepare SelectionNotify 419 | XSelectionEvent xse = { 420 | .type = SelectionNotify, 421 | .display = x->d, 422 | .requestor = xsre->requestor, 423 | .selection = xsre->selection, 424 | .target = xsre->target, 425 | .property = xsre->property ? xsre->property : xsre->target, 426 | .time = time_diff(x->delta), 427 | }; 428 | 429 | if (neo_lock(x)) { 430 | int sel = atom2sel(x, xsre->selection); 431 | 432 | // TARGETS: DELETE, MULTIPLE, SAVE_TARGETS, TIMESTAMP, _VIMENC_TEXT, _VIM_TEXT, 433 | // UTF8_STRING, COMPOUND_TEXT, STRING, TEXT 434 | if (xsre->owner != x->w || (xsre->time != CurrentTime && 435 | xsre->time < x->stamp[sel])) { 436 | // refuse non-matching request 437 | xse.property = None; 438 | } else if (xsre->target == x->atom[targets]) { 439 | // response is ATOM 440 | XChangeProperty(x->d, xse.requestor, xse.property, x->atom[atom], 32, 441 | PropModeReplace, (unsigned char*)&x->atom[targets], total - targets); 442 | } else if (xsre->target == x->atom[dele]) { 443 | // response is NULL 444 | alloc_data(x, sel, 0); 445 | XChangeProperty(x->d, xse.requestor, xse.property, x->atom[null], 32, 446 | PropModeReplace, NULL, 0); 447 | } else if (xsre->target == x->atom[save]) { 448 | // response is NULL 449 | XChangeProperty(x->d, xse.requestor, xse.property, x->atom[null], 32, 450 | PropModeReplace, NULL, 0); 451 | } else if (xsre->target == x->atom[multi]) { 452 | // response is ATOM_PAIR 453 | to_multiple(x, sel, &xse); 454 | } else if (xsre->target == x->atom[timestamp]) { 455 | // response is INTEGER 456 | XChangeProperty(x->d, xse.requestor, xse.property, x->atom[integer], 32, 457 | PropModeReplace, (unsigned char*)&x->stamp[sel], 1); 458 | } else if (best_target(x, &xsre->target, 1) != None) { 459 | // attempt to convert 460 | to_property(x, sel, xse.requestor, xse.property, xsre->target); 461 | } else { 462 | // unknown target 463 | xse.property = None; 464 | } 465 | neo_unlock(x); 466 | } else 467 | xse.property = None; 468 | 469 | // send SelectionNotify 470 | XSendEvent(x->d, xse.requestor, True, NoEventMask, (XEvent*)&xse); 471 | } 472 | 473 | 474 | // ClientMessage event handler 475 | static bool on_client_message(neo_X* x, XClientMessageEvent* xcme) 476 | { 477 | Atom param = (Atom)xcme->data.l[0]; 478 | int sel = atom2sel(x, param); 479 | 480 | if (xcme->message_type == x->atom[neo_ready]) { 481 | // NEO_READY: fetch system selection 482 | Window owner = XGetSelectionOwner(x->d, param); 483 | if (owner == x->w) { 484 | // no conversion needed 485 | if (neo_lock(x)) { 486 | neo_signal(x, sel); 487 | neo_unlock(x); 488 | } 489 | } else if (owner == None) { 490 | // empty selection 491 | neo_own(x, false, sel, NULL, 0, 0); 492 | } else { 493 | // what TARGETS are supported? 494 | XConvertSelection(x->d, param, x->atom[targets], x->atom[neo_ready], x->w, 495 | (Time)xcme->data.l[1]); 496 | } 497 | } else if (xcme->message_type == x->atom[neo_offer]) { 498 | // NEO_OFFER: offer our selection 499 | XSetSelectionOwner(x->d, param, x->w, x->stamp[sel]); 500 | } else if (xcme->message_type == x->atom[wm_proto] && param == x->atom[wm_dele]) { 501 | // WM_DELETE_WINDOW 502 | for (int i = 0; i < sel_total; ++i) { 503 | if (XGetSelectionOwner(x->d, x->atom[i]) == x->w) { 504 | // ask CLIPBOARD_MANAGER to SAVE_TARGETS first 505 | XConvertSelection(x->d, x->atom[clipman], x->atom[save], None, x->w, 506 | (Time)xcme->data.l[1]); 507 | return false; 508 | } 509 | } 510 | // stop by WM_DELETE_WINDOW 511 | return true; 512 | } 513 | 514 | return false; 515 | } 516 | 517 | 518 | // (re-)allocate data buffer for selection 519 | // Note: caller must acquire neo_lock() first 520 | static size_t alloc_data(neo_X* x, int sel, size_t cb) 521 | { 522 | if (cb > 0) { 523 | void* ptr = realloc(x->data[sel], 1 + sizeof("utf-8") + cb); 524 | if (ptr != NULL) { 525 | x->data[sel] = ptr; 526 | x->cb[sel] = cb; 527 | } 528 | } else { 529 | free(x->data[sel]); 530 | x->data[sel] = NULL; 531 | x->cb[sel] = 0; 532 | } 533 | 534 | return x->cb[sel]; 535 | } 536 | 537 | 538 | // Atom => selection index 539 | static int atom2sel(neo_X* x, Atom atom) 540 | { 541 | for (int i = 0; i < sel_total; ++i) 542 | if (atom == x->atom[i]) 543 | return i; 544 | 545 | return sel_clip; 546 | } 547 | 548 | 549 | // get best matching target atom 550 | static Atom best_target(neo_X* x, Atom* atom, int count) 551 | { 552 | int best = total; 553 | 554 | for (int i = 0; i < count && best > vimenc; ++i) 555 | for (int j = vimenc; j < best; ++j) 556 | if (atom[i] == x->atom[j]) { 557 | best = j; 558 | break; 559 | } 560 | 561 | return (best < total) ? x->atom[best] : None; 562 | } 563 | 564 | 565 | // send ClientMessage to our thread 566 | static void client_message(neo_X* x, int message, int param) 567 | { 568 | XClientMessageEvent xcme = { 569 | .type = ClientMessage, 570 | .display = x->d, 571 | .window = x->w, 572 | .message_type = x->atom[message], 573 | .format = 32, 574 | .data = { 575 | .l = { 576 | [0] = x->atom[param], 577 | [1] = time_diff(x->delta), 578 | }, 579 | }, 580 | }; 581 | XSendEvent(x->d, x->w, False, NoEventMask, (XEvent*)&xcme); 582 | XFlush(x->d); 583 | } 584 | 585 | 586 | // X11 predicate function: PropertyNotify/PropertyNewValue 587 | static Bool is_incr_notify(Display* d, XEvent* xe, XPointer arg) 588 | { 589 | (void)d; // unused 590 | if (xe->type != PropertyNotify) 591 | return False; 592 | 593 | XPropertyEvent* xpe = &xe->xproperty; 594 | XSelectionEvent* xse = (XSelectionEvent*)arg; 595 | 596 | return (xpe->window == xse->requestor && xpe->atom == xse->property 597 | && xpe->time >= xse->time && xpe->state == PropertyNewValue); 598 | } 599 | 600 | 601 | // get ms difference from reference time 602 | static Time time_diff(Time ref) 603 | { 604 | struct timespec t; 605 | 606 | if (ref == CurrentTime || clock_gettime(CLOCK_MONOTONIC, &t) < 0) 607 | return CurrentTime; 608 | 609 | return (t.tv_sec * 1000 + t.tv_nsec / 1000000) - ref; 610 | } 611 | 612 | 613 | // process MULTIPLE selection requests 614 | static void to_multiple(neo_X* x, int sel, XSelectionEvent* xse) 615 | { 616 | Atom* tgt = NULL; 617 | unsigned long ul_tgt = 0; 618 | XGetWindowProperty(x->d, xse->requestor, xse->property, 0, LONG_MAX, False, 619 | x->atom[atom_pair], &(Atom){None}, &(int){0}, &ul_tgt, &(unsigned long){0}, 620 | (unsigned char**)&tgt); 621 | int i_tgt = (long)ul_tgt; 622 | 623 | for (int i = 0; i < i_tgt; i += 2) 624 | if (best_target(x, &tgt[i], 1) != None && tgt[i + 1] != None) 625 | to_property(x, sel, xse->requestor, tgt[i + 1], tgt[i]); 626 | else 627 | tgt[i + 1] = None; 628 | 629 | if (i_tgt > 0) { 630 | XChangeProperty(x->d, xse->requestor, xse->property, x->atom[atom_pair], 32, 631 | PropModeReplace, (unsigned char*)tgt, i_tgt); 632 | XFree(tgt); 633 | } 634 | } 635 | 636 | 637 | // put selection data into window property 638 | static void to_property(neo_X* x, int sel, Window w, Atom property, Atom type) 639 | { 640 | if (x->cb[sel] == 0) { 641 | XDeleteProperty(x->d, w, property); 642 | return; 643 | } 644 | 645 | XTextProperty xtp = { 646 | .value = (unsigned char*)x->data[sel], 647 | .encoding = type, 648 | .format = 8, 649 | .nitems = (unsigned long)x->cb[sel], 650 | }; 651 | unsigned char* ptr = NULL; 652 | unsigned char* xptr = NULL; 653 | 654 | if (type == x->atom[vimenc]) { 655 | // _VIMENC_TEXT: type 'encoding' NUL text 656 | xtp.nitems += 1 + sizeof("utf-8"); 657 | } else if (type == x->atom[vimtext]) { 658 | // _VIM_TEXT: type text 659 | ptr = malloc(1 + xtp.nitems); 660 | if (ptr != NULL) { 661 | ptr[0] = xtp.value[0]; 662 | memcpy(ptr + 1, xtp.value + 1 + sizeof("utf-8"), xtp.nitems); 663 | xtp.value = ptr; 664 | ++xtp.nitems; 665 | } 666 | } else { 667 | // skip header 668 | xtp.value += 1 + sizeof("utf-8"); 669 | } 670 | 671 | // Vim-alike behaviour: STRING == UTF8_STRING, TEXT == COMPOUND_TEXT 672 | if (type == x->atom[compound] || type == x->atom[text]) { 673 | // convert UTF-8 to COMPOUND_TEXT 674 | ptr = malloc(xtp.nitems + 1); 675 | if (ptr != NULL) { 676 | memcpy(ptr, xtp.value, xtp.nitems); 677 | ptr[xtp.nitems] = 0; 678 | Xutf8TextListToTextProperty(x->d, (char**)&ptr, 1, XCompoundTextStyle, &xtp); 679 | xptr = xtp.value; 680 | } 681 | } 682 | 683 | // set property 684 | XChangeProperty(x->d, w, property, type, xtp.format, PropModeReplace, xtp.value, 685 | (int)xtp.nitems); 686 | 687 | // free memory 688 | free(ptr); 689 | if (xptr != NULL) 690 | XFree(xptr); 691 | } 692 | -------------------------------------------------------------------------------- /src/neo_x11_uv.c: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 23 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #include "neoclip_nix.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | // X11 Atoms 17 | enum { 18 | // sel_prim, // PRIMARY 19 | // sel_sec, // SECONDARY 20 | // sel_clip, // CLIPBOARD 21 | atom = sel_total, // ATOM 22 | atom_pair, // ATOM_PAIR 23 | incr, // INCR 24 | integer, // INTEGER 25 | null, // NULL 26 | neo_ready, // NEO_READY 27 | // supported targets 28 | targets, // TARGETS 29 | dele, // DELETE 30 | multi, // MULTIPLE 31 | timestamp, // TIMESTAMP 32 | // encodings from best to worst 33 | vimenc, // _VIMENC_TEXT 34 | vimtext, // _VIM_TEXT 35 | plain_utf8, // text/plain;charset=utf-8 36 | utf8_string, // UTF8_STRING 37 | plain, // text/plain 38 | compound, // COMPOUND_TEXT 39 | string, // STRING 40 | text, // TEXT 41 | // total count 42 | total 43 | }; 44 | 45 | 46 | // driver state 47 | struct neo_X { 48 | Display* d; // X Display 49 | Window w; // X Window 50 | Time delta; // X server startup time (ms from Unix epoch) 51 | Atom atom[total]; // X Atoms list 52 | uint8_t* data[sel_total]; // Selection: _VIMENC_TEXT 53 | size_t cb[sel_total]; // Selection: text size only 54 | Time stamp[sel_total]; // Selection: time stamp 55 | bool f_rdy[sel_total]; // Selection: "ready" flag 56 | }; 57 | 58 | 59 | // forward prototypes 60 | static int neo__gc(lua_State* L); 61 | static int cb_prepare(lua_State* L); 62 | static int cb_poll(lua_State* L); 63 | static void dispatch_event(neo_X* x, XEvent* xe); 64 | static void on_sel_notify(neo_X* x, XSelectionEvent* xse); 65 | static void on_sel_request(neo_X* x, XSelectionRequestEvent* xsre); 66 | static size_t alloc_data(neo_X* x, int sel, size_t cb); 67 | static int atom2sel(neo_X* x, Atom atom); 68 | static Atom best_target(neo_X* x, Atom* atom, int count); 69 | static Bool is_incr_notify(Display* d, XEvent* xe, XPointer arg); 70 | static void modal_loop(lua_State* L, bool* stop, uint32_t timeout); 71 | static Time time_diff(Time ref); 72 | static void to_multiple(neo_X* x, int sel, XSelectionEvent* xse); 73 | static void to_property(neo_X* x, int sel, Window w, Atom property, Atom type); 74 | 75 | 76 | // init state and start thread 77 | int neo_start(lua_State* L) 78 | { 79 | neo_X* x = neo_x(L); 80 | if (x == NULL) { 81 | #if 0 82 | // initialize X threads (required for xcb) 83 | if (!neo_did(L, "XInitThreads")) { 84 | if (XInitThreads() == False) { 85 | lua_pushliteral(L, "XInitThreads failed"); 86 | return lua_error(L); 87 | } 88 | } 89 | #endif 90 | 91 | // create new state 92 | x = lua_newuserdata(L, sizeof(neo_X)); 93 | 94 | // try to open display 95 | x->d = XOpenDisplay(NULL); 96 | if (x->d == NULL) { 97 | lua_pushliteral(L, "XOpenDisplay failed"); 98 | return lua_error(L); 99 | } 100 | 101 | // atom names 102 | static /*const*/ char* atom_name[total] = { 103 | [sel_prim] = "PRIMARY", 104 | [sel_sec] = "SECONDARY", 105 | [sel_clip] = "CLIPBOARD", 106 | [atom] = "ATOM", 107 | [atom_pair] = "ATOM_PAIR", 108 | [incr] = "INCR", 109 | [integer] = "INTEGER", 110 | [null] = "NULL", 111 | [neo_ready] = "NEO_READY", 112 | [targets] = "TARGETS", 113 | [dele] = "DELETE", 114 | [multi] = "MULTIPLE", 115 | [timestamp] = "TIMESTAMP", 116 | [vimenc] = "_VIMENC_TEXT", 117 | [vimtext] = "_VIM_TEXT", 118 | [plain_utf8] = "text/plain;charset=utf-8", 119 | [utf8_string] = "UTF8_STRING", 120 | [plain] = "text/plain", 121 | [compound] = "COMPOUND_TEXT", 122 | [string] = "STRING", 123 | [text] = "TEXT", 124 | }; 125 | 126 | // init state 127 | x->w = XCreateSimpleWindow(x->d, XDefaultRootWindow(x->d), 0, 0, 1, 1, 0, 0, 0); 128 | x->delta = CurrentTime; 129 | XInternAtoms(x->d, atom_name, total, False, x->atom); 130 | for (int i = 0; i < sel_total; ++i) { 131 | x->data[i] = NULL; 132 | x->cb[i] = 0; 133 | x->stamp[i] = CurrentTime; 134 | x->f_rdy[i] = false; 135 | } 136 | 137 | // metatable for state 138 | luaL_newmetatable(L, lua_tostring(L, uv_module)); 139 | neo_pushcfunction(L, neo__gc); 140 | lua_setfield(L, -2, "__gc"); 141 | lua_setmetatable(L, -2); 142 | 143 | // uv_share.x = x 144 | lua_setfield(L, uv_share, "x"); 145 | 146 | // start polling the display 147 | lua_getglobal(L, "vim"); // vim.uv or vim.loop => stack 148 | lua_getfield(L, -1, "uv"); 149 | if (lua_isnil(L, -1)) { 150 | lua_pop(L, 1); 151 | lua_getfield(L, -1, "loop"); 152 | } 153 | lua_replace(L, -2); 154 | 155 | // local prepare = uv.new_prepare() 156 | lua_getfield(L, -1, "new_prepare"); 157 | lua_call(L, 0, 1); // prepare => stack 158 | // uv.prepare_start(prepare, cb_prepare) 159 | lua_getfield(L, -2, "prepare_start"); 160 | lua_pushvalue(L, -2); 161 | neo_pushcfunction(L, cb_prepare); 162 | lua_call(L, 2, 0); 163 | 164 | // local poll = uv.new_poll(XConnectionNumber(x->d)) 165 | lua_getfield(L, -2, "new_poll"); 166 | lua_pushinteger(L, XConnectionNumber(x->d)); 167 | lua_call(L, 1, 1); // poll => stack 168 | // uv.poll_start(poll, "r", cb_poll) 169 | lua_getfield(L, -3, "poll_start"); 170 | lua_pushvalue(L, -2); 171 | lua_pushliteral(L, "r"); 172 | neo_pushcfunction(L, cb_poll); 173 | lua_call(L, 3, 0); 174 | 175 | // uv_share.poll = poll 176 | lua_setfield(L, uv_share, "poll"); // poll <= stack 177 | // uv_share.prepare = prepare 178 | lua_setfield(L, uv_share, "prepare"); // prepare <= stack 179 | // uv_share.uv = vim.uv or vim.loop 180 | lua_setfield(L, uv_share, "uv"); // uv or loop <= stack 181 | 182 | // force property change to get timestamp from X server 183 | XSelectInput(x->d, x->w, PropertyChangeMask); 184 | XChangeProperty(x->d, x->w, x->atom[timestamp], x->atom[timestamp], 32, 185 | PropModeAppend, NULL, 0); 186 | } 187 | 188 | lua_pushnil(L); 189 | return 1; 190 | } 191 | 192 | 193 | // destroy state 194 | static int neo__gc(lua_State* L) 195 | { 196 | neo_X* x = (neo_X*)neo_checkud(L, 1); 197 | 198 | lua_getfield(L, uv_share, "uv"); // uv or loop => stack 199 | // uv.poll_stop(uv_share.poll) 200 | lua_getfield(L, -1, "poll_stop"); 201 | lua_getfield(L, uv_share, "poll"); 202 | if (lua_isnil(L, -1)) { 203 | lua_pop(L, 2); 204 | } else { 205 | lua_call(L, 1, 0); 206 | // uv.close(poll) 207 | lua_getfield(L, -1, "close"); 208 | lua_getfield(L, uv_share, "poll"); 209 | lua_call(L, 1, 0); 210 | // uv_share.poll = nil 211 | lua_pushnil(L); 212 | lua_setfield(L, uv_share, "poll"); 213 | } 214 | 215 | // uv.prepare_stop(prepare) 216 | lua_getfield(L, -1, "prepare_stop"); 217 | lua_getfield(L, uv_share, "prepare"); 218 | if (lua_isnil(L, -1)) { 219 | lua_pop(L, 2); 220 | } else { 221 | lua_call(L, 1, 0); 222 | // uv.close(prepare) 223 | lua_getfield(L, -1, "close"); 224 | lua_getfield(L, uv_share, "prepare"); 225 | lua_call(L, 1, 0); 226 | // uv_share.prepare = nil 227 | lua_pushnil(L); 228 | lua_setfield(L, uv_share, "prepare"); 229 | } 230 | 231 | // clear data 232 | for (int i = 0; i < sel_total; ++i) 233 | free(x->data[i]); 234 | XDestroyWindow(x->d, x->w); 235 | XCloseDisplay(x->d); 236 | 237 | return 0; 238 | } 239 | 240 | 241 | // fetch new selection 242 | void neo_fetch(lua_State* L, int ix, int sel) 243 | { 244 | neo_X* x = neo_x(L); 245 | if (x != NULL) { 246 | // attempt to convert selection 247 | Window owner = XGetSelectionOwner(x->d, x->atom[sel]); 248 | if (owner == x->w) { 249 | // no conversion needed 250 | x->f_rdy[sel] = true; 251 | } else if (owner == None) { 252 | // empty selection 253 | neo_own(x, false, sel, NULL, 0, 0); 254 | } else { 255 | // what TARGETS are supported? 256 | x->f_rdy[sel] = false; 257 | XConvertSelection(x->d, x->atom[sel], x->atom[targets], x->atom[neo_ready], 258 | x->w, time_diff(x->delta)); 259 | modal_loop(L, &x->f_rdy[sel], 1000); 260 | } 261 | 262 | // split selection into t[ix] 263 | if (x->f_rdy[sel] && x->cb[sel] > 0) 264 | neo_split(L, ix, x->data[sel] + 1 + sizeof("utf-8"), x->cb[sel], 265 | x->data[sel][0]); 266 | } 267 | } 268 | 269 | 270 | // own new selection 271 | // (cb == 0) => empty selection 272 | void neo_own(neo_X* x, bool offer, int sel, const void* ptr, size_t cb, int type) 273 | { 274 | // _VIMENC_TEXT: type 'encoding' NUL text 275 | cb = alloc_data(x, sel, cb); 276 | if (cb > 0) { 277 | x->data[sel][0] = type; 278 | memcpy(x->data[sel] + 1, "utf-8", sizeof("utf-8")); 279 | memcpy(x->data[sel] + 1 + sizeof("utf-8"), ptr, cb); 280 | } 281 | x->stamp[sel] = time_diff(x->delta); 282 | 283 | if (offer) 284 | XSetSelectionOwner(x->d, x->atom[sel], x->w, x->stamp[sel]); 285 | else 286 | x->f_rdy[sel] = true; 287 | } 288 | 289 | 290 | // uv_prepare_t callback 291 | static int cb_prepare(lua_State* L) 292 | { 293 | neo_X* x = neo_x(L); 294 | if (x != NULL) { 295 | XEvent xe; 296 | while (XPending(x->d) > 0) { 297 | XNextEvent(x->d, &xe); 298 | dispatch_event(x, &xe); 299 | } 300 | } 301 | 302 | return 0; 303 | } 304 | 305 | 306 | // uv_poll_t callback 307 | static int cb_poll(lua_State* L) 308 | { 309 | neo_X* x = neo_x(L); 310 | if (x != NULL && lua_isnil(L, 1) && strchr(lua_tostring(L, 2), 'r') != NULL) { 311 | XEvent xe; 312 | XNextEvent(x->d, &xe); 313 | dispatch_event(x, &xe); 314 | } 315 | 316 | return 0; 317 | } 318 | 319 | 320 | // X event dispatcher 321 | static void dispatch_event(neo_X* x, XEvent* xe) 322 | { 323 | switch (xe->type) { 324 | case PropertyNotify: 325 | if (xe->xproperty.atom == x->atom[timestamp]) { 326 | x->delta = time_diff(xe->xproperty.time); 327 | XSelectInput(x->d, x->w, NoEventMask); 328 | } 329 | break; 330 | case SelectionClear: 331 | if (xe->xselectionclear.window == x->w) 332 | alloc_data(x, atom2sel(x, xe->xselectionclear.selection), 0); 333 | break; 334 | case SelectionNotify: 335 | on_sel_notify(x, &xe->xselection); 336 | break; 337 | case SelectionRequest: 338 | on_sel_request(x, &xe->xselectionrequest); 339 | break; 340 | } 341 | } 342 | 343 | 344 | // SelectionNotify event handler 345 | static void on_sel_notify(neo_X* x, XSelectionEvent* xse) 346 | { 347 | int sel = atom2sel(x, xse->selection); 348 | 349 | if (xse->property == x->atom[neo_ready]) { 350 | // read our property 351 | Atom type = None; 352 | uint8_t* ptr = NULL; 353 | unsigned char* xptr = NULL; 354 | unsigned long cxptr = 0; 355 | XGetWindowProperty(x->d, x->w, x->atom[neo_ready], 0, LONG_MAX, True, 356 | AnyPropertyType, &type, &(int){0}, &cxptr, &(unsigned long){0}, &xptr); 357 | 358 | do { 359 | uint8_t* buf = (uint8_t*)xptr; 360 | size_t cb = cxptr; 361 | 362 | if (type == x->atom[incr]) { 363 | // INCR 364 | for (cb = 0; ; cb += cxptr) { 365 | XFree(xptr); 366 | XIfEvent(x->d, &(XEvent){0}, is_incr_notify, (XPointer)xse); 367 | XGetWindowProperty(x->d, x->w, x->atom[neo_ready], 0, LONG_MAX, 368 | True, AnyPropertyType, &(Atom){None}, &(int){0}, &cxptr, 369 | &(unsigned long){0}, &xptr); 370 | void* ptr2 = cxptr ? realloc(ptr, cb + cxptr) : NULL; 371 | if (ptr2 == NULL) 372 | break; 373 | ptr = ptr2; 374 | memcpy(ptr + cb, xptr, cxptr); 375 | } 376 | type = xse->target; 377 | buf = ptr; 378 | } 379 | 380 | if (cb == 0) { 381 | // nothing to do 382 | } else if (type == x->atom[atom] || type == x->atom[targets]) { 383 | // TARGETS 384 | Atom target = best_target(x, (Atom*)buf, (int)cb); 385 | if (target != None) { 386 | XConvertSelection(x->d, xse->selection, target, x->atom[neo_ready], 387 | x->w, xse->time); 388 | break; 389 | } 390 | } else if (type == x->atom[vimenc]) { 391 | // _VIMENC_TEXT 392 | if (cb >= 1 + sizeof("utf-8") 393 | && memcmp(buf + 1, "utf-8", sizeof("utf-8")) == 0) { 394 | // this is UTF-8 395 | neo_own(x, false, sel, buf + 1 + sizeof("utf-8"), 396 | cb - 1 - sizeof("utf-8"), buf[0]); 397 | } else { 398 | // no UTF-8; ask then for UTF8_STRING 399 | XConvertSelection(x->d, xse->selection, x->atom[utf8_string], 400 | x->atom[neo_ready], x->w, xse->time); 401 | } 402 | break; 403 | } else if (type == x->atom[vimtext]) { 404 | // _VIM_TEXT: assume UTF-8 405 | neo_own(x, false, sel, buf + 1, cb - 1, buf[0]); 406 | break; 407 | } else if (type == x->atom[plain_utf8] || type == x->atom[utf8_string] 408 | || type == x->atom[plain]) { 409 | // no conversion 410 | neo_own(x, false, sel, buf, cb, MAUTO); 411 | break; 412 | } else if (type == x->atom[compound] || type == x->atom[string] 413 | || type == x->atom[text]) { 414 | // COMPOUND_TEXT, STRING or TEXT: attempt to convert to UTF-8 415 | XTextProperty xtp = { 416 | .value = buf, 417 | .encoding = type, 418 | .format = 8, 419 | .nitems = cb, 420 | }; 421 | char** list; 422 | if (Xutf8TextPropertyToTextList(x->d, &xtp, &list, &(int){0}) 423 | == Success) { 424 | neo_own(x, false, sel, list[0], strlen(list[0]), MAUTO); 425 | XFreeStringList(list); 426 | break; 427 | } 428 | } 429 | 430 | // conversion failed 431 | neo_own(x, false, sel, NULL, 0, 0); 432 | } while (0); 433 | 434 | free(ptr); 435 | if (xptr != NULL) 436 | XFree(xptr); 437 | } else if (xse->property == None) { 438 | // peer error 439 | neo_own(x, false, sel, NULL, 0, 0); 440 | } 441 | } 442 | 443 | 444 | // SelectionRequest event handler 445 | static void on_sel_request(neo_X* x, XSelectionRequestEvent* xsre) 446 | { 447 | // prepare SelectionNotify 448 | XSelectionEvent xse = { 449 | .type = SelectionNotify, 450 | .display = x->d, 451 | .requestor = xsre->requestor, 452 | .selection = xsre->selection, 453 | .target = xsre->target, 454 | .property = xsre->property ? xsre->property : xsre->target, 455 | .time = time_diff(x->delta), 456 | }; 457 | 458 | int sel = atom2sel(x, xsre->selection); 459 | 460 | // TARGETS: DELETE, MULTIPLE, TIMESTAMP, _VIMENC_TEXT, _VIM_TEXT, UTF8_STRING, 461 | // COMPOUND_TEXT, STRING, TEXT 462 | if (xsre->owner != x->w || (xsre->time != CurrentTime && 463 | xsre->time < x->stamp[sel])) { 464 | // refuse non-matching request 465 | xse.property = None; 466 | } else if (xsre->target == x->atom[targets]) { 467 | // response is ATOM 468 | XChangeProperty(x->d, xse.requestor, xse.property, x->atom[atom], 32, 469 | PropModeReplace, (unsigned char*)&x->atom[targets], total - targets); 470 | } else if (xsre->target == x->atom[dele]) { 471 | // response is NULL 472 | alloc_data(x, sel, 0); 473 | XChangeProperty(x->d, xse.requestor, xse.property, x->atom[null], 32, 474 | PropModeReplace, NULL, 0); 475 | } else if (xsre->target == x->atom[multi]) { 476 | // response is ATOM_PAIR 477 | to_multiple(x, sel, &xse); 478 | } else if (xsre->target == x->atom[timestamp]) { 479 | // response is INTEGER 480 | XChangeProperty(x->d, xse.requestor, xse.property, x->atom[integer], 32, 481 | PropModeReplace, (unsigned char*)&x->stamp[sel], 1); 482 | } else if (best_target(x, &xsre->target, 1) != None) { 483 | // attempt to convert 484 | to_property(x, sel, xse.requestor, xse.property, xsre->target); 485 | } else { 486 | // unknown target 487 | xse.property = None; 488 | } 489 | 490 | // send SelectionNotify 491 | XSendEvent(x->d, xse.requestor, True, NoEventMask, (XEvent*)&xse); 492 | } 493 | 494 | 495 | // (re-)allocate data buffer for selection 496 | static size_t alloc_data(neo_X* x, int sel, size_t cb) 497 | { 498 | if (cb > 0) { 499 | void* ptr = realloc(x->data[sel], 1 + sizeof("utf-8") + cb); 500 | if (ptr != NULL) { 501 | x->data[sel] = ptr; 502 | x->cb[sel] = cb; 503 | } 504 | } else { 505 | free(x->data[sel]); 506 | x->data[sel] = NULL; 507 | x->cb[sel] = 0; 508 | } 509 | 510 | return x->cb[sel]; 511 | } 512 | 513 | 514 | // Atom => selection index 515 | static int atom2sel(neo_X* x, Atom atom) 516 | { 517 | for (int i = 0; i < sel_total; ++i) 518 | if (atom == x->atom[i]) 519 | return i; 520 | 521 | return sel_clip; 522 | } 523 | 524 | 525 | // get best matching target atom 526 | static Atom best_target(neo_X* x, Atom* atom, int count) 527 | { 528 | int best = total; 529 | 530 | for (int i = 0; i < count && best > vimenc; ++i) 531 | for (int j = vimenc; j < best; ++j) 532 | if (atom[i] == x->atom[j]) { 533 | best = j; 534 | break; 535 | } 536 | 537 | return (best < total) ? x->atom[best] : None; 538 | } 539 | 540 | 541 | // X11 predicate function: PropertyNotify/PropertyNewValue 542 | static Bool is_incr_notify(Display* d, XEvent* xe, XPointer arg) 543 | { 544 | (void)d; // unused 545 | if (xe->type != PropertyNotify) 546 | return False; 547 | 548 | XPropertyEvent* xpe = &xe->xproperty; 549 | XSelectionEvent* xse = (XSelectionEvent*)arg; 550 | 551 | return (xpe->window == xse->requestor && xpe->atom == xse->property 552 | && xpe->time >= xse->time && xpe->state == PropertyNewValue); 553 | } 554 | 555 | 556 | // run uv_loop until stop condition or time out 557 | static void modal_loop(lua_State* L, bool* stop, uint32_t timeout) 558 | { 559 | lua_getfield(L, uv_share, "uv"); // uv or loop => stack 560 | 561 | // local now, till = uv.now() + timeout 562 | lua_Integer now, till; 563 | lua_getfield(L, -1, "now"); 564 | lua_call(L, 0, 1); 565 | till = lua_tointeger(L, -1) + timeout; 566 | lua_pop(L, 1); 567 | 568 | // run nested loop 569 | do { 570 | // uv.run"once" 571 | lua_getfield(L, -1, "run"); 572 | lua_pushliteral(L, "once"); 573 | lua_call(L, 1, 0); 574 | 575 | // check stop condition 576 | if (*stop) 577 | break; 578 | 579 | // now = uv.now() 580 | lua_getfield(L, -1, "now"); 581 | lua_call(L, 0, 1); 582 | now = lua_tointeger(L, -1); 583 | lua_pop(L, 1); 584 | } while (now < till); 585 | 586 | lua_pop(L, 1); // uv or loop <= stack 587 | } 588 | 589 | 590 | // get ms difference from reference time 591 | static Time time_diff(Time ref) 592 | { 593 | struct timespec t; 594 | 595 | if (ref == CurrentTime || clock_gettime(CLOCK_MONOTONIC, &t) < 0) 596 | return CurrentTime; 597 | 598 | return (t.tv_sec * 1000 + t.tv_nsec / 1000000) - ref; 599 | } 600 | 601 | 602 | // process MULTIPLE selection requests 603 | static void to_multiple(neo_X* x, int sel, XSelectionEvent* xse) 604 | { 605 | Atom* tgt = NULL; 606 | unsigned long ul_tgt = 0; 607 | XGetWindowProperty(x->d, xse->requestor, xse->property, 0, LONG_MAX, False, 608 | x->atom[atom_pair], &(Atom){None}, &(int){0}, &ul_tgt, &(unsigned long){0}, 609 | (unsigned char**)&tgt); 610 | int i_tgt = (long)ul_tgt; 611 | 612 | for (int i = 0; i < i_tgt; i += 2) 613 | if (best_target(x, &tgt[i], 1) != None && tgt[i + 1] != None) 614 | to_property(x, sel, xse->requestor, tgt[i + 1], tgt[i]); 615 | else 616 | tgt[i + 1] = None; 617 | 618 | if (i_tgt > 0) { 619 | XChangeProperty(x->d, xse->requestor, xse->property, x->atom[atom_pair], 32, 620 | PropModeReplace, (unsigned char*)tgt, i_tgt); 621 | XFree(tgt); 622 | } 623 | } 624 | 625 | 626 | // put selection data into window property 627 | static void to_property(neo_X* x, int sel, Window w, Atom property, Atom type) 628 | { 629 | if (x->cb[sel] == 0) { 630 | XDeleteProperty(x->d, w, property); 631 | return; 632 | } 633 | 634 | XTextProperty xtp = { 635 | .value = (unsigned char*)x->data[sel], 636 | .encoding = type, 637 | .format = 8, 638 | .nitems = (unsigned long)x->cb[sel], 639 | }; 640 | unsigned char* ptr = NULL; 641 | unsigned char* xptr = NULL; 642 | 643 | if (type == x->atom[vimenc]) { 644 | // _VIMENC_TEXT: type 'encoding' NUL text 645 | xtp.nitems += 1 + sizeof("utf-8"); 646 | } else if (type == x->atom[vimtext]) { 647 | // _VIM_TEXT: type text 648 | ptr = malloc(1 + xtp.nitems); 649 | if (ptr != NULL) { 650 | ptr[0] = xtp.value[0]; 651 | memcpy(ptr + 1, xtp.value + 1 + sizeof("utf-8"), xtp.nitems); 652 | xtp.value = ptr; 653 | ++xtp.nitems; 654 | } 655 | } else { 656 | // skip header 657 | xtp.value += 1 + sizeof("utf-8"); 658 | } 659 | 660 | // Vim-alike behaviour: STRING == UTF8_STRING, TEXT == COMPOUND_TEXT 661 | if (type == x->atom[compound] || type == x->atom[text]) { 662 | // convert UTF-8 to COMPOUND_TEXT 663 | ptr = malloc(xtp.nitems + 1); 664 | if (ptr != NULL) { 665 | memcpy(ptr, xtp.value, xtp.nitems); 666 | ptr[xtp.nitems] = 0; 667 | Xutf8TextListToTextProperty(x->d, (char**)&ptr, 1, XCompoundTextStyle, &xtp); 668 | xptr = xtp.value; 669 | } 670 | } 671 | 672 | // set property 673 | XChangeProperty(x->d, w, property, type, xtp.format, PropModeReplace, xtp.value, 674 | (int)xtp.nitems); 675 | 676 | // free memory 677 | free(ptr); 678 | if (xptr != NULL) 679 | XFree(xptr); 680 | } 681 | -------------------------------------------------------------------------------- /src/neoclip.h: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 23 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #ifndef NEOCLIP_H 10 | #define NEOCLIP_H 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | 23 | // Vim register type 24 | enum { 25 | MCHAR = 0, 26 | MLINE = 1, 27 | MBLOCK = 2, 28 | MAUTO = 255, 29 | }; 30 | 31 | enum { 32 | uv_module = lua_upvalueindex(1), // module name 33 | uv_share = lua_upvalueindex(2), // shared value (table or userdata) 34 | }; 35 | 36 | // userdata : incomplete type 37 | typedef struct neo_UD neo_UD; 38 | 39 | // neo_common.c 40 | int neo_id(lua_State* L); // lua_CFunction(uv_module) => string 41 | void neo_join(lua_State* L, int ix, const char* sep); 42 | void neo_split(lua_State* L, int ix, const void* data, size_t cb, int type); 43 | void neo_inspect(lua_State* L, int ix); // debug only 44 | void neo_printf(lua_State* L, const char* fmt, ...); // debug only 45 | 46 | // inline helpers 47 | static inline int neo_absindex(lua_State *L, int ix) 48 | { 49 | return (0 > ix && ix > LUA_REGISTRYINDEX) ? (ix + 1 + lua_gettop(L)) : ix; 50 | } 51 | static inline bool neo_did(lua_State* L, const char* what) 52 | { 53 | lua_getfield(L, LUA_REGISTRYINDEX, what); 54 | bool did = lua_toboolean(L, -1); 55 | if (!did) { 56 | lua_pushboolean(L, true); 57 | lua_setfield(L, LUA_REGISTRYINDEX, what); 58 | } 59 | lua_pop(L, 1); 60 | return did; 61 | } 62 | static inline void neo_pushcfunction(lua_State* L, lua_CFunction fn) 63 | { 64 | lua_pushvalue(L, uv_module); // upvalue 1 : module name 65 | lua_pushvalue(L, uv_share); // upvalue 2 : shared table 66 | lua_pushcclosure(L, fn, 2); 67 | } 68 | static inline int neo_type(int ch) 69 | { 70 | switch (ch) { 71 | case 'c': 72 | case 'v': 73 | return MCHAR; 74 | case 'l': 75 | case 'V': 76 | return MLINE; 77 | case 'b': 78 | case '\026': 79 | return MBLOCK; 80 | default: 81 | return MAUTO; 82 | } 83 | } 84 | static inline neo_UD* neo_checkud(lua_State* L, int ix) 85 | { 86 | return (neo_UD*)luaL_checkudata(L, ix, lua_tostring(L, uv_module)); 87 | } 88 | static inline neo_UD* neo_ud(lua_State* L, int ix) 89 | { 90 | return (neo_UD*)luaL_testudata(L, ix, lua_tostring(L, uv_module)); 91 | } 92 | 93 | 94 | #endif // NEOCLIP_H 95 | -------------------------------------------------------------------------------- /src/neoclip_mac.m: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 21 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #include "neoclip.h" 10 | #import 11 | 12 | 13 | // Vim compatible format 14 | static NSString* VimPboardType = @"VimPboardType"; 15 | 16 | 17 | // forward prototypes 18 | static int neo_nil(lua_State* L); 19 | static int neo_true(lua_State* L); 20 | static int neo_get(lua_State* L); 21 | static int neo_set(lua_State* L); 22 | 23 | 24 | // module registration 25 | __attribute__((visibility("default"))) 26 | int luaopen_driver(lua_State* L) 27 | { 28 | static struct luaL_Reg const iface[] = { 29 | { "id", neo_id }, 30 | { "start", neo_nil }, 31 | { "stop", neo_nil }, 32 | { "status", neo_true }, 33 | { "get", neo_get }, 34 | { "set", neo_set }, 35 | { NULL, NULL } 36 | }; 37 | 38 | #if defined(luaL_newlibtable) 39 | luaL_newlibtable(L, iface); 40 | #else 41 | lua_createtable(L, 0, sizeof(iface) / sizeof(iface[0]) - 1); 42 | #endif 43 | 44 | lua_pushvalue(L, 1); // upvalue 1 : module name 45 | 46 | #if defined(luaL_newlibtable) 47 | luaL_setfuncs(L, iface, 1); 48 | #else 49 | luaL_openlib(L, NULL, iface, 1); 50 | #endif 51 | return 1; 52 | } 53 | 54 | 55 | // lua_CFunction() => nil 56 | static int neo_nil(lua_State* L) 57 | { 58 | lua_pushnil(L); 59 | return 1; 60 | } 61 | 62 | 63 | // lua_CFunction() => true 64 | static int neo_true(lua_State* L) 65 | { 66 | lua_pushboolean(L, true); 67 | return 1; 68 | } 69 | 70 | 71 | // get(regname) => [lines, regtype] 72 | static int neo_get(lua_State* L) 73 | { 74 | luaL_checktype(L, 1, LUA_TSTRING); // regname (unused) 75 | 76 | // a table to return 77 | lua_createtable(L, 2, 0); 78 | 79 | // check supported types 80 | NSPasteboard* pb = [NSPasteboard generalPasteboard]; 81 | NSString* bestType = [pb availableTypeFromArray:[NSArray 82 | arrayWithObjects:VimPboardType, NSPasteboardTypeString, nil]]; 83 | 84 | if (bestType) { 85 | NSString* str = nil; 86 | int type = MAUTO; 87 | 88 | // VimPboardType is [NSArray arrayWithObjects:[NSNumber], [NSString]] 89 | if ([bestType isEqual:VimPboardType]) { 90 | id plist = [pb propertyListForType:VimPboardType]; 91 | if ([plist isKindOfClass:[NSArray class]] && [plist count] == 2) { 92 | type = [[plist objectAtIndex:0] intValue]; 93 | str = [plist objectAtIndex:1]; 94 | } 95 | } 96 | 97 | // fallback to NSPasteboardTypeString 98 | if (str == nil) 99 | str = [pb stringForType:NSPasteboardTypeString]; 100 | 101 | // convert to UTF-8 and split into table 102 | NSData* buf = [str dataUsingEncoding:NSUTF8StringEncoding]; 103 | if (buf.length > 0) 104 | neo_split(L, -1, buf.bytes, buf.length, type); 105 | } 106 | 107 | // always return table (empty on error) 108 | return 1; 109 | } 110 | 111 | 112 | // set(regname, lines, regtype) => boolean 113 | static int neo_set(lua_State* L) 114 | { 115 | luaL_checktype(L, 1, LUA_TSTRING); // regname (unused) 116 | luaL_checktype(L, 2, LUA_TTABLE); // lines 117 | luaL_checktype(L, 3, LUA_TSTRING); // regtype 118 | int type = neo_type(*lua_tostring(L, 3)); 119 | 120 | // table to string 121 | neo_join(L, 2, "\n"); 122 | 123 | // get UTF-8 124 | size_t cb; 125 | const char* ptr = lua_tolstring(L, -1, &cb); 126 | 127 | // convert UTF-8 to NSString 128 | NSString* str = [[NSString alloc] initWithBytes:ptr length:cb 129 | encoding:NSUTF8StringEncoding]; 130 | 131 | // set both VimPboardType and NSPasteboardTypeString 132 | NSPasteboard* pb = [NSPasteboard generalPasteboard]; 133 | [pb declareTypes:[NSArray arrayWithObjects:VimPboardType, NSPasteboardTypeString, 134 | nil] owner:nil]; 135 | bool success = [pb setString:str forType:NSPasteboardTypeString] 136 | && [pb setPropertyList:[NSArray arrayWithObjects:[NSNumber numberWithInt:type], 137 | str, nil] forType:VimPboardType]; 138 | 139 | // cleanup 140 | [str release]; 141 | 142 | lua_pushboolean(L, success); 143 | return 1; 144 | } 145 | -------------------------------------------------------------------------------- /src/neoclip_nix.c: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 23 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #include "neoclip_nix.h" 10 | 11 | 12 | // forward prototypes 13 | static int neo_stop(lua_State* L); 14 | static int neo_status(lua_State* L); 15 | static int neo_get(lua_State* L); 16 | static int neo_set(lua_State* L); 17 | 18 | 19 | // module registration 20 | __attribute__((visibility("default"))) 21 | int luaopen_driver(lua_State* L) 22 | { 23 | static struct luaL_Reg const iface[] = { 24 | { "id", neo_id }, 25 | { "start", neo_start }, 26 | { "stop", neo_stop }, 27 | { "status", neo_status }, 28 | { "get", neo_get }, 29 | { "set", neo_set }, 30 | { NULL, NULL } 31 | }; 32 | 33 | #if defined(luaL_newlibtable) 34 | luaL_newlibtable(L, iface); 35 | #else 36 | lua_createtable(L, 0, sizeof(iface) / sizeof(iface[0]) - 1); 37 | #endif 38 | 39 | lua_pushvalue(L, 1); // upvalue 1 : module name 40 | lua_newtable(L); // upvalue 2 : shared table 41 | 42 | #if defined(luaL_newlibtable) 43 | luaL_setfuncs(L, iface, 2); 44 | #else 45 | luaL_openlib(L, NULL, iface, 2); 46 | #endif 47 | return 1; 48 | } 49 | 50 | 51 | // invalidate state 52 | static int neo_stop(lua_State* L) 53 | { 54 | // uv_share.x = nil 55 | lua_pushnil(L); 56 | lua_setfield(L, uv_share, "x"); 57 | // collectgarbage() 58 | lua_gc(L, LUA_GCCOLLECT, 0); 59 | 60 | lua_pushnil(L); 61 | return 1; 62 | } 63 | 64 | 65 | // get status 66 | static int neo_status(lua_State* L) 67 | { 68 | lua_pushboolean(L, neo_x(L) != NULL); 69 | return 1; 70 | } 71 | 72 | 73 | // get(regname) => [lines, regtype] 74 | static int neo_get(lua_State* L) 75 | { 76 | luaL_checktype(L, 1, LUA_TSTRING); // regname 77 | int sel = (*lua_tostring(L, 1) == '*') ? sel_prim : sel_clip; 78 | 79 | // a table to return 80 | lua_createtable(L, 2, 0); 81 | neo_fetch(L, -1, sel); 82 | 83 | // always return table (empty on error) 84 | return 1; 85 | } 86 | 87 | 88 | // set(regname, lines, regtype) => boolean 89 | static int neo_set(lua_State* L) 90 | { 91 | luaL_checktype(L, 1, LUA_TSTRING); // regname 92 | luaL_checktype(L, 2, LUA_TTABLE); // lines 93 | luaL_checktype(L, 3, LUA_TSTRING); // regtype 94 | int sel = (*lua_tostring(L, 1) == '*') ? sel_prim : sel_clip; 95 | int type = neo_type(*lua_tostring(L, 3)); 96 | 97 | neo_X* x = neo_x(L); 98 | if (x != NULL) { 99 | // change selection data 100 | neo_join(L, 2, "\n"); 101 | 102 | size_t cb; 103 | const char* ptr = lua_tolstring(L, -1, &cb); 104 | neo_own(x, true, sel, ptr, cb, type); 105 | } 106 | 107 | lua_pushboolean(L, x != NULL); 108 | return 1; 109 | } 110 | -------------------------------------------------------------------------------- /src/neoclip_nix.h: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 21 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #ifndef NEOCLIP_NIX_H 10 | #define NEOCLIP_NIX_H 11 | 12 | #if !defined(_POSIX_C_SOURCE) 13 | #define _POSIX_C_SOURCE 200112L 14 | #endif // _POSIX_C_SOURCE 15 | 16 | #include "neoclip.h" 17 | 18 | 19 | // selection index 20 | enum { 21 | sel_prim, 22 | sel_sec, 23 | sel_clip, 24 | sel_total 25 | }; 26 | 27 | // driver state : incomplete type 28 | typedef struct neo_X neo_X; 29 | 30 | int neo_start(lua_State* L); 31 | void neo_fetch(lua_State* L, int ix, int sel); 32 | void neo_own(neo_X* x, bool offer, int sel, const void* ptr, size_t cb, int type); 33 | 34 | // inline helper 35 | static inline neo_X* neo_x(lua_State* L) 36 | { 37 | luaL_checktype(L, uv_share, LUA_TTABLE); 38 | lua_getfield(L, uv_share, "x"); 39 | neo_X* x = (neo_X*)neo_ud(L, -1); 40 | lua_pop(L, 1); 41 | return x; 42 | } 43 | 44 | 45 | #endif // NEOCLIP_NIX_H 46 | -------------------------------------------------------------------------------- /src/neoclip_w32.c: -------------------------------------------------------------------------------- 1 | /* 2 | * neoclip - Neovim clipboard provider 3 | * Last Change: 2024 Aug 21 4 | * License: https://unlicense.org 5 | * URL: https://github.com/matveyt/neoclip 6 | */ 7 | 8 | 9 | #define WIN32_LEAN_AND_MEAN 10 | #include "neoclip.h" 11 | #include 12 | 13 | 14 | // shared data 15 | struct neo_UD { 16 | UINT uVimMeta, uVimRaw; // Vim clipboard format 17 | UINT uOEMCP, uACP; // OEM/ANSI code page 18 | }; 19 | 20 | 21 | // forward prototypes 22 | static int neo_nil(lua_State* L); 23 | static int neo_true(lua_State* L); 24 | static int neo_get(lua_State* L); 25 | static int neo_set(lua_State* L); 26 | static HANDLE get_and_lock(UINT uFormat, LPVOID ppData, size_t* pcbMax); 27 | static bool unlock_and_set(UINT uFormat, HANDLE hData); 28 | static HANDLE mb2wc(UINT cp, LPCVOID pSrc, size_t cchSrc, LPVOID ppDst, size_t* pcch); 29 | static HANDLE wc2mb(UINT cp, LPCVOID pSrc, size_t cchSrc, LPVOID ppDst, size_t* pcch); 30 | 31 | 32 | // module registration 33 | __declspec(dllexport) 34 | int luaopen_driver(lua_State* L) 35 | { 36 | static struct luaL_Reg const iface[] = { 37 | { "id", neo_id }, 38 | { "start", neo_nil }, 39 | { "stop", neo_nil }, 40 | { "status", neo_true }, 41 | { "get", neo_get }, 42 | { "set", neo_set }, 43 | { NULL, NULL } 44 | }; 45 | 46 | #if defined(luaL_newlibtable) 47 | luaL_newlibtable(L, iface); 48 | #else 49 | lua_createtable(L, 0, sizeof(iface) / sizeof(iface[0]) - 1); 50 | #endif 51 | 52 | lua_pushvalue(L, 1); // upvalue 1 : module name 53 | neo_UD* ud = lua_newuserdata(L, sizeof(neo_UD)); // upvalue 2 : shared data 54 | ud->uVimMeta = RegisterClipboardFormatW(L"VimClipboard2"); 55 | ud->uVimRaw = RegisterClipboardFormatW(L"VimRawBytes"); 56 | GetLocaleInfoW(LOCALE_USER_DEFAULT, LOCALE_IDEFAULTCODEPAGE 57 | | LOCALE_RETURN_NUMBER, (WCHAR*)&ud->uOEMCP, sizeof(UINT) / sizeof(WCHAR)); 58 | GetLocaleInfoW(LOCALE_USER_DEFAULT, LOCALE_IDEFAULTANSICODEPAGE 59 | | LOCALE_RETURN_NUMBER, (WCHAR*)&ud->uACP, sizeof(UINT) / sizeof(WCHAR)); 60 | 61 | // metatable for shared data 62 | luaL_newmetatable(L, lua_tostring(L, 1)); 63 | //neo_pushcfunction(L, neo__gc); 64 | //lua_setfield(L, -2, "__gc"); 65 | lua_setmetatable(L, -2); 66 | 67 | #if defined(luaL_newlibtable) 68 | luaL_setfuncs(L, iface, 2); 69 | #else 70 | luaL_openlib(L, NULL, iface, 2); 71 | #endif 72 | return 1; 73 | } 74 | 75 | 76 | // lua_CFunction() => nil 77 | static int neo_nil(lua_State* L) 78 | { 79 | lua_pushnil(L); 80 | return 1; 81 | } 82 | 83 | 84 | // lua_CFunction() => true 85 | static int neo_true(lua_State* L) 86 | { 87 | lua_pushboolean(L, true); 88 | return 1; 89 | } 90 | 91 | 92 | // get(regname) => [lines, regtype] 93 | static int neo_get(lua_State* L) 94 | { 95 | luaL_checktype(L, 1, LUA_TSTRING); // regname (unused) 96 | neo_UD* ud = neo_checkud(L, uv_share); 97 | 98 | // a table to return 99 | lua_createtable(L, 2, 0); 100 | if (!OpenClipboard(NULL)) 101 | return 1; 102 | 103 | // get Vim meta 104 | int meta[4] = { 105 | MAUTO, // type 106 | INT_MAX, // ACP len 107 | INT_MAX, // UCS len 108 | 0, // Raw len 109 | }; 110 | HANDLE hBuf = NULL; 111 | LPVOID pBuf; 112 | size_t count = sizeof(meta); 113 | HANDLE hData; 114 | if ((hData = get_and_lock(ud->uVimMeta, &pBuf, &count)) != NULL) { 115 | memcpy(meta, pBuf, count & ~(sizeof(int) - 1)); 116 | GlobalUnlock(hData); 117 | hData = NULL; 118 | pBuf = NULL; 119 | } 120 | 121 | do { 122 | // VimRawBytes 123 | if ((count = meta[3]) >= sizeof("utf-8") 124 | && (hData = get_and_lock(ud->uVimRaw, &pBuf, &count)) != NULL) { 125 | if (count >= sizeof("utf-8") 126 | && memcmp(pBuf, "utf-8", sizeof("utf-8")) == 0) { 127 | *(LPSTR*)&pBuf += sizeof("utf-8"); 128 | count -= sizeof("utf-8"); 129 | break; 130 | } 131 | GlobalUnlock(hData); 132 | hData = NULL; 133 | pBuf = NULL; 134 | } 135 | 136 | // CF_UNICODETEXT 137 | if ((count = meta[2] * sizeof(WCHAR)) > 0 138 | && (hData = get_and_lock(CF_UNICODETEXT, &pBuf, &count)) != NULL) { 139 | hBuf = wc2mb(CP_UTF8, pBuf, count / sizeof(WCHAR), &pBuf, &count); 140 | break; 141 | } 142 | 143 | // CF_TEXT 144 | if ((count = meta[1]) > 0 145 | && (hData = get_and_lock(CF_TEXT, &pBuf, &count)) != NULL) { 146 | HANDLE hTemp = mb2wc(ud->uACP, pBuf, count, &pBuf, &count); 147 | if (hTemp != NULL) { 148 | hBuf = wc2mb(CP_UTF8, pBuf, count, &pBuf, &count); 149 | GlobalUnlock(hTemp); 150 | GlobalFree(hTemp); 151 | } 152 | break; 153 | } 154 | } while (0); 155 | 156 | if (hData != NULL) { 157 | // note: pBuf may contain trailing NUL 158 | if (pBuf != NULL) 159 | neo_split(L, -1, pBuf, count, meta[0]); 160 | if (hBuf != NULL) 161 | GlobalUnlock(hBuf), GlobalFree(hBuf); 162 | GlobalUnlock(hData); 163 | } 164 | CloseClipboard(); 165 | return 1; 166 | } 167 | 168 | 169 | // set(regname, lines, regtype) => boolean 170 | static int neo_set(lua_State* L) 171 | { 172 | luaL_checktype(L, 1, LUA_TSTRING); // regname (unused) 173 | luaL_checktype(L, 2, LUA_TTABLE); // lines 174 | luaL_checktype(L, 3, LUA_TSTRING); // regtype 175 | neo_UD* ud = neo_checkud(L, uv_share); 176 | 177 | bool success = OpenClipboard(NULL); 178 | if (success) { 179 | EmptyClipboard(); 180 | neo_join(L, 2, "\r\n"); 181 | 182 | // get UTF-8 183 | size_t cchACP = 0; 184 | size_t cchUCS, cchSrc; 185 | LPCSTR pSrc = lua_tolstring(L, -1, &cchSrc); ++cchSrc; 186 | HANDLE hBuf; 187 | LPVOID pBuf; 188 | 189 | // CF_UNICODETEXT 190 | hBuf = mb2wc(CP_UTF8, pSrc, (int)cchSrc, &pBuf, &cchUCS); 191 | if (hBuf != NULL) { 192 | // CF_OEMTEXT + CF_TEXT 193 | unlock_and_set(CF_OEMTEXT, 194 | wc2mb(ud->uOEMCP, pBuf, cchUCS, &(LPVOID){NULL}, &cchACP)); 195 | unlock_and_set(CF_TEXT, 196 | wc2mb(ud->uACP, pBuf, cchUCS, &(LPVOID){NULL}, &cchACP)); 197 | } 198 | success = unlock_and_set(CF_UNICODETEXT, hBuf) && success; 199 | 200 | // VimRawBytes 201 | hBuf = GlobalAlloc(GMEM_MOVEABLE, sizeof("utf-8") + cchSrc); 202 | if (hBuf != NULL) { 203 | pBuf = GlobalLock(hBuf); 204 | memcpy(pBuf, "utf-8", sizeof("utf-8")); // NUL terminated 205 | memcpy((LPSTR)pBuf + sizeof("utf-8"), pSrc, cchSrc); // NUL terminated 206 | } 207 | success = unlock_and_set(ud->uVimRaw, hBuf) && success; 208 | 209 | // VimClipboard2 210 | hBuf = GlobalAlloc(GMEM_MOVEABLE, sizeof(int) * 4); 211 | if (hBuf != NULL) { 212 | int* pMeta = GlobalLock(hBuf); 213 | *pMeta++ = neo_type(*lua_tostring(L, 3)); // type 214 | *pMeta++ = cchACP ? cchACP - 1 : INT_MAX; // ACP len 215 | *pMeta++ = cchUCS ? cchUCS - 1 : INT_MAX; // UCS len 216 | *pMeta = sizeof("utf-8") + cchSrc - 1; // Raw len 217 | } 218 | success = unlock_and_set(ud->uVimMeta, hBuf) && success; 219 | 220 | CloseClipboard(); 221 | } 222 | 223 | lua_pushboolean(L, success); 224 | return 1; 225 | } 226 | 227 | 228 | // safe get clipboard data 229 | static HANDLE get_and_lock(UINT uFormat, LPVOID ppData, size_t* pcbMax) 230 | { 231 | HANDLE hData = IsClipboardFormatAvailable(uFormat) ? 232 | GetClipboardData(uFormat) : NULL; 233 | 234 | if (hData != NULL) { 235 | *(LPVOID*)ppData = GlobalLock(hData); 236 | size_t sz = GlobalSize(hData); 237 | if (*pcbMax > sz) 238 | *pcbMax = sz; 239 | } 240 | 241 | return hData; 242 | } 243 | 244 | 245 | // safe set clipboard data 246 | static bool unlock_and_set(UINT uFormat, HANDLE hData) 247 | { 248 | if (hData == NULL || GlobalUnlock(hData) != 0) 249 | return false; 250 | 251 | DWORD dwError = GetLastError(); 252 | if ((dwError == NO_ERROR || dwError == ERROR_NOT_LOCKED) 253 | && SetClipboardData(uFormat, hData) != NULL) 254 | return true; 255 | 256 | GlobalFree(hData); 257 | return false; 258 | } 259 | 260 | 261 | // MultiByte to WideChar 262 | static HANDLE mb2wc(UINT cp, LPCVOID pSrc, size_t cchSrc, LPVOID ppDst, size_t* pcch) 263 | { 264 | int cchDst = MultiByteToWideChar(cp, 0, pSrc, (int)cchSrc, NULL, 0); 265 | HANDLE hBuf = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * cchDst); 266 | if (hBuf != NULL) { 267 | *(LPVOID*)ppDst = GlobalLock(hBuf); 268 | *pcch = (size_t)MultiByteToWideChar(cp, 0, pSrc, (int)cchSrc, *(LPVOID*)ppDst, 269 | cchDst); 270 | } else { 271 | *(LPVOID*)ppDst = NULL; 272 | *pcch = 0; 273 | } 274 | return hBuf; 275 | } 276 | 277 | 278 | // WideChar to MultiByte 279 | static HANDLE wc2mb(UINT cp, LPCVOID pSrc, size_t cchSrc, LPVOID ppDst, size_t* pcch) 280 | { 281 | int cchDst = WideCharToMultiByte(cp, 0, pSrc, (int)cchSrc, NULL, 0, NULL, NULL); 282 | HANDLE hBuf = GlobalAlloc(GMEM_MOVEABLE, cchDst); 283 | if (hBuf != NULL) { 284 | *(LPVOID*)ppDst = GlobalLock(hBuf); 285 | *pcch = (size_t)WideCharToMultiByte(cp, 0, pSrc, (int)cchSrc, *(LPVOID*)ppDst, 286 | cchDst, NULL, NULL); 287 | } else { 288 | *(LPVOID*)ppDst = NULL; 289 | *pcch = 0; 290 | } 291 | return hBuf; 292 | } 293 | --------------------------------------------------------------------------------