Configure synchronization of files and directories through a plugin.
initial commit
12 files changed, +533 -0
+9-0.gitignore
| @@ -0,0 +1,9 @@ | ||
| 1 | +*.log | |
| 2 | +/.repro | |
| 3 | +/.tests | |
| 4 | +/build | |
| 5 | +/debug | |
| 6 | +/doc/tags | |
| 7 | +foo.* | |
| 8 | +node_modules | |
| 9 | +tt.* |
+0-0.lua-format
No content changes (mode or rename only).
+100-0README.md
| @@ -0,0 +1,100 @@ | ||
| 1 | +# syncmap.nvim | |
| 2 | + | |
| 3 | +A lightweight Neovim plugin that keeps directories in sync using `inotifywait`, `rsync`, and some clever Vimscript + Lua integration. | |
| 4 | + | |
| 5 | +## Motivation | |
| 6 | + | |
| 7 | +- Dotfiles | |
| 8 | + | |
| 9 | +## Features | |
| 10 | + | |
| 11 | +- Automatically sync source → destination directories on file changes | |
| 12 | +- Uses `inotifywait`, `pgrep`, `pkill`, and `rsync` | |
| 13 | +- Supports `.gitignore`-like exclusions via `--exclude-from` | |
| 14 | +- Reverse sync on startup (optional) | |
| 15 | +- Logs and restarts lost watchers automatically | |
| 16 | +- Fully typed config for autocompletion and inline docs | |
| 17 | + | |
| 18 | +## Requirements | |
| 19 | + | |
| 20 | +- Linux (uses `inotifywait`) | |
| 21 | +- Neovim 0.8+ | |
| 22 | +- Tools: | |
| 23 | + - `inotifywait` (from `inotify-tools`) | |
| 24 | + - `rsync` | |
| 25 | + - `pgrep` and `pkill` (from `procps-ng`) | |
| 26 | + | |
| 27 | +## Installation | |
| 28 | + | |
| 29 | +Using [lazy.nvim](https://github.com/folke/lazy.nvim): | |
| 30 | + | |
| 31 | +```lua | |
| 32 | +{ | |
| 33 | + dir = vim.fn.expand("~/code/nvim-plugins/syncmap/"), | |
| 34 | + name = "syncmap.nvim", | |
| 35 | + opts = { | |
| 36 | + map = { | |
| 37 | + { vim.fn.expand("~/.dotfiles/test/"), vim.fn.expand("~/.config/test/") }, | |
| 38 | + }, | |
| 39 | + log_level = "info", | |
| 40 | + }, | |
| 41 | +} | |
| 42 | +```` | |
| 43 | + | |
| 44 | +## Default Options | |
| 45 | + | |
| 46 | +```lua | |
| 47 | +{ | |
| 48 | + map = { | |
| 49 | + { vim.fn.expand("~/.dotfiles/nvim/"), vim.fn.expand("~/.config/nvim/") }, | |
| 50 | + }, | |
| 51 | + reverse_sync_on_startup = true, | |
| 52 | + rsync = { "-a", "--delete" }, | |
| 53 | + log_level = "error", | |
| 54 | +} | |
| 55 | +``` | |
| 56 | + | |
| 57 | +## Type Hints | |
| 58 | + | |
| 59 | +For inline type hints and autocomplete, annotate your `opts` using: | |
| 60 | + | |
| 61 | +```lua | |
| 62 | +if false then | |
| 63 | + require("syncmap.lazy") | |
| 64 | +end | |
| 65 | +---@type SyncmapLazySpec | |
| 66 | +``` | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | +This gives you full visibility into available options and correct types. | |
| 71 | + | |
| 72 | +--- | |
| 73 | + | |
| 74 | +### LICENSE | |
| 75 | + | |
| 76 | +```txt | |
| 77 | +MIT License | |
| 78 | + | |
| 79 | +Copyright (c) 2025 Huncho | |
| 80 | + | |
| 81 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 82 | +of this software and associated documentation files (the "Software"), to deal | |
| 83 | +in the Software without restriction, including without limitation the rights | |
| 84 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 85 | +copies of the Software, and to permit persons to whom the Software is | |
| 86 | +furnished to do so, subject to the following conditions: | |
| 87 | + | |
| 88 | +The above copyright notice and this permission notice shall be included in all | |
| 89 | +copies or substantial portions of the Software. | |
| 90 | + | |
| 91 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 92 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 93 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 94 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 95 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 96 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 97 | +SOFTWARE. | |
| 98 | +``` | |
| 99 | + | |
| 100 | +--- |
+0-0doc/type.png
Binary file changed.
+11-0lua/syncmap/default.lua
| @@ -0,0 +1,11 @@ | ||
| 1 | +local expand = vim.fn.expand | |
| 2 | + | |
| 3 | +---@type FinalSyncmapOpts | |
| 4 | +return { | |
| 5 | + map = { | |
| 6 | + { expand("~/.dotfiles/nvim/"), expand("~/.config/nvim/") }, | |
| 7 | + }, | |
| 8 | + reverse_sync_on_startup = true, | |
| 9 | + rsync = { "-a", "--delete" }, | |
| 10 | + log_level = "error", | |
| 11 | +} |
+31-0lua/syncmap/init.lua
| @@ -0,0 +1,31 @@ | ||
| 1 | +local M = {} | |
| 2 | +M.state = require("syncmap.state") | |
| 3 | +M.rsync = require("syncmap.rsync") | |
| 4 | +M.utils = require("syncmap.utils") | |
| 5 | +M.default = require("syncmap.default") | |
| 6 | +M.log = require("syncmap.log") | |
| 7 | + | |
| 8 | +M.opts = M.default | |
| 9 | + | |
| 10 | +---Setup the sync map plugin | |
| 11 | +---@param opts SyncmapOpts | |
| 12 | +function M.setup(opts) | |
| 13 | + M.opts = vim.tbl_deep_extend("force", {}, M.default, opts or {}) | |
| 14 | + M.utils.opts = M.opts | |
| 15 | + M.state.opts = M.opts | |
| 16 | + M.rsync.opts = M.opts | |
| 17 | + M.log.opts = M.opts | |
| 18 | + M.state.sync(M.opts) | |
| 19 | +end | |
| 20 | + | |
| 21 | +---Shows the current state of Syncmap | |
| 22 | +function M.show_state() | |
| 23 | + vim.print(M.state.active) | |
| 24 | +end | |
| 25 | + | |
| 26 | +---Shows the current options of Syncmap | |
| 27 | +function M.show_opts() | |
| 28 | + vim.print(M.opts) | |
| 29 | +end | |
| 30 | + | |
| 31 | +return M |
+7-0lua/syncmap/lazy.lua
| @@ -0,0 +1,7 @@ | ||
| 1 | +require("lazy.types") | |
| 2 | + | |
| 3 | +---@class SyncMapOptsWrapper | |
| 4 | +---@field opts? SyncmapOpts | |
| 5 | + | |
| 6 | +---LazyVim Specs for syncmap | |
| 7 | +---@alias SyncmapLazySpec LazySpec|SyncMapOptsWrapper |
+38-0lua/syncmap/log.lua
| @@ -0,0 +1,38 @@ | ||
| 1 | +local M = { | |
| 2 | + opts = require("syncmap.default"), | |
| 3 | +} | |
| 4 | + | |
| 5 | +---@param opts FinalSyncmapOpts | |
| 6 | +function M.setup(opts) | |
| 7 | + M.opts = opts | |
| 8 | +end | |
| 9 | + | |
| 10 | +---@type table<LogLevel, vim.log.levels> | |
| 11 | +M.log_table = { | |
| 12 | + ["info"] = vim.log.levels.INFO, | |
| 13 | + ["error"] = vim.log.levels.ERROR, | |
| 14 | + ["none"] = vim.log.levels.OFF, | |
| 15 | +} | |
| 16 | + | |
| 17 | +---Logs a message based on options log level | |
| 18 | +---@param msg any | |
| 19 | +---@param level vim.log.levels | |
| 20 | +function M.log(msg, level) | |
| 21 | + if level >= M.log_table[M.opts.log_level] then | |
| 22 | + vim.notify("[syncmap] " .. msg, level) | |
| 23 | + end | |
| 24 | +end | |
| 25 | + | |
| 26 | +---Logs message as info | |
| 27 | +---@param msg any | |
| 28 | +function M.info(msg) | |
| 29 | + M.log(msg, vim.log.levels.INFO) | |
| 30 | +end | |
| 31 | + | |
| 32 | +---Logs message as error | |
| 33 | +---@param msg any | |
| 34 | +function M.error(msg) | |
| 35 | + M.log(msg, vim.log.levels.ERROR) | |
| 36 | +end | |
| 37 | + | |
| 38 | +return M |
+61-0lua/syncmap/rsync.lua
| @@ -0,0 +1,61 @@ | ||
| 1 | +local log = require("syncmap.log") | |
| 2 | +local utils = require("syncmap.utils") | |
| 3 | +local M = { | |
| 4 | + opts = require("syncmap.default"), | |
| 5 | +} | |
| 6 | + | |
| 7 | +---Runs rsync | |
| 8 | +---@param args RsyncParams | |
| 9 | +function M.run(args) | |
| 10 | + local cmd = { "rsync", unpack(args.flags), args.src, args.dst } | |
| 11 | + local result = vim.fn.system(cmd) | |
| 12 | + if vim.v.shell_error ~= 0 then | |
| 13 | + log.error(table.concat(cmd, " ") .. " failed:\n" .. result) | |
| 14 | + else | |
| 15 | + log.info(table.concat(cmd, " ") .. " succed") | |
| 16 | + end | |
| 17 | + return result | |
| 18 | +end | |
| 19 | + | |
| 20 | +---@param args RsyncParams | |
| 21 | +function M.spawn_watcher(args) | |
| 22 | + if M.opts.reverse_sync_on_startup then | |
| 23 | + M.run(args) | |
| 24 | + end | |
| 25 | + local cmd = string.format( | |
| 26 | + "while inotifywait -r -e modify,create,delete %q; do rsync %s %q %q; done", | |
| 27 | + args.src, | |
| 28 | + table.concat(utils.extract_flags(args.flags), " "), | |
| 29 | + args.src, | |
| 30 | + args.dst | |
| 31 | + ) | |
| 32 | + | |
| 33 | + local handle | |
| 34 | + handle, _ = vim.uv.spawn("sh", { | |
| 35 | + args = { | |
| 36 | + "-c", | |
| 37 | + string.format( | |
| 38 | + "while inotifywait -r -e modify,create,delete %q; do rsync %s %q %q; done", | |
| 39 | + args.src, | |
| 40 | + table.concat(args.flags, " "), | |
| 41 | + args.src, | |
| 42 | + args.dst | |
| 43 | + ), | |
| 44 | + }, | |
| 45 | + stdio = { nil, nil, nil }, | |
| 46 | + cwd = args.src, | |
| 47 | + env = vim.fn.environ(), | |
| 48 | + detached = true, | |
| 49 | + hide = true, | |
| 50 | + ---@diagnostic disable-next-line: assign-type-mismatch | |
| 51 | + uid = vim.uv.getuid(), | |
| 52 | + verbatim = false, | |
| 53 | + ---@diagnostic disable-next-line: assign-type-mismatch | |
| 54 | + gid = vim.uv.getgid(), | |
| 55 | + }, function(code, signal) | |
| 56 | + log.info(cmd .. "\nEnded with code " .. code .. " and signal " .. signal) | |
| 57 | + handle:close() | |
| 58 | + end) | |
| 59 | +end | |
| 60 | + | |
| 61 | +return M |
+111-0lua/syncmap/state.lua
| @@ -0,0 +1,111 @@ | ||
| 1 | +local rsync = require("syncmap.rsync") | |
| 2 | +local utils = require("syncmap.utils") | |
| 3 | +local log = require("syncmap.log") | |
| 4 | +local M = { | |
| 5 | + opts = require("syncmap.default"), | |
| 6 | +} | |
| 7 | + | |
| 8 | +M.dir = vim.fn.expand("~/.local/share/nvim/syncmap/") | |
| 9 | + | |
| 10 | +---Make sure the directory exists | |
| 11 | +if vim.fn.isdirectory(M.dir) == 0 then | |
| 12 | + vim.fn.mkdir(M.dir, "p") | |
| 13 | +end | |
| 14 | + | |
| 15 | +M.state_file = vim.fn.expand("~/.local/share/nvim/syncmap/state.json") | |
| 16 | + | |
| 17 | +-- Create file if it doesn't exist | |
| 18 | +local function create_state_file() | |
| 19 | + if vim.fn.filereadable(M.state_file) == 0 then | |
| 20 | + local fd = io.open(M.state_file, "w") | |
| 21 | + if fd then | |
| 22 | + fd:write("{}") -- empty JSON object | |
| 23 | + fd:close() | |
| 24 | + else | |
| 25 | + utils.log("Failed to create state file", vim.log.levels.ERROR) | |
| 26 | + end | |
| 27 | + end | |
| 28 | +end | |
| 29 | +create_state_file() | |
| 30 | + | |
| 31 | +---@alias RunningSrcs table<RsyncPath, boolean> | |
| 32 | + | |
| 33 | +---A dictionairy keyed on the src for a match located in ~/.local/share/nvim/syncmap/state.json | |
| 34 | +---@type RunningSrcs | |
| 35 | +M.active = {} | |
| 36 | + | |
| 37 | +---Load the existing config if possible | |
| 38 | +local function load_state() | |
| 39 | + local content = vim.fn.readfile(M.state_file) | |
| 40 | + local joined = table.concat(content, "\n") | |
| 41 | + local ok, parsed = pcall(vim.json.decode, joined) | |
| 42 | + if ok and type(parsed) == "table" then | |
| 43 | + M.active = parsed | |
| 44 | + else | |
| 45 | + utils.log("Failed to parse state file", vim.log.levels.ERROR) | |
| 46 | + end | |
| 47 | +end | |
| 48 | +load_state() | |
| 49 | + | |
| 50 | +---Removes stale states, calls reverse_rsync on new folders, and starts watch on dead pids | |
| 51 | +---@param opts FinalSyncmapOpts | |
| 52 | +function M.sync(opts) | |
| 53 | + ---@type table<RsyncPath, SyncmapConfigMatch> | |
| 54 | + local lookup = {} | |
| 55 | + for _, m in ipairs(opts.map) do | |
| 56 | + lookup[m[1]] = m | |
| 57 | + end | |
| 58 | + | |
| 59 | + for src in pairs(M.active) do | |
| 60 | + if not lookup[src] then | |
| 61 | + M.active[src] = nil | |
| 62 | + utils.kill(src) | |
| 63 | + elseif utils.search(src) == "" then | |
| 64 | + local m = lookup[src] | |
| 65 | + local r = utils.row_to_rsync_params(m) | |
| 66 | + log.info("Couldn't find process for " .. m[1] .. " " .. m[2]) | |
| 67 | + rsync.spawn_watcher(r) | |
| 68 | + M.active[r.src] = true | |
| 69 | + end | |
| 70 | + end | |
| 71 | + | |
| 72 | + for _, m in ipairs(opts.map) do | |
| 73 | + if not M.active[m[1]] then | |
| 74 | + local r = utils.row_to_rsync_params(m) | |
| 75 | + rsync.spawn_watcher(r) | |
| 76 | + M.active[r.src] = true | |
| 77 | + end | |
| 78 | + end | |
| 79 | + M.save() | |
| 80 | +end | |
| 81 | + | |
| 82 | +function M.save() | |
| 83 | + local ok, encoded = pcall(vim.json.encode, M.active) | |
| 84 | + if not ok then | |
| 85 | + utils.log("Failed to encode state", vim.log.levels.ERROR) | |
| 86 | + return | |
| 87 | + end | |
| 88 | + | |
| 89 | + local fd = io.open(M.state_file, "w") | |
| 90 | + if not fd then | |
| 91 | + utils.log("Failed to open state file for writing", vim.log.levels.ERROR) | |
| 92 | + return | |
| 93 | + end | |
| 94 | + | |
| 95 | + fd:write(encoded) | |
| 96 | + fd:close() | |
| 97 | +end | |
| 98 | + | |
| 99 | +---Clears the saved information by deleting the state file | |
| 100 | +function M.clear() | |
| 101 | + for _, a in ipairs(M.active) do | |
| 102 | + end | |
| 103 | + local ok, err = os.remove(M.state_file) | |
| 104 | + if not ok then | |
| 105 | + log.error("Failed to delete state file: " .. err) | |
| 106 | + else | |
| 107 | + log.info("State file deleted") | |
| 108 | + end | |
| 109 | +end | |
| 110 | + | |
| 111 | +return M |
+58-0lua/syncmap/types.lua
| @@ -0,0 +1,58 @@ | ||
| 1 | +---A file or folder to use with rsync | |
| 2 | +---Keep in mind that slash matters. A final slash means to use the contents so you most likely want to use slashes at the end of folders. | |
| 3 | +--- | |
| 4 | +---A file or folder to use with rsync. | |
| 5 | +---⚠️ A trailing slash means \"sync the contents\" — without it, rsync syncs the whole folder. | |
| 6 | +---You usually want a slash at the end (e.g., `"~/.dotfiles/nvim/"`) | |
| 7 | +---@class RsyncPath : string | |
| 8 | + | |
| 9 | +---@alias ReverseSyncOnSpawn boolean @[default=true] | |
| 10 | + | |
| 11 | +---@alias RsyncFlag | |
| 12 | +---| "-a" # archive: preserve everything | |
| 13 | +---| "--delete" # delete files not in source | |
| 14 | +---| "-v" # verbose output | |
| 15 | +---| "-z" # compress during transfer | |
| 16 | +---| "--dry-run" # simulate the sync | |
| 17 | +---| string # allow custom flags too | |
| 18 | + | |
| 19 | +---@alias LogLevel | |
| 20 | +---| "info" show info logs | |
| 21 | +---| "error" show error logs | |
| 22 | +---| "none" show no logs | |
| 23 | + | |
| 24 | +---Ex: `--exclude-from='.gitignore'` | |
| 25 | +---@alias ExcludeFrom string @[default=".gitignore"] Adds --exclude-from in rsync | |
| 26 | + | |
| 27 | +---A match to use for syncing using files or folders. | |
| 28 | +--- | |
| 29 | +---Ex: `{vim.fn.expand("~/.dotfiles/nvim/"), vim.fn.expand("~/.config/nvim/")}` sync everything within `~/.dotfiles/nvim` into `~/.config/nvim` | |
| 30 | +---@class SyncmapConfigMatch | |
| 31 | +---@field [1] RsyncPath @[required] Path to sync from | |
| 32 | +---@field [2] RsyncPath @[required] Path to sync to | |
| 33 | +---@field reverse_sync_on_spawn? ReverseSyncOnSpawn @[default=parent.reverse_sync_on_startup] | |
| 34 | +---@field rsync? RsyncFlag[] @[default=parent.rsync] Flags to use with rsync | |
| 35 | +---@field exclude_from? ExcludeFrom @[default=parent.exclude_from] | |
| 36 | + | |
| 37 | +---Configurations for syncmap | |
| 38 | +---@class SyncmapOpts | |
| 39 | +---@field map? SyncmapConfigMatch[] @[default=~/.dotfiles/nvim/ ~/.config/nvim] The folders and files to keep synchronized | |
| 40 | +---@field reverse_sync_on_startup? ReverseSyncOnSpawn | |
| 41 | +---@field rsync? RsyncFlag[] @[default={"-a", "--delete"}] Rsync flags that will be used if a map item doesn't include anything. I.E. Default flags | |
| 42 | +---@field log_level? LogLevel @[default="error"] Sets the log level for syncmap | |
| 43 | +---@field exclude_from? ExcludeFrom | |
| 44 | + | |
| 45 | +---Final config | |
| 46 | +---@class FinalSyncmapOpts | |
| 47 | +---@field map SyncmapConfigMatch[] @[default=~/.dotfiles/nvim/ ~/.config/nvim] The folders and files to keep synchronized | |
| 48 | +---@field reverse_sync_on_startup ReverseSyncOnSpawn | |
| 49 | +---@field rsync RsyncFlag[] @[default={"-a", "--delete"}] Rsync flags that will be used if a map item doesn't include anything. I.E. Default flags | |
| 50 | +---@field log_level LogLevel @[default="error"] Sets the log level for syncmap | |
| 51 | +---@field exclude_from? ExcludeFrom | |
| 52 | + | |
| 53 | +---Parameters pased into rsync methods | |
| 54 | +---@class RsyncParams | |
| 55 | +---@field src RsyncPath Source path to sync from | |
| 56 | +---@field dst RsyncPath Destination path to sync from | |
| 57 | +---@field flags? RsyncFlag[] @[default={}] Flags used while running rsync. Default to {} for safety precautions. | |
| 58 | +---@field reverse_sync_on_spawn ReverseSyncOnSpawn |
+107-0lua/syncmap/utils.lua
| @@ -0,0 +1,107 @@ | ||
| 1 | +local M = { | |
| 2 | + opts = require("syncmap.default"), | |
| 3 | +} | |
| 4 | + | |
| 5 | +---Correctly extracts reverse_sync_on_spawn | |
| 6 | +---@param row SyncmapConfigMatch | |
| 7 | +function M.extract_reverse(row) | |
| 8 | + if row.reverse_sync_on_spawn == nil then | |
| 9 | + if M.opts.reverse_sync_on_startup == nil then | |
| 10 | + return true | |
| 11 | + else | |
| 12 | + return M.opts.reverse_sync_on_startup | |
| 13 | + end | |
| 14 | + else | |
| 15 | + return row.reverse_sync_on_spawn | |
| 16 | + end | |
| 17 | +end | |
| 18 | + | |
| 19 | +---Checks if a file exists | |
| 20 | +---@param path string | |
| 21 | +local function file_exists(path) | |
| 22 | + local f = io.open(path, "r") | |
| 23 | + if f then | |
| 24 | + f:close() | |
| 25 | + return true | |
| 26 | + end | |
| 27 | + return false | |
| 28 | +end | |
| 29 | + | |
| 30 | +---Checks if a path is a directory | |
| 31 | +---@param path string | |
| 32 | +local function is_dir(path) | |
| 33 | + local stat = vim.uv.fs_stat(path) | |
| 34 | + return stat and stat.type == "directory" | |
| 35 | +end | |
| 36 | + | |
| 37 | +--- Ensures trailing slash | |
| 38 | +---@param path string | |
| 39 | +local function with_trailing_slash(path) | |
| 40 | + if path:sub(-1) ~= "/" then | |
| 41 | + return path .. "/" | |
| 42 | + end | |
| 43 | + return path | |
| 44 | +end | |
| 45 | + | |
| 46 | +---@param s string | |
| 47 | +function M.string_to_exclude_from(s) | |
| 48 | + return "--exclude_from='" .. s .. "'" | |
| 49 | +end | |
| 50 | + | |
| 51 | +---Gets the exclude from using parent pattern | |
| 52 | +---@param m SyncmapConfigMatch | |
| 53 | +function M.extract_exclude_from(m) | |
| 54 | + if m.exclude_from == nil then | |
| 55 | + if M.opts.exclude_from ~= nil then | |
| 56 | + return M.string_to_exclude_from(M.opts.exclude_from) | |
| 57 | + end | |
| 58 | + return nil | |
| 59 | + else | |
| 60 | + return M.string_to_exclude_from(m.exclude_from) | |
| 61 | + end | |
| 62 | +end | |
| 63 | + | |
| 64 | +---Makes sure to extract flags correctly | |
| 65 | +---@param m SyncmapConfigMatch | |
| 66 | +---@return RsyncFlag[] | |
| 67 | +function M.extract_flags(m) | |
| 68 | + local flags = m.rsync | |
| 69 | + if flags == nil then | |
| 70 | + flags = M.opts.rsync | |
| 71 | + end | |
| 72 | + if m.exclude_from == nil then | |
| 73 | + return flags | |
| 74 | + end | |
| 75 | + if is_dir(m[1]) then | |
| 76 | + if file_exists(with_trailing_slash(m[1]) .. m.exclude_from) then | |
| 77 | + table.insert(flags, M.extract_exclude_from(m)) | |
| 78 | + end | |
| 79 | + end | |
| 80 | + return flags | |
| 81 | +end | |
| 82 | + | |
| 83 | +---@param m SyncmapConfigMatch | |
| 84 | +function M.row_to_rsync_params(m) | |
| 85 | + local flags = M.extract_flags(m) | |
| 86 | + ---@type RsyncParams | |
| 87 | + return { | |
| 88 | + src = m[1], | |
| 89 | + dst = m[2], | |
| 90 | + flags = flags, | |
| 91 | + reverse_sync_on_spawn = M.extract_reverse(m), | |
| 92 | + } | |
| 93 | +end | |
| 94 | + | |
| 95 | +---Kills an inotifywait instance based on the src name | |
| 96 | +---@param src string | |
| 97 | +function M.kill(src) | |
| 98 | + vim.fn.system({ "pkill", "-f", "^inotifywait.*" .. src }) | |
| 99 | +end | |
| 100 | + | |
| 101 | +---Searches for an existing inotifywait instance based on the src name | |
| 102 | +---@param src string | |
| 103 | +function M.search(src) | |
| 104 | + return vim.fn.system({ "pgrep", "-f", "^inotifywait.*" .. src }) | |
| 105 | +end | |
| 106 | + | |
| 107 | +return M |