923 lines
27 KiB
Bash
923 lines
27 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
: <<TODO
|
||
The main.lua now passes to commandline httm which fails because not snapshots
|
||
found (autosnapshot not installed yet), but not other tests done.
|
||
|
||
main.lua_old is broken (old syntax) but outlines more functionalty.
|
||
TODO
|
||
|
||
DEST=${1:-/etc/skel/}
|
||
|
||
YAZI_HOME="${DEST}/.config/yazi"
|
||
YAZI_PLUGIN_HOME="${YAZI_HOME}/plugins"
|
||
HTTM_PLUGIN_HOME="${YAZI_PLUGIN_HOME}/httm.yazi"
|
||
|
||
mkdir -p "${HTTM_PLUGIN_HOME}"
|
||
|
||
: <<TODO
|
||
Add support for current selection as input to httm (pipe paths via
|
||
:stdin(Command.PIPED) + write targets to child's stdin)
|
||
Extra modes: --args='last' → httm -l -r (restore latest), --args='diff' →
|
||
wrap bowie if installed
|
||
--no-live, --omit-ditto, --json parsing for richer integration
|
||
Custom preview command via --preview="bat --color=always {}" (needs
|
||
interactive mode tweak)
|
||
TODO
|
||
|
||
#----------------------------------------------------------------
|
||
|
||
conf_print_httm_require() {
|
||
cat <<'EOF'
|
||
require("httm"):setup({
|
||
-- Example: showing the number of snapshots in the status bar
|
||
show_count = true,
|
||
-- Example: custom color for the httm indicator
|
||
color = "#ff0000"
|
||
})
|
||
EOF
|
||
}
|
||
# conf_print_httm_require | tee -a "${YAZI_HOME}/init.lua"
|
||
|
||
#----------------------------------------------------------------
|
||
conf_print_httm_keymap() {
|
||
cat <<'EOF'
|
||
# ~/.config/yazi/keymap.toml
|
||
|
||
# httm plugin – leader: <SHIFT>M
|
||
|
||
# Core / frequently used
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "s" ]
|
||
run = "plugin httm snapshot"
|
||
desc = "httm: Snapshot current or selected items"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "b" ]
|
||
run = "plugin httm browse"
|
||
desc = "httm: Interactive browse + restore (full TUI)"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "y" ]
|
||
run = "plugin httm select"
|
||
desc = "httm: Select snapshot versions → yank paths"
|
||
|
||
# Time-travel navigation (non-interactive cd into snapshots)
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "p" ]
|
||
run = "plugin httm prev"
|
||
desc = "httm: Jump to previous (older) snapshot"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "n" ]
|
||
run = "plugin httm next"
|
||
desc = "httm: Jump to next (newer) snapshot"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "l" ] # l = live
|
||
run = "plugin httm exit"
|
||
desc = "httm: Exit back to live filesystem"
|
||
|
||
# Diffs (bowie)
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "d" ]
|
||
run = "plugin httm bowie"
|
||
desc = "httm/bowie: Show diff with last snapshot"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "D" ]
|
||
run = "plugin httm bowie-all"
|
||
desc = "httm/bowie: Show diffs across all versions"
|
||
|
||
# Other helpers
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "o" ]
|
||
run = "plugin httm ounce"
|
||
desc = "httm/ounce: Pre-emptive dynamic snapshot check"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "g" ]
|
||
run = "plugin httm nicotine"
|
||
desc = "httm/nicotine: Export versions as git archive"
|
||
|
||
# Time Machine (equine) – optional sub-prefix `ht`
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "t", "m" ]
|
||
run = "plugin httm equine-mount-local"
|
||
desc = "httm/equine: Mount local Time Machine snapshots"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "t", "u" ]
|
||
run = "plugin httm equine-umount-local"
|
||
desc = "httm/equine: Unmount local Time Machine"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "t", "r" ]
|
||
run = "plugin httm equine-mount-remote"
|
||
desc = "httm/equine: Mount remote Time Machine"
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "t", "R" ]
|
||
run = "plugin httm equine-umount-remote"
|
||
desc = "httm/equine: Unmount remote Time Machine"
|
||
EOF
|
||
}
|
||
# conf_print_httm_keymap | tee -a "${YAZI_HOME}/keymap.toml"
|
||
|
||
#----------------------------------------------------------------
|
||
|
||
conf_print_httm_help_keymap() {
|
||
cat <<'EOF'
|
||
|
||
[[manager.prepend_keymap]]
|
||
on = [ "M", "?" ]
|
||
run = '''shell --confirm '
|
||
cat << "FOE"
|
||
httm commands (leader: ` h)
|
||
s snapshot current/selected
|
||
b browse + restore (TUI)
|
||
y select versions → yank
|
||
p previous snapshot
|
||
n next snapshot
|
||
l back to live filesystem
|
||
d diff last version (bowie)
|
||
D diff all versions
|
||
o ounce dynamic snapshot
|
||
g nicotine git archive
|
||
t m mount local Time Machine
|
||
t u unmount local
|
||
t r mount remote
|
||
t R unmount remote
|
||
FOE
|
||
' '''
|
||
desc = "httm: Show command help"
|
||
EOF
|
||
}
|
||
# conf_print_httm_help_keymap | tee -a "${YAZI_HOME}/keymap.toml"
|
||
|
||
#----------------------------------------------------------------
|
||
|
||
# Create the plugin main.lua
|
||
conf_print_httm_main() {
|
||
cat <<'EOF'
|
||
-- Helper to get targets (selected or hovered files)
|
||
-- Uses ya.sync because cx (context) access must be synchronous
|
||
local get_targets = ya.sync(function()
|
||
local tab = cx.active
|
||
local selected = tab.selected
|
||
if #selected == 0 then
|
||
local hovered = tab.current.hovered
|
||
if hovered then
|
||
return { tostring(hovered.url) }
|
||
else
|
||
return { tostring(tab.current.cwd) }
|
||
end
|
||
end
|
||
|
||
local targets = {}
|
||
for _, u in ipairs(selected) do
|
||
table.insert(targets, tostring(u))
|
||
end
|
||
return targets
|
||
end)
|
||
|
||
local function notify(msg, level)
|
||
ya.notify({
|
||
title = "httm.yazi",
|
||
content = msg,
|
||
timeout = level == "error" and 6 or level == "warn" and 4 or 3,
|
||
level = level or "info",
|
||
})
|
||
end
|
||
|
||
local function script_exists(name)
|
||
local cmd = os.Command("which"):arg(name):stdout(os.Command.PIPED):output()
|
||
return cmd and cmd.status.success and cmd.stdout:match("%S") ~= nil
|
||
end
|
||
|
||
local function run_interactive(cmd_name, args)
|
||
local permit = ui.hide() -- ← this is the key change
|
||
|
||
local child, err =
|
||
Command(cmd_name):arg(args or {}):stdin(Command.INHERIT):stdout(Command.INHERIT):stderr(Command.INHERIT):spawn()
|
||
|
||
if not child then
|
||
ya.notify({
|
||
title = "Spawn Error",
|
||
content = "Failed to start " .. cmd_name .. ": " .. tostring(err),
|
||
level = "error",
|
||
timeout = 5,
|
||
})
|
||
permit:drop() -- important: always release the permit on early exit
|
||
return
|
||
end
|
||
|
||
local status = child:wait()
|
||
|
||
-- Clean up
|
||
permit:drop()
|
||
|
||
if not status or not status.success then
|
||
ya.notify({
|
||
title = cmd_name,
|
||
content = "Process exited with non-zero status",
|
||
level = "warn",
|
||
timeout = 3,
|
||
})
|
||
end
|
||
end
|
||
|
||
-- Improved sudo handling
|
||
local function sudo_already()
|
||
local status = os.Command("sudo"):args({ "--validate", "--non-interactive" }):status()
|
||
return status and status.success
|
||
end
|
||
|
||
local function run_with_sudo(program, args)
|
||
local cmd = os.Command("sudo"):arg(program):args(args):stdout(os.Command.PIPED):stderr(os.Command.PIPED)
|
||
if sudo_already() then
|
||
return cmd:output()
|
||
end
|
||
|
||
local _permit = ya.hide()
|
||
ya.notify({ title = "httm.yazi", content = "Sudo password required...", level = "info" })
|
||
local output = cmd:output()
|
||
return (output and (output.status.success or sudo_already())) and output or nil
|
||
end
|
||
|
||
-- FS detection utilities
|
||
local get_cwd = ya.sync(function()
|
||
return tostring(cx.active.current.cwd)
|
||
end)
|
||
|
||
local function trim(s)
|
||
return s:match("^%s*(.-)%s*$")
|
||
end
|
||
|
||
local function get_filesystem_type(cwd)
|
||
local out = os.Command("stat"):args({ "-f", "-c", "%T", cwd }):output()
|
||
return out and out.status.success and trim(out.stdout) or nil
|
||
end
|
||
|
||
-- [ZFS/BTRFS logic remains logically the same, updated to os.Command]
|
||
local function zfs_dataset(cwd)
|
||
local out = os.Command("df"):args({ "--output=source", cwd }):output()
|
||
if not out or not out.status.success then
|
||
return nil
|
||
end
|
||
local dataset
|
||
for line in out.stdout:gmatch("[^\r\n]+") do
|
||
dataset = line
|
||
end
|
||
return dataset
|
||
end
|
||
|
||
-- ... [Other ZFS/BTRFS helpers omitted for brevity, ensure they use os.Command] ...
|
||
|
||
local function snapshot()
|
||
local targets = get_targets()
|
||
if #targets == 0 then
|
||
return notify("No target", "error")
|
||
end
|
||
|
||
local cmd = os.Command("httm"):arg("--snap")
|
||
for _, t in ipairs(targets) do
|
||
cmd:arg(t)
|
||
end
|
||
|
||
local output = cmd:output()
|
||
if output and output.status.success then
|
||
notify("Snapshot created")
|
||
ya.mgr_emit("refresh", {})
|
||
else
|
||
notify("httm --snap failed; trying sudo...", "warn")
|
||
run_with_sudo("httm", { "--snap", unpack(targets) })
|
||
ya.mgr_emit("refresh", {})
|
||
end
|
||
end
|
||
|
||
local function select_files()
|
||
local _permit = ya.hide()
|
||
local cmd = os.Command("httm"):args({ "-s", "-R" }):stdout(os.Command.PIPED):stderr(os.Command.PIPED)
|
||
local output = cmd:output()
|
||
|
||
if not output or not output.status.success then
|
||
notify("httm selection failed", "error")
|
||
return
|
||
end
|
||
|
||
local paths = {}
|
||
for line in output.stdout:gmatch("[^\n]+") do
|
||
local trimmed = trim(line)
|
||
if trimmed ~= "" then
|
||
table.insert(paths, trimmed)
|
||
end
|
||
end
|
||
|
||
if #paths > 0 then
|
||
ya.mgr_emit("yank", { paths = paths, silent = true })
|
||
notify("Yanked " .. #paths .. " paths")
|
||
end
|
||
end
|
||
|
||
-- Time travel implementation using updated ya.mgr_emit
|
||
local function time_travel(action)
|
||
-- ... (Logic for finding next/prev snapshot path) ...
|
||
-- Use: ya.mgr_emit("cd", { path })
|
||
end
|
||
|
||
return {
|
||
entry = function(_, job)
|
||
local arg = job.args[1]
|
||
|
||
if arg == "snapshot" then
|
||
snapshot()
|
||
elseif arg == "browse" then
|
||
run_interactive("httm", { "-r", "-R" })
|
||
elseif arg == "select" then
|
||
select_files()
|
||
elseif arg == "bowie" then
|
||
local targets = get_targets()
|
||
run_interactive("bowie", targets)
|
||
-- ... add other cases matching your original logic ...
|
||
elseif arg == "prev" or arg == "next" or arg == "exit" then
|
||
time_travel(arg)
|
||
else
|
||
notify("Unknown command: " .. tostring(arg), "error")
|
||
end
|
||
end,
|
||
}
|
||
EOF
|
||
}
|
||
conf_print_httm_main | tee "${HTTM_PLUGIN_HOME}/main.lua"
|
||
|
||
#----------------------------------------------------------------
|
||
|
||
conf_print_httm_main_old() {
|
||
cat <<'EOF'
|
||
-- ~/.config/yazi/plugins/httm.yazi/main.lua
|
||
|
||
local function get_targets(cx)
|
||
local tab = cx.active
|
||
local selected = tab.selected
|
||
|
||
if #selected == 0 then
|
||
local hovered = tab.current.hovered
|
||
if hovered then
|
||
return { tostring(hovered.url) }
|
||
else
|
||
return { tostring(tab.current.cwd) }
|
||
end
|
||
end
|
||
|
||
local targets = {}
|
||
for _, u in ipairs(selected) do
|
||
table.insert(targets, tostring(u))
|
||
end
|
||
return targets
|
||
end
|
||
|
||
local function notify(msg, level)
|
||
ya.notify {
|
||
title = "httm.yazi",
|
||
content = msg,
|
||
timeout = level == "error" and 6 or level == "warn" and 4 or 3,
|
||
level = level or "info",
|
||
}
|
||
end
|
||
|
||
local function script_exists(name)
|
||
local cmd = Command("which"):arg(name):stdout(Command.PIPED):output()
|
||
return cmd and cmd.status.success and cmd.stdout:match("%S") ~= nil
|
||
end
|
||
|
||
local function run_interactive(cx, cmd_name, args)
|
||
local _permit = ya.hide()
|
||
|
||
local cmd = Command(cmd_name):args(args or {})
|
||
|
||
local child, err = cmd:spawn()
|
||
if not child then
|
||
notify("Failed to spawn " .. cmd_name .. ": " .. tostring(err), "error")
|
||
return
|
||
end
|
||
|
||
local status = child:wait()
|
||
if not status or not status.success then
|
||
notify(cmd_name .. " exited abnormally", "warn")
|
||
end
|
||
|
||
ya.manager_emit("refresh", {})
|
||
end
|
||
|
||
local function run_capture_output(cmd_name, args)
|
||
local cmd = Command(cmd_name)
|
||
:args(args or {})
|
||
:stdout(Command.PIPED)
|
||
:stderr(Command.PIPED)
|
||
|
||
local output = cmd:output()
|
||
if not output or not output.status.success then
|
||
local msg = cmd_name .. " failed"
|
||
if output and output.stderr and #output.stderr > 0 then
|
||
msg = msg .. ": " .. output.stderr:gsub("\n$", "")
|
||
end
|
||
notify(msg, "error")
|
||
return nil
|
||
end
|
||
return output.stdout
|
||
end
|
||
|
||
-- Improved sudo handling from reference
|
||
local function sudo_already()
|
||
local status = Command("sudo"):args({ "--validate", "--non-interactive" }):status()
|
||
return status and status.success
|
||
end
|
||
|
||
local function run_with_sudo(program, args)
|
||
local cmd = Command("sudo"):arg(program):args(args):stdout(Command.PIPED):stderr(Command.PIPED)
|
||
if sudo_already() then
|
||
local output = cmd:output()
|
||
return output and output.status.success and output or nil
|
||
end
|
||
|
||
local _permit = ya.hide()
|
||
ya.notify({ title = "httm.yazi", content = "Sudo password required for " .. program, level = "info", timeout = 3 })
|
||
local output = cmd:output()
|
||
_permit:drop()
|
||
|
||
if output and (output.status.success or sudo_already()) then
|
||
return output
|
||
end
|
||
return nil
|
||
end
|
||
|
||
-- FS detection and snapshot listing from time-travel.yazi reference
|
||
local get_cwd = ya.sync(function()
|
||
return tostring(cx.active.current.cwd)
|
||
end)
|
||
|
||
local function trim(s)
|
||
return s:match("^%s*(.-)%s*$")
|
||
end
|
||
|
||
local function get_filesystem_type(cwd)
|
||
local out = Command("stat"):args({ "-f", "-c", "%T", cwd }):output()
|
||
return out and out.status.success and trim(out.stdout) or nil
|
||
end
|
||
|
||
local function zfs_dataset(cwd)
|
||
local out = Command("df"):args({ "--output=source", cwd }):output()
|
||
if not out or not out.status.success then return nil end
|
||
|
||
local dataset
|
||
for line in out.stdout:gmatch("[^\r\n]+") do
|
||
dataset = line
|
||
end
|
||
return dataset
|
||
end
|
||
|
||
local function zfs_mountpoint(dataset)
|
||
local out = Command("zfs"):args({ "get", "-H", "-o", "value", "mountpoint", dataset }):output()
|
||
if not out or not out.status.success then return nil end
|
||
|
||
local mp = trim(out.stdout)
|
||
if mp == "legacy" then
|
||
local df_out = Command("df"):output()
|
||
if not df_out or not df_out.status.success then return nil end
|
||
|
||
for line in df_out.stdout:gmatch("[^\r\n]+") do
|
||
if line:find(dataset, 1, true) == 1 then
|
||
local mountpoint
|
||
for field in line:gmatch("%S+") do
|
||
mountpoint = field
|
||
end
|
||
return mountpoint
|
||
end
|
||
end
|
||
end
|
||
return mp
|
||
end
|
||
|
||
local function zfs_relative(cwd, mountpoint)
|
||
local relative = cwd:sub(1, #mountpoint) == mountpoint and cwd:sub(#mountpoint + 1) or cwd
|
||
local snap_pos = cwd:find(".zfs/snapshot")
|
||
if snap_pos then
|
||
local after = cwd:sub(snap_pos + #"/snapshot" + 1)
|
||
local first_slash = after:find("/")
|
||
return first_slash and after:sub(first_slash) or "/"
|
||
end
|
||
return relative
|
||
end
|
||
|
||
local function zfs_snapshots(dataset, mountpoint, relative)
|
||
local out = Command("zfs"):args({ "list", "-H", "-t", "snapshot", "-o", "name", "-S", "creation", dataset }):output()
|
||
if not out or not out.status.success then return {} end
|
||
|
||
local snapshots = {}
|
||
for snap in out.stdout:gmatch("[^\r\n]+") do
|
||
local sep = snap:find("@")
|
||
if sep then
|
||
local id = snap:sub(sep + 1)
|
||
table.insert(snapshots, { id = id, path = mountpoint .. "/.zfs/snapshot/" .. id .. relative })
|
||
end
|
||
end
|
||
return snapshots
|
||
end
|
||
|
||
local function btrfs_mountpoint(cwd)
|
||
local out = Command("findmnt"):args({ "-no", "TARGET", "-T", cwd }):output()
|
||
return out and out.status.success and trim(out.stdout) or nil
|
||
end
|
||
|
||
local function btrfs_uuids(cwd)
|
||
local out = run_with_sudo("btrfs", { "subvolume", "show", cwd })
|
||
if not out then return nil, nil end
|
||
|
||
local parent_uuid, uuid
|
||
for line in out.stdout:gmatch("[^\r\n]+") do
|
||
local p = line:match("^%s*Parent UUID:%s*(%S+)")
|
||
if p then parent_uuid = trim(p) end
|
||
local u = line:match("^%s*UUID:%s*(%S+)")
|
||
if u then uuid = trim(u) end
|
||
end
|
||
return parent_uuid, uuid
|
||
end
|
||
|
||
local function btrfs_snapshots(mountpoint, current_uuid, current_parent_uuid)
|
||
local out = run_with_sudo("btrfs", { "subvolume", "list", "-q", "-u", mountpoint })
|
||
if not out then return { snapshots = {}, latest_path = "", current_snapshot_id = "" } end
|
||
|
||
local snapshots = {}
|
||
local latest_path = ""
|
||
local current_snapshot_id = ""
|
||
for line in out.stdout:gmatch("[^\r\n]+") do
|
||
local subvol_id, parent_uuid, uuid, name = line:match("ID (%d+) gen %d+ top level %d+ parent_uuid ([%w-]+)%s+uuid ([%w-]+) path (%S+)")
|
||
if subvol_id then
|
||
parent_uuid = trim(parent_uuid)
|
||
local path = mountpoint .. "/" .. name
|
||
local is_parent = (current_parent_uuid == "-" and parent_uuid == "-" and uuid == current_uuid) or (uuid == current_parent_uuid)
|
||
if is_parent then
|
||
latest_path = path
|
||
elseif uuid == current_uuid then
|
||
current_snapshot_id = name
|
||
end
|
||
if not is_parent then
|
||
table.insert(snapshots, { id = name, subvol_id = tonumber(subvol_id), path = path })
|
||
end
|
||
end
|
||
end
|
||
|
||
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
|
||
|
||
-- Emulated time-travel actions
|
||
local function time_travel(cx, action)
|
||
if not (action == "prev" or action == "next" or action == "exit") then
|
||
return notify("Invalid time-travel action: " .. action, "error")
|
||
end
|
||
|
||
local cwd = get_cwd()
|
||
local fs_type = get_filesystem_type(cwd)
|
||
if not (fs_type == "zfs" or fs_type == "btrfs") then
|
||
return notify("Unsupported FS: " .. (fs_type or "unknown"), "error")
|
||
end
|
||
|
||
local current_snapshot_id = ""
|
||
local latest_path = ""
|
||
local snapshots = {}
|
||
|
||
if fs_type == "zfs" then
|
||
local dataset = zfs_dataset(cwd)
|
||
if not dataset then return notify("No ZFS dataset", "error") end
|
||
|
||
local sep = dataset:find("@")
|
||
if sep then
|
||
current_snapshot_id = dataset:sub(sep + 1)
|
||
dataset = dataset:sub(1, sep - 1)
|
||
end
|
||
|
||
local mountpoint = zfs_mountpoint(dataset)
|
||
if not mountpoint then return notify("No ZFS mountpoint", "error") end
|
||
|
||
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 not mountpoint or not uuid then return notify("No BTRFS subvolume", "error") 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("No snapshots found", "warn")
|
||
end
|
||
|
||
local function find_index(arr, pred)
|
||
for i, v in ipairs(arr) do
|
||
if pred(v) then return i end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
local function goto_snapshot(start_idx, end_idx, step)
|
||
if start_idx == 0 then
|
||
ya.manager_emit("cd", { latest_path })
|
||
return true
|
||
elseif start_idx < 1 or start_idx > #snapshots then
|
||
notify("No more snapshots in that direction", "warn")
|
||
return false
|
||
end
|
||
|
||
for i = start_idx, end_idx, step do
|
||
local path = snapshots[i].path
|
||
local f = io.open(path, "r")
|
||
if f then
|
||
f:close()
|
||
ya.manager_emit("cd", { path })
|
||
return true
|
||
end
|
||
end
|
||
notify("No accessible snapshots in that direction", "warn")
|
||
return false
|
||
end
|
||
|
||
if current_snapshot_id == "" then
|
||
if action == "prev" then
|
||
goto_snapshot(1, #snapshots, 1)
|
||
elseif action == "next" then
|
||
notify("Already at latest; use exit to go live", "info")
|
||
end
|
||
return
|
||
end
|
||
|
||
local idx = find_index(snapshots, function(s) return s.id == current_snapshot_id end)
|
||
if not idx then return notify("Current snapshot not found", "error") end
|
||
|
||
if action == "prev" then
|
||
goto_snapshot(idx + 1, #snapshots, 1)
|
||
elseif action == "next" then
|
||
goto_snapshot(idx - 1, 0, -1)
|
||
end
|
||
end
|
||
|
||
local function snapshot(cx)
|
||
-- [existing snapshot function unchanged]
|
||
local targets = get_targets(cx)
|
||
if #targets == 0 then return notify("No target", "error") end
|
||
|
||
local cmd = Command("httm"):arg("--snap")
|
||
for _, t in ipairs(targets) do cmd:arg(t) end
|
||
|
||
local child, err = cmd:spawn()
|
||
if not child then return notify("Spawn failed: " .. tostring(err), "error") end
|
||
|
||
local status = child:wait()
|
||
if status and status.success then
|
||
notify("Snapshot created")
|
||
ya.manager_emit("refresh", {})
|
||
return
|
||
end
|
||
|
||
notify("httm --snap failed → retrying sudo...", "warn")
|
||
local sudo_cmd = Command("sudo"):arg("httm"):arg("--snap")
|
||
for _, t in ipairs(targets) do sudo_cmd:arg(t) end
|
||
|
||
local schild, serr = sudo_cmd:spawn()
|
||
if not schild then return notify("sudo spawn failed: " .. tostring(serr), "error") end
|
||
|
||
local sstatus = schild:wait()
|
||
if sstatus and sstatus.success then
|
||
notify("Snapshot created (sudo)")
|
||
ya.manager_emit("refresh", {})
|
||
else
|
||
notify("Snapshot failed (even sudo)", "error")
|
||
end
|
||
end
|
||
|
||
local function browse(cx)
|
||
run_interactive(cx, "httm", { "-r", "-R" })
|
||
end
|
||
|
||
local function select_files(cx)
|
||
-- [existing select_files unchanged]
|
||
local _permit = ya.hide()
|
||
|
||
local cmd = Command("httm")
|
||
:args({ "-s", "-R" })
|
||
:stdout(Command.PIPED)
|
||
:stderr(Command.PIPED)
|
||
|
||
local output = cmd:output()
|
||
if not output or not output.status.success then
|
||
# notify("httm -s -R failed: " .. (output and output.stderr or "unknown"), "error")
|
||
# return
|
||
# end
|
||
#
|
||
# local paths = {}
|
||
# for line in output.stdout:gmatch("[^\n]+") do
|
||
# local trimmed = line:match("^%s*(.-)%s*$")
|
||
# if trimmed ~= "" then table.insert(paths, trimmed) end
|
||
# end
|
||
#
|
||
# if #paths == 0 then
|
||
# notify("No files selected", "info")
|
||
# return
|
||
# end
|
||
#
|
||
# ya.manager_emit("yank", { paths = paths, silent = true })
|
||
# notify("Yanked " .. #paths .. " snapshot path(s)")
|
||
# end
|
||
#
|
||
# -- [bowie, ounce, nicotine, equine unchanged]
|
||
#
|
||
# local function bowie(cx, mode)
|
||
# local targets = get_targets(cx)
|
||
# if #targets == 0 then return notify("No target for bowie", "error") end
|
||
#
|
||
if not script_exists("bowie") then
|
||
return notify("bowie script not found in PATH", "error")
|
||
end
|
||
|
||
local args = {}
|
||
if mode == "all" then table.insert(args, "--all") end
|
||
if mode == "select" then table.insert(args, "--select") end
|
||
for _, t in ipairs(targets) do table.insert(args, t) end
|
||
|
||
run_interactive(cx, "bowie", args)
|
||
end
|
||
|
||
local function ounce(cx)
|
||
local targets = get_targets(cx)
|
||
if #targets == 0 then return notify("No target for ounce", "error") end
|
||
|
||
if not script_exists("ounce") then
|
||
return notify("ounce script not found in PATH", "error")
|
||
end
|
||
|
||
local args = { "--direct" }
|
||
for _, t in ipairs(targets) do table.insert(args, t) end
|
||
|
||
run_interactive(cx, "ounce", args)
|
||
end
|
||
|
||
local function nicotine(cx)
|
||
local targets = get_targets(cx)
|
||
if #targets == 0 then return notify("No target for nicotine", "error") end
|
||
|
||
if not script_exists("nicotine") then
|
||
return notify("nicotine script not found in PATH", "error")
|
||
end
|
||
|
||
run_interactive(cx, "nicotine", targets)
|
||
end
|
||
|
||
local function equine(cx, subcmd)
|
||
if not script_exists("equine") then
|
||
return notify("equine script not found in PATH", "error")
|
||
end
|
||
|
||
local arg = "--mount-local"
|
||
if subcmd == "umount-local" then arg = "--unmount-local"
|
||
elseif subcmd == "mount-remote" then arg = "--mount-remote"
|
||
elseif subcmd == "umount-remote" then arg = "--unmount-remote" end
|
||
|
||
run_interactive(cx, "equine", { arg })
|
||
end
|
||
|
||
return {
|
||
entry = function(_, job)
|
||
local arg = job.args[1]
|
||
|
||
if arg == "snapshot" then
|
||
snapshot(_)
|
||
elseif arg == "browse" then
|
||
browse(_)
|
||
elseif arg == "select" then
|
||
select_files(_)
|
||
elseif arg == "bowie" then
|
||
bowie(_, "last")
|
||
elseif arg == "bowie-all" then
|
||
bowie(_, "all")
|
||
elseif arg == "bowie-select" then
|
||
bowie(_, "select")
|
||
elseif arg == "ounce" then
|
||
ounce(_)
|
||
elseif arg == "nicotine" then
|
||
nicotine(_)
|
||
elseif arg == "equine-mount-local" then
|
||
equine(_, "mount-local")
|
||
elseif arg == "equine-umount-local" then
|
||
equine(_, "umount-local")
|
||
elseif arg == "equine-mount-remote" then
|
||
equine(_, "mount-remote")
|
||
elseif arg == "equine-umount-remote" then
|
||
equine(_, "umount-remote")
|
||
elseif arg == "prev" or arg == "next" or arg == "exit" then
|
||
time_travel(_, arg)
|
||
else
|
||
notify("Unknown command: " .. tostring(arg or "<none>"), "error")
|
||
end
|
||
end,
|
||
}
|
||
|
||
EOF
|
||
}
|
||
conf_print_httm_main_old | tee "${HTTM_PLUGIN_HOME}/main.lua_old"
|
||
|
||
#----------------------------------------------------------------
|
||
|
||
conf_print_httm_license() {
|
||
cat <<EOF
|
||
MIT License
|
||
|
||
Copyright (c) 2026 ${AUTHOR_NAME}
|
||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
of this software and associated documentation files (the "Software"), to deal
|
||
in the Software without restriction, including without limitation the rights
|
||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
copies of the Software, and to permit persons to whom the Software is
|
||
furnished to do so, subject to the following conditions:
|
||
|
||
The above copyright notice and this permission notice shall be included in all
|
||
copies or substantial portions of the Software.
|
||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
SOFTWARE.
|
||
EOF
|
||
}
|
||
conf_print_httm_license | tee "${HTTM_PLUGIN_HOME}/LICENSE"
|
||
|
||
#----------------------------------------------------------------
|
||
|
||
conf_print_httm_README() {
|
||
cat <<'EOF'
|
||
# **kimono-koans/httm** plugin for **Yazi**
|
||
This plugin enables **time-travel navigation** using filesystem snapshots (e.g., BTRFS/ZFS),
|
||
allowing you to browse, restore, diff, and manage file versions efficiently.
|
||
|
||
## Requirements
|
||
|
||
- [Yazi][yazi-link] v26.2.2
|
||
- [httm][httm-link] v0.49.9
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
sh
|
||
# Add the plugin
|
||
ya pkg add cyteen/httm
|
||
|
||
# Install plugin
|
||
ya pkg install
|
||
|
||
# Update plugin
|
||
ya pkg upgrade
|
||
```
|
||
|
||
### Key Features & Usage
|
||
- **Snapshot Management**:
|
||
- $(\s): Create a snapshot of current/selected items.
|
||
- $(\b): Browse and restore files via an interactive TUI.
|
||
- $(\y): Select snapshot versions and yank file paths for reuse.
|
||
|
||
- **Time Navigation**:
|
||
- $(\p) / $(\n): Jump to previous/next snapshot (older/newer).
|
||
- $(\l): Exit snapshot view and return to live filesystem.
|
||
|
||
- **Diffs with Bowie**:
|
||
- $(\d): Show differences with the last snapshot.
|
||
- $(\D): Show diffs across all versions.
|
||
|
||
- **Git Integration**:
|
||
- $(\g): Export file versions as a Git archive using **nicotine**.
|
||
|
||
- **Time Machine (Equine)**:
|
||
- $(\tm) / $(\tu): Mount/unmount local Time Machine snapshots.
|
||
- $(\tr) / $(\tR): Mount/unmount remote snapshots.
|
||
|
||
- **Helpers**:
|
||
- $(\o): Run **ounce** for dynamic snapshot checks.
|
||
- $(\?): Display help menu with all httm commands.
|
||
|
||
EOF
|
||
}
|
||
conf_print_httm_README | tee "${HTTM_PLUGIN_HOME}/README.md"
|