Configure synchronization of files and directories through a plugin.
cleaned up state management
7 files changed, +99 -45
+2-2lua/syncmap/commands.lua
| @@ -10,8 +10,8 @@ function M.setup() | ||
| 10 | 10 | { desc = "Clears everything and starts all the inotify servers again" } |
| 11 | 11 | ) |
| 12 | 12 | vim.api.nvim_create_user_command( |
| 13 | - "SyncmapStop", | |
| 14 | - state.clear, | |
| 13 | + "SyncmapClean", | |
| 14 | + state.clean, | |
| 15 | 15 | { desc = "Clears the active map and all of it's processes" } |
| 16 | 16 | ) |
| 17 | 17 | vim.api.nvim_create_user_command( |
+6-0lua/syncmap/log.lua
| @@ -37,4 +37,10 @@ function M.error(msg) | ||
| 37 | 37 | M.log(msg, vim.log.levels.ERROR) |
| 38 | 38 | end |
| 39 | 39 | |
| 40 | +---Prints a message regardless of log level | |
| 41 | +---@param msg any | |
| 42 | +function M.print(msg) | |
| 43 | + vim.print("[syncmap] " .. msg) | |
| 44 | +end | |
| 45 | + | |
| 40 | 46 | return M |
+7-3lua/syncmap/rsync.lua
| @@ -28,17 +28,19 @@ function M.spawn_watcher(args) | ||
| 28 | 28 | }) |
| 29 | 29 | end |
| 30 | 30 | |
| 31 | + local tag = string.format("syncmap:%s->%s", args.src, args.dst) | |
| 32 | + | |
| 31 | 33 | local cmd = string.format( |
| 32 | - "while inotifywait -r -m -e modify,create,delete %q; do rsync %s %q %q; done #syncmap:%s", | |
| 34 | + "while inotifywait -r -m -e modify,create,delete %q; do rsync %s %q %q; done; : %s", | |
| 33 | 35 | args.src, |
| 34 | 36 | table.concat(args.flags, " "), |
| 35 | 37 | args.src, |
| 36 | 38 | args.dst, |
| 37 | - args.src | |
| 39 | + tag | |
| 38 | 40 | ) |
| 39 | 41 | local cmd_str = "sh -c '" .. cmd .. "'" |
| 40 | 42 | |
| 41 | - simple_cmd.spawn("sh", { | |
| 43 | + local _, pid = simple_cmd.spawn("sh", { | |
| 42 | 44 | args = { "-c", cmd }, |
| 43 | 45 | path = "sh", |
| 44 | 46 | cwd = args.src, |
| @@ -46,6 +48,8 @@ function M.spawn_watcher(args) | ||
| 46 | 48 | log.info(string.format("%s\n%d completed with %d code and %d status", cmd_str, pid, code, status)) |
| 47 | 49 | end, |
| 48 | 50 | }) |
| 51 | + log.info(string.format("%s\nPid is %d", cmd_str, pid)) | |
| 52 | + return pid | |
| 49 | 53 | end |
| 50 | 54 | |
| 51 | 55 | return M |
+27-1lua/syncmap/simple_cmd.lua
| @@ -2,6 +2,8 @@ local M = {} | ||
| 2 | 2 | |
| 3 | 3 | ---@alias StdIO integer|uv.uv_stream_t|nil |
| 4 | 4 | |
| 5 | +---@alias SpawnCommandCallback fun(code:integer, signal:integer, handle:uv.uv_process_t, pid:integer) | |
| 6 | + | |
| 5 | 7 | ---@class SpawnCommandParams |
| 6 | 8 | ---@field args string[] The arguments to run on the exectuable |
| 7 | 9 | ---@field cwd? string @[default=vim.fn.getcwd()] Set the current working directory for the sub-process. |
| @@ -13,7 +15,7 @@ local M = {} | ||
| 13 | 15 | ---@field verbatim? boolean @[default=false] If true, do not wrap any arguments in quotes, or perform any other escaping, when converting the argument list into a command line string. This option is only meaningful on Windows systems. On Unix it is silently ignored. |
| 14 | 16 | ---@field gid? integer @[default=vim.uv.getgid()] Set the child process' group id. |
| 15 | 17 | ---The callback to run after the command has returned |
| 16 | ----@field callback? fun(code:integer, signal:integer, handle:uv.uv_process_t, pid:integer) | |
| 18 | +---@field callback? SpawnCommandCallback | |
| 17 | 19 | ---@field close? boolean @[default=true] Request the handle to be closed (to simplify callbacks) |
| 18 | 20 | |
| 19 | 21 | ---A wrapper for uv.spawn that includes defaults. This makes it easier to spawn commands in the background. Also automatically closes the handle unless close is specified to false. |
| @@ -55,4 +57,28 @@ function M.spawn(path, p) | ||
| 55 | 57 | return handle, pid |
| 56 | 58 | end |
| 57 | 59 | |
| 60 | +---Kills a process and all of its descendents | |
| 61 | +---@param pid integer|string | |
| 62 | +function M.kill(pid) | |
| 63 | + vim.fn.system({ "kill", "-TERM", "-" .. tostring(pid) }) | |
| 64 | +end | |
| 65 | + | |
| 66 | +---Checks if a process is running | |
| 67 | +---@param pid string|integer | |
| 68 | +function M.exists(pid) | |
| 69 | + local result = false | |
| 70 | + local done = false | |
| 71 | + M.spawn("ps", { | |
| 72 | + args = { "-s", tostring(pid) }, | |
| 73 | + callback = function(code, _, _, _) | |
| 74 | + result = code == 0 | |
| 75 | + done = true | |
| 76 | + end, | |
| 77 | + }) | |
| 78 | + vim.wait(100, function() | |
| 79 | + return done | |
| 80 | + end, 10) | |
| 81 | + return result | |
| 82 | +end | |
| 83 | + | |
| 58 | 84 | return M |
+41-22lua/syncmap/state.lua
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | local rsync = require("syncmap.rsync") |
| 2 | 2 | local utils = require("syncmap.utils") |
| 3 | 3 | local log = require("syncmap.log") |
| 4 | +local simple_cmd = require("syncmap.simple_cmd") | |
| 4 | 5 | local M = { |
| 5 | 6 | opts = require("syncmap.default"), |
| 6 | 7 | } |
| @@ -28,7 +29,9 @@ local function create_state_file() | ||
| 28 | 29 | end |
| 29 | 30 | create_state_file() |
| 30 | 31 | |
| 31 | ----@alias RunningSrcs table<RsyncPath, boolean> | |
| 32 | +---@alias RunningPid string|integer The process id for a SyncmapWatchItem | |
| 33 | +---@alias RunningTag string A key based on src:dst for a SyncmapWatchItem | |
| 34 | +---@alias RunningSrcs table<RsyncPath, RunningPid> | |
| 32 | 35 | |
| 33 | 36 | ---A dictionairy keyed on the src for a match located in ~/.local/share/nvim/syncmap/state.json |
| 34 | 37 | ---@type RunningSrcs |
| @@ -50,30 +53,40 @@ load_state() | ||
| 50 | 53 | ---Removes stale states, calls reverse_rsync on new folders, and starts watch on dead pids |
| 51 | 54 | function M.sync() |
| 52 | 55 | local opts = M.opts |
| 53 | - ---@type table<RsyncPath, SyncmapConfigMatch> | |
| 56 | + ---@type table<RunningTag, SyncmapWatchItem> | |
| 54 | 57 | local lookup = {} |
| 55 | 58 | for _, m in ipairs(opts.map) do |
| 56 | - lookup[m[1]] = m | |
| 59 | + local tag = vim.fn.expand(m[1]) .. ":" .. vim.fn.expand(m[2]) | |
| 60 | + lookup[tag] = m | |
| 57 | 61 | end |
| 58 | 62 | |
| 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] | |
| 63 | + -- Iterate active and sync what exists | |
| 64 | + for tag, pid in pairs(M.active) do | |
| 65 | + if not lookup[tag] then | |
| 66 | + log.info(string.format("Removed from the config.\n%s\nRemoving the process %d.", tag, pid)) | |
| 67 | + M.active[tag] = nil | |
| 68 | + utils.kill(pid) | |
| 69 | + elseif not simple_cmd.exists(pid) then | |
| 70 | + log.info( | |
| 71 | + string.format( | |
| 72 | + "In active table but the process %d is not running.\n%s\nStarting a new process.", | |
| 73 | + pid, | |
| 74 | + tag | |
| 75 | + ) | |
| 76 | + ) | |
| 77 | + local m = lookup[tag] | |
| 65 | 78 | 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 | |
| 79 | + M.active[tag] = rsync.spawn_watcher(r) | |
| 69 | 80 | end |
| 70 | 81 | end |
| 71 | 82 | |
| 83 | + ---Iterate config and add new items | |
| 72 | 84 | for _, m in ipairs(opts.map) do |
| 73 | - if not M.active[m[1]] then | |
| 85 | + local tag = vim.fn.expand(m[1]) .. ":" .. vim.fn.expand(m[2]) | |
| 86 | + if not M.active[tag] then | |
| 87 | + log.info(string.format("%s\nIn the config but not in the active table. Starting a new process.", tag)) | |
| 74 | 88 | local r = utils.row_to_rsync_params(m) |
| 75 | - rsync.spawn_watcher(r) | |
| 76 | - M.active[r.src] = true | |
| 89 | + M.active[tag] = rsync.spawn_watcher(r) | |
| 77 | 90 | end |
| 78 | 91 | end |
| 79 | 92 | M.save() |
| @@ -82,13 +95,13 @@ end | ||
| 82 | 95 | function M.save() |
| 83 | 96 | local ok, encoded = pcall(vim.json.encode, M.active) |
| 84 | 97 | if not ok then |
| 85 | - utils.log("Failed to encode state", vim.log.levels.ERROR) | |
| 98 | + log.error("Failed to encode state") | |
| 86 | 99 | return |
| 87 | 100 | end |
| 88 | 101 | |
| 89 | 102 | local fd = io.open(M.state_file, "w") |
| 90 | 103 | if not fd then |
| 91 | - utils.log("Failed to open state file for writing", vim.log.levels.ERROR) | |
| 104 | + log.error("Failed to open state file for writing") | |
| 92 | 105 | return |
| 93 | 106 | end |
| 94 | 107 | |
| @@ -98,15 +111,17 @@ end | ||
| 98 | 111 | |
| 99 | 112 | ---Kills all the active inotifywatch servers |
| 100 | 113 | function M.killall() |
| 101 | - for src in pairs(M.active) do | |
| 102 | - utils.kill(src) | |
| 114 | + local s = "" | |
| 115 | + for tag, pid in pairs(M.active) do | |
| 116 | + s = string.format("%s\n%d: %s", s, pid, tag) | |
| 117 | + simple_cmd.kill(pid) | |
| 103 | 118 | end |
| 119 | + log.info(string.format("Killed the following watch items\n%s", s)) | |
| 104 | 120 | M.active = {} |
| 105 | 121 | end |
| 106 | 122 | |
| 107 | 123 | ---Clears the saved information by deleting the state file |
| 108 | -function M.clear() | |
| 109 | - M.killall() | |
| 124 | +function M.clean() | |
| 110 | 125 | local ok, err = os.remove(M.state_file) |
| 111 | 126 | if not ok then |
| 112 | 127 | log.error("Failed to delete state file: " .. err) |
| @@ -123,7 +138,11 @@ end | ||
| 123 | 138 | |
| 124 | 139 | ---List active |
| 125 | 140 | function M.show_active() |
| 126 | - vim.print(M.active) | |
| 141 | + local s = "" | |
| 142 | + for tag, pid in M.active do | |
| 143 | + s = string.format("%d: %s", pid, tag) | |
| 144 | + end | |
| 145 | + log.print("Current active watchers\n" .. s) | |
| 127 | 146 | end |
| 128 | 147 | |
| 129 | 148 | ---Shows the current options of Syncmap |
+3-3lua/syncmap/types.lua
| @@ -27,7 +27,7 @@ | ||
| 27 | 27 | ---A match to use for syncing using files or folders. |
| 28 | 28 | --- |
| 29 | 29 | ---Ex: `{"~/.dotfiles/nvim/", "~/.config/nvim/"}` sync everything within `~/.dotfiles/nvim/` into `~/.config/nvim/` |
| 30 | ----@class SyncmapConfigMatch | |
| 30 | +---@class SyncmapWatchItem | |
| 31 | 31 | ---@field [1] RsyncPath @[required] Path to sync from |
| 32 | 32 | ---@field [2] RsyncPath @[required] Path to sync to |
| 33 | 33 | ---@field reverse_sync_on_spawn? ReverseSyncOnSpawn @[default=parent.reverse_sync_on_startup] |
| @@ -36,7 +36,7 @@ | ||
| 36 | 36 | |
| 37 | 37 | ---Configurations for syncmap |
| 38 | 38 | ---@class SyncmapOpts |
| 39 | ----@field map? SyncmapConfigMatch[] @[default={}] The folders and files to keep synchronized | |
| 39 | +---@field map? SyncmapWatchItem[] @[default={}] The folders and files to keep synchronized | |
| 40 | 40 | ---@field reverse_sync_on_startup? ReverseSyncOnSpawn |
| 41 | 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 | 42 | ---@field log_level? LogLevel @[default="error"] Sets the log level for syncmap |
| @@ -44,7 +44,7 @@ | ||
| 44 | 44 | |
| 45 | 45 | ---Final config used in the plugin code. Use `SyncmapOpts` for your config |
| 46 | 46 | ---@class FinalSyncmapOpts |
| 47 | ----@field map SyncmapConfigMatch[] @[default={}] The folders and files to keep synchronized | |
| 47 | +---@field map SyncmapWatchItem[] @[default={}] The folders and files to keep synchronized | |
| 48 | 48 | ---@field reverse_sync_on_startup ReverseSyncOnSpawn |
| 49 | 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 | 50 | ---@field log_level LogLevel @[default="error"] Sets the log level for syncmap |
+13-14lua/syncmap/utils.lua
| @@ -3,7 +3,7 @@ local M = { | ||
| 3 | 3 | } |
| 4 | 4 | |
| 5 | 5 | ---Correctly extracts reverse_sync_on_spawn |
| 6 | ----@param row SyncmapConfigMatch | |
| 6 | +---@param row SyncmapWatchItem | |
| 7 | 7 | function M.extract_reverse(row) |
| 8 | 8 | if row.reverse_sync_on_spawn == nil then |
| 9 | 9 | if M.opts.reverse_sync_on_startup == nil then |
| @@ -49,7 +49,7 @@ function M.string_to_exclude_from(s) | ||
| 49 | 49 | end |
| 50 | 50 | |
| 51 | 51 | ---Gets the exclude from using parent pattern |
| 52 | ----@param m SyncmapConfigMatch | |
| 52 | +---@param m SyncmapWatchItem | |
| 53 | 53 | function M.extract_exclude_from(m) |
| 54 | 54 | if m.exclude_from == nil then |
| 55 | 55 | if M.opts.exclude_from ~= nil then |
| @@ -62,7 +62,7 @@ function M.extract_exclude_from(m) | ||
| 62 | 62 | end |
| 63 | 63 | |
| 64 | 64 | ---Makes sure to extract flags correctly |
| 65 | ----@param m SyncmapConfigMatch | |
| 65 | +---@param m SyncmapWatchItem | |
| 66 | 66 | ---@return RsyncFlag[] |
| 67 | 67 | function M.extract_flags(m) |
| 68 | 68 | local flags = m.rsync |
| @@ -80,7 +80,7 @@ function M.extract_flags(m) | ||
| 80 | 80 | return flags |
| 81 | 81 | end |
| 82 | 82 | |
| 83 | ----@param m SyncmapConfigMatch | |
| 83 | +---@param m SyncmapWatchItem | |
| 84 | 84 | function M.row_to_rsync_params(m) |
| 85 | 85 | local flags = M.extract_flags(m) |
| 86 | 86 | ---@type RsyncParams |
| @@ -92,16 +92,15 @@ function M.row_to_rsync_params(m) | ||
| 92 | 92 | } |
| 93 | 93 | end |
| 94 | 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 }) | |
| 95 | +---@param str string | |
| 96 | +---@param sep string | |
| 97 | +---@return string[] | |
| 98 | +function M.split(str, sep) | |
| 99 | + local result = {} | |
| 100 | + for part in string.gmatch(str, "([^" .. sep .. "]+)") do | |
| 101 | + table.insert(result, part) | |
| 102 | + end | |
| 103 | + return result | |
| 105 | 104 | end |
| 106 | 105 | |
| 107 | 106 | return M |