mirror of
https://github.com/kristoferssolo/solorice.git
synced 2026-02-04 06:32:03 +00:00
fix(yazi): update plugins
This commit is contained in:
386
config/yazi/plugins/time-travel.yazi/main.lua
Normal file
386
config/yazi/plugins/time-travel.yazi/main.lua
Normal file
@@ -0,0 +1,386 @@
|
||||
---@param msg string
|
||||
local notify_warn = function(msg)
|
||||
ya.notify { title = "ZFS", content = msg, level = "warn", timeout = 5 }
|
||||
end
|
||||
|
||||
---@param msg string
|
||||
local notify_error = function(msg)
|
||||
ya.notify { title = "ZFS", content = msg, level = "error", timeout = 5 }
|
||||
end
|
||||
|
||||
---@param arr table
|
||||
---@param predicate fun(value: any): boolean
|
||||
---@return number|nil # index if found, nil if not found
|
||||
local find_index = function(arr, predicate)
|
||||
for i, value in ipairs(arr) do
|
||||
if predicate(value) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Verify if `sudo` is already authenticated
|
||||
--- @return boolean
|
||||
local function sudo_already()
|
||||
local status = Command("sudo"):args({ "--validate", "--non-interactive" }):status()
|
||||
assert(status, "Failed to run `sudo --validate --non-interactive`")
|
||||
return status.success
|
||||
end
|
||||
|
||||
--- Run a program with `sudo` privilege
|
||||
--- @param program string
|
||||
--- @param args table
|
||||
--- @return Output|nil output
|
||||
--- @return integer|nil err
|
||||
--- nil: no error
|
||||
--- 1: sudo failed
|
||||
local function run_with_sudo(program, args)
|
||||
local cmd = Command("sudo"):args({ program, table.unpack(args) }):stdout(Command.PIPED):stderr(Command.PIPED)
|
||||
if sudo_already() then
|
||||
return cmd:output()
|
||||
end
|
||||
|
||||
local permit = ya.hide()
|
||||
print(string.format("Sudo password required to run: `%s %s`", program, table.concat(args, " ")))
|
||||
local output = cmd:output()
|
||||
permit:drop()
|
||||
|
||||
if output.status.success or sudo_already() then
|
||||
return output
|
||||
end
|
||||
return nil, 1
|
||||
end
|
||||
|
||||
|
||||
---@return string
|
||||
local get_cwd = ya.sync(function()
|
||||
return tostring(cx.active.current.cwd)
|
||||
end)
|
||||
|
||||
---@param s string
|
||||
---@return string
|
||||
local trim = function(s)
|
||||
return s:match("^%s*(.-)%s*$")
|
||||
end
|
||||
|
||||
---@param cwd string
|
||||
---@return string|nil
|
||||
local get_filesystem_type = function(cwd)
|
||||
local stat, _ = Command("stat"):args({ "-f", "-c", "%T", cwd }):output()
|
||||
if not stat.status.success then
|
||||
return nil
|
||||
end
|
||||
return trim(stat.stdout)
|
||||
end
|
||||
|
||||
---@param cwd string
|
||||
---@return string|nil
|
||||
local zfs_dataset = function(cwd)
|
||||
local df, _ = Command("df"):args({ "--output=source", cwd }):output()
|
||||
local dataset = nil
|
||||
for line in df.stdout:gmatch("[^\r\n]+") do
|
||||
-- dataset is last line in output
|
||||
dataset = line
|
||||
end
|
||||
return dataset
|
||||
end
|
||||
|
||||
---@param dataset string
|
||||
---@return string|nil
|
||||
local zfs_mountpoint = function(dataset)
|
||||
local zfs, _ = Command("zfs"):args({ "get", "-H", "-o", "value", "mountpoint", dataset }):output()
|
||||
|
||||
-- not a dataset!
|
||||
if not zfs.status.success then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- legacy mountpoint, search for actual mountpoint using df
|
||||
if zfs.stdout == "legacy\n" then
|
||||
local df, _ = Command("df"):output()
|
||||
if not df.status.success then
|
||||
return nil
|
||||
end
|
||||
|
||||
for line in df.stdout:gmatch("[^\r\n]+") do
|
||||
-- match start of line
|
||||
if string.sub(line, 1, #dataset) == dataset then
|
||||
local mountpoint = nil
|
||||
for field in line:gmatch("%S+") do
|
||||
-- mountpoint is last field in df output
|
||||
mountpoint = field
|
||||
end
|
||||
return mountpoint
|
||||
end
|
||||
end
|
||||
else
|
||||
return zfs.stdout:gsub("\n$", "")
|
||||
end
|
||||
|
||||
-- shouldn't be here
|
||||
return nil
|
||||
end
|
||||
|
||||
-- returns the path relative to the mountpoint / snapshot
|
||||
---@param cwd string
|
||||
---@param mountpoint string
|
||||
local zfs_relative = function(cwd, mountpoint)
|
||||
-- relative path to get mountpoint
|
||||
local relative = (cwd:sub(0, #mountpoint) == mountpoint) and cwd:sub(#mountpoint + 1) or cwd
|
||||
|
||||
-- is a snapshot dir, strip everything after "/snapshot"
|
||||
if cwd:find(".zfs/snapshot") ~= nil then
|
||||
local snapshot_pos = cwd:find("/snapshot")
|
||||
|
||||
-- everything after the "/snapshot/"
|
||||
local after = cwd:sub(snapshot_pos + #"/snapshot" + 1)
|
||||
local first_slash = after:find("/")
|
||||
-- root of snapshot?
|
||||
if first_slash == nil then
|
||||
return "/"
|
||||
else
|
||||
return after:sub(first_slash)
|
||||
end
|
||||
end
|
||||
|
||||
return relative
|
||||
end
|
||||
|
||||
---@class Snapshot
|
||||
---@field name string
|
||||
---@field path string
|
||||
|
||||
---@param dataset string
|
||||
---@param mountpoint string
|
||||
---@param relative string
|
||||
---@return Snapshot[]
|
||||
local zfs_snapshots = function(dataset, mountpoint, relative)
|
||||
-- -S is for reverse order
|
||||
local zfs_snapshots, _ = Command("zfs"):args({ "list", "-H", "-t", "snapshot", "-o", "name", "-S", "creation",
|
||||
dataset })
|
||||
:output()
|
||||
|
||||
if not zfs_snapshots.status.success then
|
||||
return {}
|
||||
end
|
||||
|
||||
---@type Snapshot[]
|
||||
local snapshots = {}
|
||||
for snapshot in zfs_snapshots.stdout:gmatch("[^\r\n]+") do
|
||||
-- in the format dataset@snapshot
|
||||
local sep = snapshot:find("@")
|
||||
local id = snapshot:sub(sep + 1)
|
||||
|
||||
table.insert(snapshots, {
|
||||
id = id,
|
||||
path = mountpoint .. "/.zfs/snapshot/" .. id .. relative,
|
||||
})
|
||||
end
|
||||
return snapshots
|
||||
end
|
||||
|
||||
---@param cwd string
|
||||
---@return string|nil
|
||||
local function btrfs_mountpoint(cwd)
|
||||
local cmd, _ = Command("findmnt"):args({ "-no", "TARGET", "-T", cwd }):output()
|
||||
if not cmd.status.success then
|
||||
return nil
|
||||
end
|
||||
return trim(cmd.stdout)
|
||||
end
|
||||
|
||||
---Returns the current uuid and the parent uuid
|
||||
---@param cwd string
|
||||
---@return string|nil, string|nil
|
||||
local function btrfs_uuids(cwd)
|
||||
local cmd, _ = run_with_sudo("btrfs", { "subvolume", "show", cwd })
|
||||
if not cmd then
|
||||
return nil
|
||||
end
|
||||
|
||||
local parent_uuid = nil
|
||||
local uuid = nil
|
||||
for line in cmd.stdout:gmatch("[^\r\n]+") do
|
||||
local parent_uuid_re = line:match("^%s*Parent UUID:%s*(%S+)")
|
||||
if parent_uuid_re then
|
||||
parent_uuid = trim(parent_uuid_re)
|
||||
end
|
||||
|
||||
local uuid_re = line:match("^%s*UUID:%s*(%S+)")
|
||||
if uuid_re then
|
||||
uuid = trim(uuid_re)
|
||||
end
|
||||
end
|
||||
return parent_uuid, uuid
|
||||
end
|
||||
|
||||
---@param mountpoint string
|
||||
---@param current_uuid string
|
||||
---@param current_parent_uuid string|nil
|
||||
---@return { snapshots: Snapshot[], latest_path: string, current_snapshot_id: string }
|
||||
local function btrfs_snapshots(mountpoint, current_uuid, current_parent_uuid)
|
||||
local snapshots_cmd, _ = run_with_sudo("btrfs", { "subvolume", "list", "-q", "-u", mountpoint })
|
||||
if not snapshots_cmd then
|
||||
return {}
|
||||
end
|
||||
|
||||
local snapshots = {}
|
||||
local latest_path = ""
|
||||
local current_snapshot_id = ""
|
||||
|
||||
for line in snapshots_cmd.stdout:gmatch("[^\r\n]+") do
|
||||
local pattern = "ID (%d+) gen %d+ top level %d+ parent_uuid ([%w-]+)%s+uuid ([%w-]+) path (%S+)"
|
||||
-- Extract the fields
|
||||
local subvol_id, parent_uuid, uuid, name = line:match(pattern)
|
||||
parent_uuid = trim(parent_uuid)
|
||||
|
||||
local path = mountpoint .. "/" .. name
|
||||
local is_parent = false
|
||||
|
||||
if current_parent_uuid == "-" then
|
||||
if parent_uuid == "-" and uuid == current_uuid then
|
||||
is_parent = true
|
||||
end
|
||||
else
|
||||
if uuid == current_parent_uuid then
|
||||
is_parent = true
|
||||
end
|
||||
end
|
||||
|
||||
if is_parent then
|
||||
latest_path = path
|
||||
end
|
||||
|
||||
if uuid == current_uuid and not is_parent then
|
||||
current_snapshot_id = name
|
||||
end
|
||||
|
||||
if not is_parent then
|
||||
table.insert(snapshots, {
|
||||
id = name,
|
||||
subvol_id = subvol_id, -- used only for sorting
|
||||
path = path,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Sort snapshots by time descending
|
||||
table.sort(snapshots, function(a, b)
|
||||
return a.subvol_id > b.subvol_id
|
||||
end)
|
||||
|
||||
return { snapshots = snapshots, latest_path = latest_path, current_snapshot_id = current_snapshot_id }
|
||||
end
|
||||
|
||||
return {
|
||||
entry = function(_, job)
|
||||
local action = job.args[1]
|
||||
local cwd = get_cwd()
|
||||
|
||||
if action ~= "exit" and action ~= "prev" and action ~= "next" then
|
||||
return notify_error("Invalid action: " .. action)
|
||||
end
|
||||
|
||||
local fs_type = get_filesystem_type(cwd)
|
||||
if fs_type ~= "zfs" and fs_type ~= "btrfs" then
|
||||
return notify_error("Current directory is not on a BTRFS / ZFS filesystem.")
|
||||
end
|
||||
|
||||
local current_snapshot_id = ""
|
||||
local latest_path = ""
|
||||
local snapshots = {}
|
||||
|
||||
if fs_type == "zfs" then
|
||||
local dataset = zfs_dataset(cwd)
|
||||
if dataset == nil then
|
||||
return notify_error("Current directory is not within a ZFS dataset.")
|
||||
end
|
||||
|
||||
if cwd:find(".zfs/snapshot") ~= nil then
|
||||
-- in the format dataset@snapshot
|
||||
local sep = dataset:find("@")
|
||||
current_snapshot_id = dataset:sub(sep + 1)
|
||||
dataset = dataset:sub(1, sep - 1)
|
||||
end
|
||||
|
||||
local mountpoint = zfs_mountpoint(dataset)
|
||||
if mountpoint == nil then
|
||||
return notify_error("Current directory is not within a ZFS dataset.")
|
||||
end
|
||||
|
||||
-- NOTE: relative already has leading "/"
|
||||
local relative = zfs_relative(cwd, mountpoint)
|
||||
|
||||
latest_path = mountpoint .. relative
|
||||
snapshots = zfs_snapshots(dataset, mountpoint, relative)
|
||||
elseif fs_type == "btrfs" then
|
||||
local mountpoint = btrfs_mountpoint(cwd)
|
||||
local parent_uuid, uuid = btrfs_uuids(cwd)
|
||||
|
||||
if mountpoint == nil or uuid == nil then
|
||||
return notify_error("Current directory is not within a BTRFS subvolume.")
|
||||
end
|
||||
|
||||
local ret = btrfs_snapshots(mountpoint, uuid, parent_uuid)
|
||||
snapshots = ret.snapshots
|
||||
latest_path = ret.latest_path
|
||||
current_snapshot_id = ret.current_snapshot_id
|
||||
end
|
||||
|
||||
if action == "exit" then
|
||||
ya.manager_emit("cd", { latest_path })
|
||||
return
|
||||
end
|
||||
|
||||
if #snapshots == 0 then
|
||||
return notify_warn("No snapshots found.")
|
||||
end
|
||||
|
||||
---@param start_idx integer
|
||||
---@param end_idx integer
|
||||
---@param step integer
|
||||
local find_and_goto_snapshot = function(start_idx, end_idx, step)
|
||||
if start_idx == 0 then
|
||||
-- going from newest snapshot to current state
|
||||
return ya.manager_emit("cd", { latest_path })
|
||||
elseif start_idx < 0 then
|
||||
return notify_warn("No earlier snapshots found.")
|
||||
elseif start_idx > #snapshots then
|
||||
return notify_warn("No earlier snapshots found.")
|
||||
end
|
||||
|
||||
for i = start_idx, end_idx, step do
|
||||
local snapshot_dir = snapshots[i].path
|
||||
if io.open(snapshot_dir, "r") then
|
||||
return ya.manager_emit("cd", { snapshot_dir })
|
||||
end
|
||||
end
|
||||
|
||||
local direction = action == "prev" and "earlier" or "later"
|
||||
return notify_warn("No " .. direction .. " snapshots found.")
|
||||
end
|
||||
|
||||
-- NOTE: latest snapshot is first in list
|
||||
if current_snapshot_id == "" then
|
||||
if action == "prev" then
|
||||
-- go to latest snapshot
|
||||
return find_and_goto_snapshot(1, #snapshots, 1)
|
||||
elseif action == "next" then
|
||||
return notify_warn("No later snapshots found.")
|
||||
end
|
||||
end
|
||||
|
||||
-- has current snapshot
|
||||
local idx = find_index(snapshots, function(snapshot) return snapshot.id == current_snapshot_id end)
|
||||
if idx == nil then
|
||||
return notify_error("Snapshot not found.")
|
||||
end
|
||||
|
||||
if action == "prev" then
|
||||
find_and_goto_snapshot(idx + 1, #snapshots, 1)
|
||||
elseif action == "next" then
|
||||
find_and_goto_snapshot(idx - 1, 1, -1)
|
||||
end
|
||||
end,
|
||||
}
|
||||
Reference in New Issue
Block a user