274 lines
7.8 KiB
Lua
274 lines
7.8 KiB
Lua
--- @since 25.2.7
|
|
|
|
local M = {}
|
|
local shell = os.getenv("SHELL") or ""
|
|
local PackageName = "Restore"
|
|
local function success(s, ...)
|
|
ya.notify({ title = PackageName, content = string.format(s, ...), timeout = 5, level = "info" })
|
|
end
|
|
|
|
local function fail(s, ...)
|
|
ya.notify({ title = PackageName, content = string.format(s, ...), timeout = 5, level = "error" })
|
|
end
|
|
|
|
---@enum STATE
|
|
local STATE = {
|
|
POSITION = "position",
|
|
SHOW_CONFIRM = "show_confirm",
|
|
THEME = "theme",
|
|
}
|
|
|
|
local set_state = ya.sync(function(state, key, value)
|
|
if state then
|
|
state[key] = value
|
|
else
|
|
state = {}
|
|
state[key] = value
|
|
end
|
|
end)
|
|
|
|
local get_state = ya.sync(function(state, key)
|
|
if state then
|
|
return state[key]
|
|
else
|
|
return nil
|
|
end
|
|
end)
|
|
|
|
---@enum File_Type
|
|
local File_Type = {
|
|
File = "file",
|
|
Dir = "dir_all",
|
|
None_Exist = "unknown",
|
|
}
|
|
|
|
---@alias TRASHED_ITEM {trash_index: number, trashed_date_time: string, trashed_path: string, type: File_Type} Item in trash list
|
|
|
|
function get_basename(filepath)
|
|
return filepath:match("^.+/(.+)$") or filepath
|
|
end
|
|
|
|
local get_cwd = ya.sync(function()
|
|
return tostring(cx.active.current.cwd)
|
|
end)
|
|
|
|
local function path_quote(path)
|
|
local result = "'" .. string.gsub(path, "'", "'\\''") .. "'"
|
|
return result
|
|
end
|
|
|
|
local function get_file_type(path)
|
|
local cha, _ = fs.cha(Url(path))
|
|
if cha then
|
|
return cha.is_dir and File_Type.Dir or File_Type.File
|
|
else
|
|
return File_Type.None_Exist
|
|
end
|
|
end
|
|
|
|
local function get_trash_volume()
|
|
local cwd = get_cwd()
|
|
local trash_volumes_stream, cmr_err =
|
|
Command("trash-list"):args({ "--volumes" }):stdout(Command.PIPED):stderr(Command.PIPED):output()
|
|
|
|
local matched_vol_path = nil
|
|
if trash_volumes_stream then
|
|
local matched_vol_length = 0
|
|
for vol in trash_volumes_stream.stdout:gmatch("[^\r\n]+") do
|
|
local vol_length = utf8.len(vol) or 0
|
|
if cwd:sub(1, vol_length) == vol and vol_length > matched_vol_length then
|
|
matched_vol_path = vol
|
|
matched_vol_length = vol_length
|
|
end
|
|
end
|
|
if not matched_vol_path then
|
|
fail("Can't get trash directory")
|
|
end
|
|
else
|
|
fail("Failed to start `trash-list` with error: `%s`. Do you have `trash-cli` installed?", cmr_err)
|
|
end
|
|
return matched_vol_path
|
|
end
|
|
|
|
---get list of latest files/folders trashed
|
|
---@param curr_working_volume currently working volume
|
|
---@return TRASHED_ITEM[]|nil
|
|
local function get_latest_trashed_items(curr_working_volume)
|
|
---@type TRASHED_ITEM[]
|
|
local restorable_items = {}
|
|
local fake_enter = Command("printf"):stderr(Command.PIPED):stdout(Command.PIPED):spawn():take_stdout()
|
|
local trash_list_stream, err_cmd = Command(shell)
|
|
:args({ "-c", "trash-restore " .. path_quote(curr_working_volume) })
|
|
:stdin(fake_enter)
|
|
:stdout(Command.PIPED)
|
|
:stderr(Command.PIPED)
|
|
:output()
|
|
|
|
if trash_list_stream then
|
|
---@type TRASHED_ITEM[]
|
|
local trash_list = {}
|
|
for line in trash_list_stream.stdout:gmatch("[^\r\n]+") do
|
|
-- remove leading spaces
|
|
line = line:match("^%s*(.+)$")
|
|
local trash_index, item_date, item_path = line:match("^(%d+) (%S+ %S+) (.+)$")
|
|
if item_date and item_path and trash_index ~= nil then
|
|
table.insert(trash_list, {
|
|
trash_index = tonumber(trash_index),
|
|
trashed_date_time = item_date,
|
|
trashed_path = item_path,
|
|
type = File_Type.None_Exist,
|
|
})
|
|
end
|
|
end
|
|
|
|
if #trash_list == 0 then
|
|
success("Nothing left to restore")
|
|
return
|
|
end
|
|
|
|
local last_item_datetime = trash_list[#trash_list].trashed_date_time
|
|
|
|
for _, trash_item in ipairs(trash_list) do
|
|
if trash_item then
|
|
if trash_item.trashed_date_time == last_item_datetime then
|
|
trash_item.type = get_file_type(trash_item.trashed_path)
|
|
table.insert(restorable_items, trash_item)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
fail("Failed to start `trash-restore` with error: `%s`. Do you have `trash-cli` installed?", err_cmd)
|
|
return
|
|
end
|
|
return restorable_items
|
|
-- return newest_trashed_items
|
|
end
|
|
|
|
---@param trash_list TRASHED_ITEM[]
|
|
local function filter_none_exised_paths(trash_list)
|
|
---@type TRASHED_ITEM[]
|
|
local existed_trash_items = {}
|
|
for _, v in ipairs(trash_list) do
|
|
if v.type ~= File_Type.None_Exist then
|
|
table.insert(existed_trash_items, v)
|
|
end
|
|
end
|
|
return existed_trash_items
|
|
end
|
|
|
|
local function restore_files(curr_working_volume, start_index, end_index)
|
|
if type(start_index) ~= "number" or type(end_index) ~= "number" or start_index < 0 or end_index < 0 then
|
|
fail("Failed to restore file(s): out of range")
|
|
return
|
|
end
|
|
|
|
ya.manager_emit("shell", {
|
|
"echo " .. ya.quote(start_index .. "-" .. end_index) .. " | trash-restore --overwrite " .. path_quote(
|
|
curr_working_volume
|
|
),
|
|
confirm = true,
|
|
})
|
|
local file_to_restore_count = end_index - start_index + 1
|
|
success("Restored " .. tostring(file_to_restore_count) .. " file" .. (file_to_restore_count > 1 and "s" or ""))
|
|
end
|
|
|
|
function M:setup(opts)
|
|
if opts and opts.position and type(opts.position) == "table" then
|
|
set_state(STATE.POSITION, opts.position)
|
|
else
|
|
set_state(STATE.POSITION, { "center", w = 70, h = 40 })
|
|
end
|
|
if opts and opts.show_confirm then
|
|
set_state(STATE.SHOW_CONFIRM, opts.show_confirm)
|
|
else
|
|
set_state(STATE.SHOW_CONFIRM, false)
|
|
end
|
|
if opts and opts.theme and type(opts.theme) == "table" then
|
|
set_state(STATE.THEME, opts.theme)
|
|
else
|
|
set_state(STATE.THEME, {})
|
|
end
|
|
end
|
|
|
|
---@param trash_list TRASHED_ITEM[]
|
|
local function get_components(trash_list)
|
|
local theme = get_state(STATE.THEME) or {}
|
|
local item_odd_style = theme.list_item and theme.list_item.odd and ui.Style():fg(theme.list_item.odd)
|
|
or (th and th.confirm and th.confirm.list or ui.Style():fg("blue"))
|
|
local item_even_style = theme.list_item and theme.list_item.even and ui.Style():fg(theme.list_item.even)
|
|
or (th and th.confirm and th.confirm.list or ui.Style():fg("blue"))
|
|
|
|
local trashed_items_components = {}
|
|
for idx, item in pairs(trash_list) do
|
|
table.insert(
|
|
trashed_items_components,
|
|
ui.Line({
|
|
ui.Span(" "),
|
|
ui.Span(item.trashed_path):style(idx % 2 == 0 and item_even_style or item_odd_style),
|
|
}):align(ui.Line.LEFT)
|
|
)
|
|
end
|
|
return trashed_items_components
|
|
end
|
|
|
|
function M:entry()
|
|
local curr_working_volume = get_trash_volume()
|
|
if not curr_working_volume then
|
|
return
|
|
end
|
|
local trashed_items = get_latest_trashed_items(curr_working_volume)
|
|
if trashed_items == nil then
|
|
return
|
|
end
|
|
local collided_items = filter_none_exised_paths(trashed_items)
|
|
local overwrite_confirmed = true
|
|
local show_confirm = get_state(STATE.SHOW_CONFIRM)
|
|
local pos = get_state(STATE.POSITION)
|
|
pos = pos or { "center", w = 70, h = 40 }
|
|
|
|
local theme = get_state(STATE.THEME) or {}
|
|
theme.title = theme.title and ui.Style():fg(theme.title):bold() or (th and th.confirm and th.confirm.title)
|
|
theme.header = theme.header and ui.Style():fg(theme.header) or (th and th.confirm and th.confirm.content)
|
|
theme.header_warning = ui.Style():fg(theme.header_warning or "yellow")
|
|
if ya.confirm and show_confirm then
|
|
local continue_restore = ya.confirm({
|
|
-- title = ui.Line("Restore files/folders"):fg(theme.title):bold(),
|
|
title = ui.Line("Restore files/folders"):style(theme.title),
|
|
content = ui.Text({
|
|
ui.Line(""),
|
|
ui.Line("The following files and folders are going to be restored:"):style(theme.header),
|
|
ui.Line(""),
|
|
table.unpack(get_components(trashed_items)),
|
|
})
|
|
:align(ui.Text.LEFT)
|
|
:wrap(ui.Text.WRAP),
|
|
pos = pos,
|
|
})
|
|
-- stopping
|
|
if not continue_restore then
|
|
return
|
|
end
|
|
end
|
|
|
|
-- show Confirm dialog with list of collided items
|
|
if #collided_items > 0 then
|
|
overwrite_confirmed = ya.confirm({
|
|
title = ui.Line("Restore files/folders"):style(theme.title),
|
|
content = ui.Text({
|
|
ui.Line(""),
|
|
ui.Line("The following files and folders are existed, overwrite?"):style(theme.header_warning),
|
|
ui.Line(""),
|
|
table.unpack(get_components(collided_items)),
|
|
})
|
|
:align(ui.Text.LEFT)
|
|
:wrap(ui.Text.WRAP),
|
|
pos = pos,
|
|
})
|
|
end
|
|
if overwrite_confirmed then
|
|
restore_files(curr_working_volume, trashed_items[1].trash_index, trashed_items[#trashed_items].trash_index)
|
|
end
|
|
end
|
|
|
|
return M
|