-- Copyright (c) 2024 Florian Fischer. All rights reserved. -- -- vis-lockfiles is free software: you can redistribute it and/or modify it under -- the terms of the GNU General Public License as published by the Free Software -- Foundation, either version 3 of the License, or (at your option) any later -- version. -- -- vis-lockfiles is distributed in the hope that it will be useful, but WITHOUT -- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -- FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License along with -- vis-lockfiles found in the LICENSE file. -- If not, see . local M = {} M.directory = '${XDG_STATE_HOME:-${HOME}/.local/state}/vis/locks' -- Helper to execute a command and capture its output local function capture_cmd(cmd) local p = assert(io.popen(cmd, 'r')) local s = assert(p:read('*a')) local success, _, status = p:close() if not success then local err = cmd .. ' failed with exit code: ' .. status vis:info('vis-lockfiles: ' .. err) end return s end local function lock_file_path(path) local filename = path:gsub('/', '%%') assert(os.execute('mkdir -p "' .. M.directory .. '"')) local lfile_path = capture_cmd('echo "' .. M.directory .. '/' .. filename .. '.lock"') return lfile_path:sub(1, #lfile_path - 1) -- trim newline from path end -- determine vis' pid local vis_pid do local vis_proc_file = io.open('/proc/self/stat', 'r') if vis_proc_file then vis_pid = vis_proc_file:read('*n') vis_proc_file:close() else -- fallback if /proc/self/stat is not available local out = capture_cmd('sh -c "echo $PPID"') vis_pid = tonumber(out) end end assert(vis_pid) -- get a user confirmation -- return true if user selected yes, false otherwise local function confirm(path) local choices = 'no\nyes' local prompt = path .. ' exists. Edit anyway:' local cmd = 'printf "' .. choices .. '" | vis-menu -b -p \'' .. prompt .. '\'' local choice = nil -- local status, output = vis:pipe(nil, {start = 0, finish = 0}, cmd) local menu = assert(io.popen(cmd)) local output = menu:read('*a') local _, _, status = menu:close() if status == 0 then -- trim newline from selection if output:sub(-1) == '\n' then choice = output:sub(1, -2) else choice = output end end vis:redraw() return choice == 'yes' end local created_locks = {} local function create_lock_file(lfile_path) local already_locked = os.execute('test -e ' .. '"' .. lfile_path .. '"') if already_locked then return false end -- create the lock file with our pid local cmd = 'echo ' .. vis_pid .. ' > "' .. lfile_path .. '"' assert(os.execute(cmd)) -- detect a possible race local lock_file = io.open(lfile_path) assert(lock_file) local lock_pid = lock_file:read('*n') lock_file:close() local locked = tonumber(lock_pid) == vis_pid -- remember we created the lock file if locked then created_locks[lfile_path] = true end return locked end local function delete_lock_file(lfile_path) os.execute('rm ' .. '"' .. lfile_path .. '"') created_locks[lfile_path] = nil end vis.events.subscribe(vis.events.FILE_OPEN, function(file) local lfile_path = lock_file_path(file.path) if not create_lock_file(lfile_path) and not confirm(lfile_path) then os.exit(1) end end) vis.events.subscribe(vis.events.FILE_CLOSE, function(file) local lfile_path = lock_file_path(file.path) if created_locks[lfile_path] then delete_lock_file(lfile_path) end end) return M