mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-09 13:45:26 +02:00
358 lines
10 KiB
Go
358 lines
10 KiB
Go
package dnd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/emmansun/base64"
|
|
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
|
"github.com/kovidgoyal/kitty/tools/utils"
|
|
"github.com/kovidgoyal/kitty/tools/utils/streaming_base64"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
type data_request struct {
|
|
drag_source *drag_source
|
|
send_remote_data bool
|
|
index int
|
|
write_id loop.IdType
|
|
base64 streaming_base64.StreamingBase64Encoder
|
|
}
|
|
|
|
type remote_data_item struct {
|
|
path string
|
|
metadata os.FileInfo
|
|
file *os.File
|
|
write_id loop.IdType
|
|
base64 streaming_base64.StreamingBase64Encoder
|
|
parent_dir_handle, idx_in_parent int
|
|
}
|
|
|
|
type drag_status struct {
|
|
active bool
|
|
terminal_accepted_drag bool
|
|
offered_mimes []string
|
|
accepted_mime int
|
|
accepted_operation int
|
|
dropped bool
|
|
data_requests []*data_request
|
|
remote_items []*remote_data_item
|
|
current_remote_file *remote_data_item
|
|
dir_handle_counter int
|
|
remote_item_write_id loop.IdType
|
|
}
|
|
|
|
func (dnd *dnd) on_potential_drag_start(cell_x, cell_y int) (err error) {
|
|
if !dnd.allow_drags || dnd.drag_status.active {
|
|
return
|
|
}
|
|
mimes := slices.Collect(maps.Keys(dnd.drag_sources))
|
|
actions := 3
|
|
if dnd.copy_button_region.has(cell_x, cell_y) {
|
|
actions = 1
|
|
} else if dnd.move_button_region.has(cell_x, cell_y) {
|
|
actions = 2
|
|
}
|
|
dnd.lp.QueueDnDData(DC{Type: 'o', Operation: actions, Payload: utils.UnsafeStringToBytes(strings.Join(mimes, " "))})
|
|
total_preloaded_data_sz := 0
|
|
for i, mt := range mimes {
|
|
s := dnd.drag_sources[mt]
|
|
if len(s.data) > 0 && len(s.data)+total_preloaded_data_sz < 64*1024*1024 {
|
|
total_preloaded_data_sz += len(s.data)
|
|
dnd.lp.QueueDnDData(DC{Type: 'p', X: i, Operation: actions, Payload: s.data})
|
|
dnd.lp.QueueDnDData(DC{Type: 'p', X: i, Operation: actions})
|
|
}
|
|
}
|
|
dnd.drag_status.offered_mimes = mimes
|
|
// TODO: set the drag image
|
|
dnd.lp.QueueDnDData(DC{Type: 'P', X: -1}) // start drag
|
|
dnd.drag_status.active = true
|
|
|
|
return dnd.render_screen()
|
|
}
|
|
|
|
func (dnd *dnd) on_drag_error(cmd DC) (err error) {
|
|
payload := string(cmd.Payload)
|
|
switch payload {
|
|
case "OK":
|
|
if dnd.drag_status.active && !dnd.drag_status.terminal_accepted_drag {
|
|
dnd.drag_status.terminal_accepted_drag = true
|
|
err = dnd.render_screen()
|
|
}
|
|
default:
|
|
err = fmt.Errorf("terminal responded with drag source error: %s", payload)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (dnd *dnd) reset_drag() {
|
|
for _, dr := range dnd.drag_status.data_requests {
|
|
if dr.drag_source.file != nil {
|
|
dr.drag_source.file.Close()
|
|
dr.drag_source.file = nil
|
|
}
|
|
}
|
|
if dnd.drag_status.current_remote_file != nil && dnd.drag_status.current_remote_file.file != nil {
|
|
dnd.drag_status.current_remote_file.file.Close()
|
|
}
|
|
dnd.drag_status = drag_status{}
|
|
}
|
|
|
|
func (dnd *dnd) on_drag_event(x, y, operation, Y int) (err error) {
|
|
switch x {
|
|
case 1:
|
|
dnd.drag_status.accepted_mime = y
|
|
case 2:
|
|
dnd.drag_status.accepted_operation = operation
|
|
case 3:
|
|
dnd.drag_status.dropped = true
|
|
case 4:
|
|
dnd.reset_drag()
|
|
case 5:
|
|
if err = dnd.handle_data_request(y, Y == 1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return dnd.render_screen()
|
|
}
|
|
|
|
func (dnd *dnd) finish_drag(errname string) {
|
|
if errname == "" { // cancel drag
|
|
dnd.lp.QueueDnDData(DC{Type: 'E', Y: -1})
|
|
} else {
|
|
dnd.lp.QueueDnDData(DC{Type: 'E', Payload: []byte(errname)})
|
|
}
|
|
dnd.reset_drag()
|
|
}
|
|
|
|
func (dnd *dnd) handle_data_request(idx int, send_remote_data bool) (err error) {
|
|
if idx < 0 || idx >= len(dnd.drag_status.offered_mimes) {
|
|
dnd.finish_drag("EINVAL")
|
|
return fmt.Errorf("terminal asked for drag data from MIME list with out of bounds index: %d", idx)
|
|
}
|
|
mime := dnd.drag_status.offered_mimes[idx]
|
|
ds := dnd.drag_sources[mime]
|
|
send_remote_data = send_remote_data && mime == "text/uri-list" && len(ds.uri_list) > 0
|
|
for _, dr := range dnd.drag_status.data_requests {
|
|
if dr.index == idx {
|
|
dnd.finish_drag("EINVAL")
|
|
return fmt.Errorf("terminal sent a duplicate drag data request")
|
|
}
|
|
}
|
|
dr := &data_request{drag_source: ds, send_remote_data: send_remote_data, index: idx}
|
|
if ds.path == "" {
|
|
dnd.lp.QueueDnDData(DC{Type: 'e', Y: idx, Payload: utils.UnsafeStringToBytes(base64.RawStdEncoding.EncodeToString(ds.data))})
|
|
dnd.lp.QueueDnDData(DC{Type: 'e', Y: idx}) // EOF
|
|
if !dr.send_remote_data {
|
|
return
|
|
}
|
|
return dnd.start_remote_data_send(ds)
|
|
} else {
|
|
if ds.file != nil {
|
|
ds.file.Close()
|
|
}
|
|
if ds.file, err = os.Open(ds.path); err != nil {
|
|
dnd.finish_drag("EIO")
|
|
return err
|
|
}
|
|
}
|
|
dnd.drag_status.data_requests = append(dnd.drag_status.data_requests, dr)
|
|
return dnd.send_data_for_data_request(len(dnd.drag_status.data_requests) - 1)
|
|
}
|
|
|
|
var read_buf [64 * 1024]byte
|
|
var encode_buf [128 * 1024]byte
|
|
|
|
func (dnd *dnd) send_data_for_data_request(i int) (err error) {
|
|
dr := dnd.drag_status.data_requests[i]
|
|
n, err := dr.drag_source.file.Read(read_buf[:])
|
|
if n > 0 {
|
|
for chunk := range dr.base64.Encode(read_buf[:n], encode_buf[:]) {
|
|
dr.write_id = dnd.lp.QueueDnDData(DC{Type: 'e', Y: dr.index, Payload: chunk})
|
|
}
|
|
}
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if errors.Is(err, io.EOF) {
|
|
chunk := dr.base64.Finish()
|
|
if len(chunk) > 0 {
|
|
dr.write_id = dnd.lp.QueueDnDData(DC{Type: 'e', Y: dr.index, Payload: chunk})
|
|
}
|
|
dr.write_id = dnd.lp.QueueDnDData(DC{Type: 'e', Y: dr.index}) // EOF
|
|
return dnd.on_data_request_finished(i)
|
|
}
|
|
dnd.finish_drag("EIO")
|
|
return err
|
|
}
|
|
|
|
func (dnd *dnd) on_send_done(id loop.IdType) (err error) {
|
|
for i, dr := range dnd.drag_status.data_requests {
|
|
if dr.write_id == id {
|
|
return dnd.send_data_for_data_request(i)
|
|
}
|
|
}
|
|
if id == dnd.drag_status.remote_item_write_id {
|
|
err = dnd.next_remote_item()
|
|
}
|
|
return
|
|
}
|
|
|
|
func (dnd *dnd) on_data_request_finished(i int) (err error) {
|
|
dr := dnd.drag_status.data_requests[i]
|
|
if dr.drag_source.file != nil {
|
|
dr.drag_source.file.Close()
|
|
dr.drag_source.file = nil
|
|
}
|
|
dnd.drag_status.data_requests = slices.Delete(dnd.drag_status.data_requests, i, i+1)
|
|
if dr.send_remote_data {
|
|
err = dnd.start_remote_data_send(dr.drag_source)
|
|
} else if len(dnd.drag_status.data_requests) > 0 {
|
|
err = dnd.send_data_for_data_request(0)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (dnd *dnd) send_remote_item_payload(parent_dir_handle, idx, item_type int, payload []byte) loop.IdType {
|
|
cmd := DC{Type: 'k', X: item_type}
|
|
if len(payload) > 0 {
|
|
cmd.Payload = utils.UnsafeStringToBytes(base64.RawStdEncoding.EncodeToString(payload))
|
|
}
|
|
if parent_dir_handle != 0 {
|
|
cmd.Yp = parent_dir_handle
|
|
cmd.Y = idx + 1
|
|
} else {
|
|
cmd.X = idx + 1
|
|
}
|
|
return dnd.lp.QueueDnDData(cmd)
|
|
}
|
|
|
|
func (dnd *dnd) send_remote_dir(path string, parent_dir_handle, idx int) (children []*remote_data_item, err error) {
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
dnd.finish_drag("EIO")
|
|
return nil, err
|
|
}
|
|
handle := dnd.drag_status.dir_handle_counter
|
|
dnd.drag_status.dir_handle_counter++
|
|
names := make([]string, 0, len(entries))
|
|
for i, entry := range entries {
|
|
st, err := entry.Info()
|
|
if err != nil {
|
|
dnd.finish_drag("EIO")
|
|
return nil, err
|
|
}
|
|
x := remote_data_item{parent_dir_handle: handle, idx_in_parent: i, metadata: st, path: filepath.Join(path, entry.Name())}
|
|
children = append(children, &x)
|
|
names = append(names, entry.Name())
|
|
}
|
|
payload := utils.UnsafeStringToBytes(strings.Join(names, "\x00"))
|
|
dnd.send_remote_item_payload(parent_dir_handle, idx, handle, payload)
|
|
dnd.drag_status.remote_item_write_id = dnd.send_remote_item_payload(parent_dir_handle, idx, handle, nil)
|
|
return
|
|
}
|
|
|
|
func (dnd *dnd) send_remote_symlink(path string, parent_dir_handle, idx int) (err error) {
|
|
target, err := os.Readlink(path)
|
|
if err != nil {
|
|
dnd.finish_drag("EIO")
|
|
return err
|
|
}
|
|
dnd.send_remote_item_payload(parent_dir_handle, idx, 1, utils.UnsafeStringToBytes(target))
|
|
dnd.drag_status.remote_item_write_id = dnd.send_remote_item_payload(parent_dir_handle, idx, 1, nil)
|
|
return
|
|
}
|
|
|
|
func (dnd *dnd) send_next_file_chunk() (err error) {
|
|
cr := dnd.drag_status.current_remote_file
|
|
if cr == nil {
|
|
return dnd.next_remote_item()
|
|
}
|
|
n, err := cr.file.Read(read_buf[:])
|
|
if n > 0 {
|
|
for chunk := range cr.base64.Encode(read_buf[:n], encode_buf[:]) {
|
|
dnd.drag_status.remote_item_write_id = dnd.send_remote_item_payload(cr.parent_dir_handle, cr.idx_in_parent, 0, chunk)
|
|
}
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
cr.file.Close()
|
|
dnd.drag_status.current_remote_file = nil
|
|
dnd.drag_status.remote_items = dnd.drag_status.remote_items[1:]
|
|
if chunk := cr.base64.Finish(); len(chunk) > 0 {
|
|
dnd.send_remote_item_payload(cr.parent_dir_handle, cr.idx_in_parent, 0, chunk)
|
|
}
|
|
dnd.drag_status.remote_item_write_id = dnd.send_remote_item_payload(cr.parent_dir_handle, cr.idx_in_parent, 0, nil)
|
|
return
|
|
}
|
|
dnd.finish_drag("EIO")
|
|
return err
|
|
}
|
|
return
|
|
}
|
|
|
|
func (dnd *dnd) next_remote_item() (err error) {
|
|
if len(dnd.drag_status.remote_items) < 1 {
|
|
if len(dnd.drag_status.data_requests) > 0 {
|
|
err = dnd.send_data_for_data_request(0)
|
|
}
|
|
return
|
|
}
|
|
x := dnd.drag_status.remote_items[0]
|
|
if x.metadata.IsDir() {
|
|
children, err := dnd.send_remote_dir(x.path, x.parent_dir_handle, x.idx_in_parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, children...)
|
|
} else if x.metadata.Mode().Type()&os.ModeSymlink != 0 {
|
|
if err = dnd.send_remote_symlink(x.path, x.parent_dir_handle, x.idx_in_parent); err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
if x.file, err = os.Open(x.path); err != nil {
|
|
dnd.finish_drag("EIO")
|
|
return err
|
|
}
|
|
dnd.drag_status.current_remote_file = x
|
|
if err = dnd.send_next_file_chunk(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (dnd *dnd) start_remote_data_send(ds *drag_source) (err error) {
|
|
dnd.drag_status.dir_handle_counter = 2
|
|
dnd.drag_status.remote_item_write_id = 0
|
|
items := []*remote_data_item{}
|
|
for i, x := range ds.uri_list {
|
|
if x.metadata.IsDir() {
|
|
if children, err := dnd.send_remote_dir(x.path, 0, i); err != nil {
|
|
return err
|
|
} else {
|
|
items = append(items, children...)
|
|
}
|
|
} else if x.metadata.Mode().Type()&os.ModeSymlink != 0 {
|
|
if err = dnd.send_remote_symlink(x.path, 0, i); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
f := remote_data_item{idx_in_parent: i, metadata: x.metadata, path: x.path}
|
|
dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, &f)
|
|
}
|
|
}
|
|
dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, items...)
|
|
if dnd.drag_status.remote_item_write_id == 0 {
|
|
return dnd.next_remote_item()
|
|
}
|
|
return
|
|
}
|