Conversation
|
init.lua function Root:drop(data)
-- Default download_cmd = 'curl -sSLOJ "$1"'
ya.emit("plugin", { "dragdrop", data })
-- Or with specific downloader and args
-- data = data .. " --download-cmd='wget -q --content-disposition \"$1\"'"
-- ya.emit("plugin", { "dragdrop", data })
endUse this plugin to handle drops URLs or drop local files/folders from other file manager (ctrl shift v to paste also work): Supported formats: /home/huyhoang/file
/home/huyhoang/file2
# or
file:///home/huyhoang/file
file:///home/huyhoang/file2
# or http url
https://example.com/file.zip
https://example.com/file2.zipdragdrop.yazi/main.lua --- @since 25.5.31
local M = {}
local path_separator = package.config:sub(1, 1)
local PLUGIN_NAME = "Drag-and-Drop"
local function error(s, ...)
ya.notify({ title = PLUGIN_NAME, content = string.format(s, ...), timeout = 3, level = "error" })
end
local function info(s, ...)
ya.notify({ title = PLUGIN_NAME, content = string.format(s, ...), timeout = 3, level = "info" })
end
-- Normalize paths across OS
local function normalize_path(path)
-- Trim surrounding quotes (Windows drag-drop)
path = path:gsub('^"(.*)"$', "%1")
-- macOS Finder drops like "file:///Users/foo/file.txt"
path = path:gsub("^file://", "")
-- On Windows: convert backslashes to forward slashes internally
path = path:gsub("\\", "/")
return path
end
-- Copy file content
local function copy_file(src, dst)
local infile, err = io.open(src, "rb")
if not infile then
return nil, "open src failed: " .. tostring(err)
end
local outfile, oerr = io.open(dst, "wb")
if not outfile then
infile:close()
return nil, "open dst failed: " .. tostring(oerr)
end
local blocksize = 1024 * 1024 -- 1MB buffer
while true do
local chunk = infile:read(blocksize)
if not chunk then
break
end
outfile:write(chunk)
end
infile:close()
outfile:close()
return true
end
-- Safe move into folder
local function move_into(src, dst_folder)
src = normalize_path(src)
dst_folder = normalize_path(dst_folder)
-- Extract filename (last component)
local filename = src:match("([^/]+)$")
if not filename then
return nil, "invalid source path: " .. src
end
-- Use proper OS separator
local dst = dst_folder .. path_separator .. filename
-- Try rename first (fast, but fails across devices/filesystems)
local ok, err = os.rename(src, dst)
if ok then
return true
end
-- Fallback: copy+delete
local ok2, cerr = copy_file(src, dst)
if not ok2 then
return nil, cerr
end
os.remove(src)
return true
end
local function download_file(url, dst, command)
command = command or ("curl -sSLOJ" .. ' "$1"')
local child, err = Command("sh")
:arg({ "-c", command, "sh", tostring(url) })
:cwd(dst)
:stdout(Command.PIPED)
:stderr(Command.PIPED)
:spawn()
if not child then
return nil, err
end
local status, child_err = child:wait()
child:start_kill()
if child_err or (status and not status.success) then
return nil, child_err
end
return true
end
--- Convert file list to UI component list
---@return ui.List[]
local function get_components(file_list)
local item_odd_style = th.confirm.list
local item_even_style = th.confirm.list
local components = {}
local display_index = 1
for _, item in ipairs(file_list) do
table.insert(
components,
ui.Line({
ui.Span(" "),
ui.Span(item):style((display_index % 2 == 1) and item_odd_style or item_even_style),
}):align(ui.Align.LEFT)
)
display_index = display_index + 1
end
return components
end
local get_cwd = ya.sync(function()
return tostring(cx.active.current.cwd)
end)
function M:entry(job)
local raw_drop_data = job.args
local download_cmd = job.args.download_cmd
local cwd = get_cwd()
local urls, files = {}, {}
---@type "file"|"url"|nil
local drop_type
for _, line in ipairs(raw_drop_data) do
line = line:match("^%s*(.-)%s*$")
if line:match("^https?://") then
-- Handle URL
if not drop_type then
drop_type = "url"
elseif drop_type ~= "url" then
-- Fallback: plain text or unsupported
return
end
urls[#urls + 1] = line
else
line = normalize_path(line)
local file_url = Url(line)
if file_url.is_absolute and file_url.is_regular and fs.cha(file_url) then
-- Handle file path
if not drop_type then
drop_type = "file"
elseif drop_type ~= "file" then
-- Fallback: plain text or unsupported
return
end
if tostring(file_url) == cwd then
goto continue
end
files[#files + 1] = tostring(file_url)
else
-- Fallback: plain text or unsupported
return
end
end
::continue::
end
if #files > 0 or #urls > 0 then
-- Show confirm dialog if any files or urls is dropped
local confirmed = ya.confirm({
title = ui.Line("Drag-and-Drop"):style(th.confirm.title),
body = ui.Text({
ui.Line(""),
ui.Line(
"Confirm to "
.. (
drop_type == "file" and "move these files and folders to current folder?"
or "download these urls to current folder?"
)
):style(ui.Style():fg("yellow")),
ui.Line(""),
table.unpack(get_components(drop_type == "file" and files or urls)),
})
:align(ui.Align.LEFT)
:wrap(ui.Wrap.YES),
pos = { "center", w = 70, h = 40 },
})
if not confirmed then
return
end
else
-- Exit if no files or urls is dropped
return
end
if drop_type == "file" then
for _, file_path in ipairs(files) do
local ok, err = move_into(file_path, cwd)
if not ok then
error("Failed to move: " .. file_path .. " -> " .. tostring(err))
break
end
end
elseif drop_type == "url" then
for _, url in ipairs(urls) do
info("Downloading: " .. url)
local ok, err = download_file(url, cwd, download_cmd)
if not ok then
error("Failed to download: " .. url .. " -> " .. tostring(err))
break
end
end
end
end
return M |
|
Thank you for the patch, that's actually a clever way to capture the drop event! Some initial thoughts:
A better name might be needed here, since |
|
Yup, I also thought DDS was much better. Unfortunately, my knowledge of Rust and the Yazi codebase is very limited, so I hope you or someone else can fix it. 🥲 |
41034d0 to
449450d
Compare
|
FYI, I've requested a new terminal drag-and-drop protocol in kovidgoyal/kitty#9459. We might end up going with that instead, since it supports full drag-and-drop interactions, while this PR only handles the drop part, and that should be more sophisticated than relying on bracketed paste here. |
Rationale of this PR
Current Yazi doesn’t handle mouse drop events.
Many terminals map drag-and-drop into a
Pasteevent in Crossterm. When no input element is visible, if the user drops something into the Yazi area, theRoot:dropmethod will be called with the dropped string. From there, the user can decide how to handle it.