aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Rahm <rahm@josher.dev>2026-10-18 15:49:48 -0600
committerJosh Rahm <rahm@josher.dev>2026-10-18 15:49:48 -0600
commit7b15a3a4b4fb4dc6be5d53a22c20d68f8181d68b (patch)
treed3e08937bc78441a2f3d64ad6935337cf6d9b0be
downloadextregs.nvim-main.tar.gz
extregs.nvim-main.tar.bz2
extregs.nvim-main.zip
[extregs] initial commit.HEADmain
added a bunch of useful extregs
-rw-r--r--README.md75
-rw-r--r--lua/extregs/buffer.lua13
-rw-r--r--lua/extregs/file.lua26
-rw-r--r--lua/extregs/file_basename.lua8
-rw-r--r--lua/extregs/file_dirname.lua8
-rw-r--r--lua/extregs/file_stem.lua8
-rw-r--r--lua/extregs/init.lua72
-rw-r--r--lua/extregs/osc52.lua15
-rw-r--r--lua/extregs/ssh.lua31
-rw-r--r--lua/extregs/storage.lua24
-rw-r--r--lua/extregs/timestamp.lua6
-rw-r--r--lua/extregs/tmux.lua24
-rw-r--r--lua/extregs/util.lua31
-rw-r--r--lua/extregs/uuid.lua16
-rw-r--r--plugin/extregs.lua3
-rw-r--r--tests/storage_spec.lua9
16 files changed, 369 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d452f58
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+# extregs.nvim
+
+Useful extended registers for a Neovim build with `has('extregs')`.
+
+## Install
+
+Add the repo to `vim.pack.add({...})`. The plugin automatically calls `setup()`.
+For options, instead call `require('extregs').setup({...})` from an earlier startup
+config file (before Neovim loads `plugin/` runtime files):
+
+```lua
+require('extregs').setup({
+ file = { path = '/var/run/user/' .. vim.env.USER .. '/nvim.regfile' },
+ ssh = { host = 'rahm@photon', path = '~/.local/state/nvim/regfile' },
+ tmux = { buffer = nil }, -- nil: most recent paste buffer
+ timestamp = { format = '%Y-%m-%dT%H:%M:%S%z' },
+})
+```
+
+The plugin registers `osc52`, `tmux`, `buffer`, `file.basename`, `file.stem`,
+`file.dirname`, `file.backed`, `ssh.backed`, `timestamp`, and `uuid`.
+For extreg syntax use your Neovim patch's `~name` register notation.
+
+### tmux paste buffer
+
+`~tmux` reads and writes tmux's current paste buffer. A write runs
+`tmux load-buffer -` and a read runs `tmux show-buffer`.
+Set `tmux = { buffer = 'nvim' }` to use a named tmux buffer instead.
+The content is ordinary text so it can be pasted with tmux's normal paste
+binding; no JSON cookie is written. Reading follows the same plaintext rule
+as backed-register reads: one line is characterwise, multiple lines linewise.
+Writing a linewise register appends a newline. tmux must be accessible from
+Neovim (`TMUX` or a compatible tmux environment).
+
+### Backed-register format
+
+A structured write produces:
+
+```
+NVIM_EXTREGS_JSON_V1
+{"lines":["hello"],"type":"v"}
+```
+
+A read without that first-line magic cookie treats the whole file as plain
+text: one line → characterwise (`v`); multiple lines → linewise (`V`).
+
+### Limitations
+
+- The source buffer is replaced in memory by `buffer` writes; saving it is separate.
+- OSC 52 clipboard reads are terminal-dependent and may time out or fail.
+- SSH calls are synchronous, and `ssh.backed` requires passwordless SSH.
+- Filename registers are read-only in this initial implementation (as are timestamp/uuid).
+- Register callbacks rely on a custom, unreleased Neovim API; validate against your
+ exact extregs branch before using this in production.
+- Missing `has('extregs')` yields a scheduled error notification (not a blocking
+ error prompt), and registration is skipped.
+
+### Provider architecture
+
+Every register has a dedicated module under `lua/extregs/`. Each module exposes
+`get_provider(cfg)` and returns a provider object with `read()` and `write(reg)`
+methods. The initializer alone owns registration, error notifications, and the
+`has('extregs')` guard. To add a register, create a provider module and add one
+entry to the `providers` table in `lua/extregs/init.lua`.
+
+```lua
+local provider = require('extregs.tmux').get_provider(cfg.tmux)
+-- The initializer wraps these with error handling and registers them:
+vim._extregs.register('tmux',
+ function(reg) return provider:write(reg) end,
+ function() return provider:read() end)
+```
+
+The original yank and put callback argument ordering is preserved. The
+shared `storage.lua` serializer is used by both file-backed providers.
diff --git a/lua/extregs/buffer.lua b/lua/extregs/buffer.lua
new file mode 100644
index 0000000..de7c1af
--- /dev/null
+++ b/lua/extregs/buffer.lua
@@ -0,0 +1,13 @@
+local M = {}
+function M.get_provider(_)
+ return {
+ read = function()
+ return { lines = vim.api.nvim_buf_get_lines(0, 0, -1, false), type = 'V' }
+ end,
+ write = function(_, reg)
+ vim.api.nvim_buf_set_lines(0, 0, -1, false, reg.lines)
+ return true
+ end,
+ }
+end
+return M
diff --git a/lua/extregs/file.lua b/lua/extregs/file.lua
new file mode 100644
index 0000000..0dfe88c
--- /dev/null
+++ b/lua/extregs/file.lua
@@ -0,0 +1,26 @@
+local util = require('extregs.util')
+local storage = require('extregs.storage')
+local M = {}
+
+function M.get_provider(cfg)
+ local function path() return util.path(cfg.path) end
+ return {
+ read = function()
+ local fd, err = io.open(path(), 'rb')
+ if not fd then error('cannot read ' .. path() .. ': ' .. tostring(err)) end
+ local data = fd:read('*a')
+ fd:close()
+ return storage.decode(data)
+ end,
+ write = function(_, reg)
+ local dest = path()
+ vim.fn.mkdir(vim.fn.fnamemodify(dest, ':h'), 'p')
+ local fd, err = io.open(dest, 'wb')
+ if not fd then error('cannot write ' .. dest .. ': ' .. tostring(err)) end
+ assert(fd:write(storage.encode(reg)))
+ assert(fd:close())
+ return true
+ end,
+ }
+end
+return M
diff --git a/lua/extregs/file_basename.lua b/lua/extregs/file_basename.lua
new file mode 100644
index 0000000..de59fea
--- /dev/null
+++ b/lua/extregs/file_basename.lua
@@ -0,0 +1,8 @@
+local util = require('extregs.util')
+local M = {}
+function M.get_provider(_)
+ return util.readonly(function()
+ return vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':t')
+ end)
+end
+return M
diff --git a/lua/extregs/file_dirname.lua b/lua/extregs/file_dirname.lua
new file mode 100644
index 0000000..8ba5e77
--- /dev/null
+++ b/lua/extregs/file_dirname.lua
@@ -0,0 +1,8 @@
+local util = require('extregs.util')
+local M = {}
+function M.get_provider(_)
+ return util.readonly(function()
+ return vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':h')
+ end)
+end
+return M
diff --git a/lua/extregs/file_stem.lua b/lua/extregs/file_stem.lua
new file mode 100644
index 0000000..49ebc24
--- /dev/null
+++ b/lua/extregs/file_stem.lua
@@ -0,0 +1,8 @@
+local util = require('extregs.util')
+local M = {}
+function M.get_provider(_)
+ return util.readonly(function()
+ return vim.fn.fnamemodify(vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':t'), ':r')
+ end)
+end
+return M
diff --git a/lua/extregs/init.lua b/lua/extregs/init.lua
new file mode 100644
index 0000000..b9dcbe1
--- /dev/null
+++ b/lua/extregs/init.lua
@@ -0,0 +1,72 @@
+local M = {}
+
+M.defaults = {
+ file = { path = (vim.env.XDG_RUNTIME_DIR or vim.fn.stdpath('state')) .. '/nvim.regfile' },
+ ssh = { host = nil, path = '~/.local/state/nvim/regfile' },
+ osc52 = { register = '+' },
+ tmux = { buffer = nil },
+ timestamp = { format = '%Y-%m-%dT%H:%M:%S%z' },
+}
+
+-- name, module, configuration section; registration semantics are (write, read).
+local providers = {
+ { 'osc52', 'osc52', 'osc52' },
+ { 'tmux', 'tmux', 'tmux' },
+ { 'buffer', 'buffer' },
+ { 'file.basename', 'file_basename' },
+ { 'file.stem', 'file_stem' },
+ { 'file.dirname', 'file_dirname' },
+ { 'file.backed', 'file', 'file' },
+ { 'ssh.backed', 'ssh', 'ssh' },
+ { 'timestamp', 'timestamp', 'timestamp' },
+ { 'uuid', 'uuid' },
+}
+
+local function notify(msg, level)
+ vim.schedule(function()
+ vim.notify('extregs.nvim: ' .. msg, level or vim.log.levels.ERROR)
+ end)
+end
+
+local function guard(name, fn)
+ return function(...)
+ local ok, result = pcall(fn, ...)
+ if not ok then
+ notify(name .. ': ' .. tostring(result))
+ return false
+ end
+ return result
+ end
+end
+
+local function register(name, provider)
+ assert(type(provider.read) == 'function', name .. ': missing read()')
+ assert(type(provider.write) == 'function', name .. ': missing write()')
+ vim._extregs.register(name,
+ guard(name, function(reg)
+ if provider.readonly then
+ notify(name .. ' is read-only', vim.log.levels.WARN)
+ return false
+ end
+ return provider:write(reg)
+ end),
+ guard(name, function() return provider:read() end))
+end
+
+function M.setup(opts)
+ if vim.fn.has('extregs') ~= 1 or not (vim._extregs and vim._extregs.register) then
+ notify("requires a Neovim build with has('extregs') and vim._extregs.register")
+ return false
+ end
+ if M._initialized then return true end
+ local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), opts or {})
+ for _, spec in ipairs(providers) do
+ local name, module, section = unpack(spec)
+ local provider = require('extregs.' .. module).get_provider(cfg[section] or {})
+ register(name, provider)
+ end
+ M._initialized = true
+ return true
+end
+
+return M
diff --git a/lua/extregs/osc52.lua b/lua/extregs/osc52.lua
new file mode 100644
index 0000000..92fa12d
--- /dev/null
+++ b/lua/extregs/osc52.lua
@@ -0,0 +1,15 @@
+local M = {}
+function M.get_provider(cfg)
+ local clipboard = require('vim.ui.clipboard.osc52')
+ return {
+ read = function()
+ return false
+ end,
+ write = function(_, reg)
+ clipboard.copy(cfg.register)(reg.lines)
+ return true
+ end,
+ }
+end
+
+return M
diff --git a/lua/extregs/ssh.lua b/lua/extregs/ssh.lua
new file mode 100644
index 0000000..8102434
--- /dev/null
+++ b/lua/extregs/ssh.lua
@@ -0,0 +1,31 @@
+local util = require('extregs.util')
+local storage = require('extregs.storage')
+local M = {}
+
+local function remote_path(path)
+ -- Expand ~/ on the remote system; quote everything else.
+ if path:sub(1, 2) == '~/' then
+ return '"$HOME"/' .. util.shell_quote(path:sub(3))
+ end
+ return util.shell_quote(path)
+end
+
+function M.get_provider(cfg)
+ assert(type(cfg.path) == 'string' and cfg.path ~= '', 'ssh.path required')
+ local path = remote_path(cfg.path)
+ local function exec(script, input)
+ assert(type(cfg.host) == 'string' and cfg.host:match('^[%w_.%-]+@[%w_.%-]+$'),
+ 'ssh.host must be user@host')
+ -- SSH joins remote argv with spaces: quote the *whole* sh -c argument.
+ return util.system({ 'ssh', '-o', 'BatchMode=yes', cfg.host,
+ 'sh -c ' .. util.shell_quote(script) }, input, 'ssh')
+ end
+ return {
+ read = function() return storage.decode(exec('cat ' .. path)) end,
+ write = function(_, reg)
+ exec('mkdir -p "$(dirname ' .. path .. ')" && cat > ' .. path, storage.encode(reg))
+ return true
+ end,
+ }
+end
+return M
diff --git a/lua/extregs/storage.lua b/lua/extregs/storage.lua
new file mode 100644
index 0000000..c532485
--- /dev/null
+++ b/lua/extregs/storage.lua
@@ -0,0 +1,24 @@
+local M = {}
+M.cookie = 'NVIM_EXTREGS_JSON_V1'
+
+local function validate(reg)
+ if type(reg) ~= 'table' or type(reg.lines) ~= 'table' or type(reg.type) ~= 'string' then
+ error('invalid yankreg: expected { lines = {...}, type = "v"|"V"|blockwise }')
+ end
+ return reg
+end
+
+function M.encode(reg)
+ return M.cookie .. '\n' .. vim.json.encode(validate(reg)) .. '\n'
+end
+
+function M.decode(data)
+ if data:sub(1, #M.cookie + 1) == M.cookie .. '\n' then
+ return validate(vim.json.decode(data:sub(#M.cookie + 2)))
+ end
+ local lines = vim.split(data, '\n', { plain = true })
+ if #lines > 1 and lines[#lines] == '' then table.remove(lines) end
+ return { lines = lines, type = #lines > 1 and 'V' or 'v' }
+end
+
+return M
diff --git a/lua/extregs/timestamp.lua b/lua/extregs/timestamp.lua
new file mode 100644
index 0000000..6405285
--- /dev/null
+++ b/lua/extregs/timestamp.lua
@@ -0,0 +1,6 @@
+local util = require('extregs.util')
+local M = {}
+function M.get_provider(cfg)
+ return util.readonly(function() return os.date(cfg.format) end)
+end
+return M
diff --git a/lua/extregs/tmux.lua b/lua/extregs/tmux.lua
new file mode 100644
index 0000000..ea0299b
--- /dev/null
+++ b/lua/extregs/tmux.lua
@@ -0,0 +1,24 @@
+local util = require('extregs.util')
+local storage = require('extregs.storage')
+local M = {}
+function M.get_provider(cfg)
+ local function run(cmd, input)
+ local argv = { 'tmux', cmd }
+ if cfg.buffer ~= nil then
+ assert(type(cfg.buffer) == 'string' and cfg.buffer ~= '', 'tmux.buffer must be a nonempty string')
+ vim.list_extend(argv, { '-b', cfg.buffer })
+ end
+ if cmd == 'load-buffer' then argv[#argv + 1] = '-' end
+ return util.system(argv, input, 'tmux')
+ end
+ return {
+ read = function() return storage.decode(run('show-buffer')) end,
+ write = function(_, reg)
+ local data = table.concat(reg.lines, '\n')
+ if reg.type == 'V' then data = data .. '\n' end
+ run('load-buffer', data)
+ return true
+ end,
+ }
+end
+return M
diff --git a/lua/extregs/util.lua b/lua/extregs/util.lua
new file mode 100644
index 0000000..6dd1f00
--- /dev/null
+++ b/lua/extregs/util.lua
@@ -0,0 +1,31 @@
+local M = {}
+
+function M.textreg(s)
+ return { lines = { tostring(s) }, type = 'v' }
+end
+
+function M.readonly(getter)
+ return {
+ read = function() return M.textreg(getter()) end,
+ write = function() return false end,
+ readonly = true,
+ }
+end
+
+function M.path(path)
+ return vim.fn.expand(path)
+end
+
+function M.shell_quote(s)
+ return "'" .. s:gsub("'", "'\\''") .. "'"
+end
+
+function M.system(argv, input, label)
+ local result = vim.system(argv, { text = false, stdin = input }):wait()
+ if result.code ~= 0 then
+ error((label or argv[1]) .. ': ' .. (result.stderr or ('exit status ' .. result.code)))
+ end
+ return result.stdout or ''
+end
+
+return M
diff --git a/lua/extregs/uuid.lua b/lua/extregs/uuid.lua
new file mode 100644
index 0000000..e23a37f
--- /dev/null
+++ b/lua/extregs/uuid.lua
@@ -0,0 +1,16 @@
+local util = require('extregs.util')
+local M = {}
+function M.get_provider(_)
+ return util.readonly(function()
+ local bytes = vim.uv.random(16)
+ assert(bytes and #bytes == 16, 'unable to obtain random bytes')
+ local b = { bytes:byte(1, 16) }
+ b[7] = (b[7] % 16) + 64
+ b[9] = (b[9] % 64) + 128
+ local h = {}
+ for i = 1, 16 do h[i] = string.format('%%02x', b[i]) end
+ return table.concat(h, '', 1, 4) .. '-' .. table.concat(h, '', 5, 6) .. '-' ..
+ table.concat(h, '', 7, 8) .. '-' .. table.concat(h, '', 9, 10) .. '-' .. table.concat(h, '', 11, 16)
+ end)
+end
+return M
diff --git a/plugin/extregs.lua b/plugin/extregs.lua
new file mode 100644
index 0000000..095dd1b
--- /dev/null
+++ b/plugin/extregs.lua
@@ -0,0 +1,3 @@
+-- Register built-in extregs on startup. Optional customization may call setup()
+-- earlier; subsequent calls are harmless.
+require('extregs').setup()
diff --git a/tests/storage_spec.lua b/tests/storage_spec.lua
new file mode 100644
index 0000000..7da2bf7
--- /dev/null
+++ b/tests/storage_spec.lua
@@ -0,0 +1,9 @@
+-- Can be run under Neovim with: nvim --headless -u NONE -l tests/storage_spec.lua
+vim.opt.rtp:prepend(vim.fn.getcwd())
+local s = require('extregs.storage')
+local reg = { lines = { 'one', 'two' }, type = 'V', size = 2 }
+local got = s.decode(s.encode(reg))
+assert(vim.deep_equal(got, reg))
+assert(vim.deep_equal(s.decode('hello'), { lines = { 'hello' }, type = 'v' }))
+assert(vim.deep_equal(s.decode('hello\nworld\n'), { lines = { 'hello', 'world' }, type = 'V' }))
+print('storage tests passed')