package dnd import ( "bytes" "container/list" "context" "errors" "fmt" "io" "io/fs" "net/url" "os" "path/filepath" "slices" "strconv" "strings" "github.com/emmansun/base64" "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/streaming_base64" ) var _ = fmt.Print const copy_on_drop = 1 const move_on_drop = 2 type drop_dest struct { human_name, path string dest io.WriteCloser mime_type string completed bool close_on_finish bool b64_decoder streaming_base64.StreamingBase64Decoder } func open_file_for_writing(path string) (*os.File, error) { f, err := os.Create(path) if errors.Is(err, os.ErrNotExist) { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return nil, err } return os.Create(path) } return f, err } func (d *drop_dest) write(chunk []byte) (err error) { if d.dest == nil { if d.path == "" { d.dest = &bufferWriteCloser{&bytes.Buffer{}} } else { d.dest, err = open_file_for_writing(d.path) d.close_on_finish = true if err != nil { return } } } _, err = d.dest.Write(chunk) return } func (d *drop_dest) finish() error { defer func() { d.completed = true if d.dest != nil && d.close_on_finish { d.dest.Close() d.dest = nil } }() if chunk, err := d.b64_decoder.Finish(); err != nil { return err } else if len(chunk) > 0 { return d.write(chunk) } return nil } func (d *drop_dest) add_data(x []byte, output_buf []byte, has_more bool) error { d.completed = false for chunk, err := range d.b64_decoder.Decode(x, output_buf) { if err == nil { err = d.write(chunk) } if err != nil { return err } } if !has_more { if chunk, err := d.b64_decoder.Finish(); err != nil { return err } else if len(chunk) > 0 { return d.write(chunk) } } return nil } type remote_dir_entry struct { base_dir *dir_handle name string item_type int children []*remote_dir_entry num_children_finished int dest io.WriteCloser b64_decoder streaming_base64.StreamingBase64Decoder } const case_conflict_template = "case-conflict-%d-%s" func uniqify_child_names(names []string, is_case_sensitive_filesystem bool) []string { if !is_case_sensitive_filesystem { seen := utils.NewSet[string](len(names)) for i, x := range names { name := x key := strings.ToLower(name) for q := 0; seen.Has(key); q++ { name = fmt.Sprintf(case_conflict_template, q+1, x) key = strings.ToLower(name) } seen.Add(key) names[i] = name } } return names } func do_local_copy(ctx context.Context, dest_dir *os.File, uri_list []string) (err error) { var src_file *os.File defer func() { if src_file != nil { src_file.Close() } }() for _, path := range uri_list { if path == "" { continue } select { case <-ctx.Done(): return ctx.Err() default: } if src_file != nil { src_file.Close() } st, err := os.Lstat(path) if err != nil { return err } if st.IsDir() { if src_file, err = os.Open(path); err != nil { return err } d, err := utils.CreateDirAt(dest_dir, filepath.Base(path), st.Mode().Perm()) if err != nil { return err } err = utils.CopyFolderContents(ctx, src_file, d, utils.CopyFolderOptions{ Filter_files: func(parent *os.File, child os.FileInfo) bool { return child.IsDir() || child.Mode().IsRegular() || child.Mode()&fs.ModeSymlink != 0 }, }) d.Close() if err != nil { return err } } else if st.Mode().IsRegular() { // First try a hard link dest := filepath.Join(dest_dir.Name(), filepath.Base(path)) if err = os.Link(path, dest); err == nil { continue } if src_file, err = os.Open(path); err != nil { return err } d, err := utils.CreateAt(dest_dir, filepath.Base(dest), st.Mode().Perm()) if err != nil { return err } err = utils.CopyFileAndClose(ctx, src_file, d) src_file = nil // already closed if err != nil { return err } } else if st.Mode()&fs.ModeSymlink != 0 { target, err := os.Readlink(path) if err != nil { return err } dest := filepath.Join(dest_dir.Name(), filepath.Base(path)) if err := os.Symlink(target, dest); err != nil { return err } } } return } func do_local_copy_in_goroutine(ctx context.Context, dest_dir *os.File, completion chan error, uri_list []string, wakeup func()) { var err error defer func() { if r := recover(); r != nil { err = parallel.Format_stacktrace_on_panic(r, 1) } completion <- err wakeup() }() err = do_local_copy(ctx, dest_dir, uri_list) } type path_stack struct { s list.List } func (p *path_stack) push(x string) { p.s.PushBack(x) } func (p *path_stack) pop() string { return p.s.Remove(p.s.Front()).(string) } func (p *path_stack) empty() bool { return p.s.Front() == nil } // Return a list of relative paths for all entries in the tree rooted at // src_dir that also exist in the tree rooted at dest_dir, except for // directories that exist in both places. func find_overwrites(src_dir *os.File, dest_dir *os.File) (ans []string, err error) { stack := path_stack{} stack.push(".") for !stack.empty() { relpath := stack.pop() if err = func() (err error) { sd, err := utils.OpenDirAt(src_dir, relpath) if err != nil { return err } defer sd.Close() dd, err := utils.OpenDirAt(dest_dir, relpath) if err != nil { return err } defer dd.Close() dest_children, err := dd.ReadDir(0) src_children, err := sd.ReadDir(0) if err != nil { return err } dest_map := make(map[string]os.DirEntry) for _, x := range dest_children { dest_map[x.Name()] = x } for _, x := range src_children { if d, found := dest_map[x.Name()]; found { crelpath := utils.IfElse(relpath == ".", x.Name(), filepath.Join(relpath, x.Name())) if !d.IsDir() || !x.IsDir() { ans = append(ans, crelpath) } else { stack.push(crelpath) } } else { continue } } return }(); err != nil { return nil, err } } return } // Rename the contents of src_dir into dest_dir, handling the case of // directories already existing in dest_dir transparently. func rename_contents(src_dir *os.File, dest_dir *os.File) (err error) { stack := path_stack{} stack.push(".") for !stack.empty() { relpath := stack.pop() if err = func() error { sd, err := utils.OpenDirAt(src_dir, relpath) if err != nil { return err } defer sd.Close() dd, err := utils.OpenDirAt(dest_dir, relpath) if err != nil { return err } defer dd.Close() for { src_children, err := sd.ReadDir(64) if err != nil { if errors.Is(err, io.EOF) { break } return err } for _, child := range src_children { crelpath := utils.IfElse(relpath == ".", child.Name(), filepath.Join(relpath, child.Name())) rerr := utils.RenameAt(sd, child.Name(), dd, child.Name()) if rerr != nil { if child.IsDir() { stack.push(crelpath) } else { return rerr } } } } return nil }(); err != nil { return err } } return } func (d *remote_dir_entry) add_remote_data(data []byte, output_buf []byte, has_more bool, is_case_sensitive_filesystem bool) error { if len(data) > 0 { for chunk, derr := range d.b64_decoder.Decode(data, output_buf) { if derr != nil { return derr } if _, derr = d.dest.Write(chunk); derr != nil { return derr } } } else if !has_more { if chunk, derr := d.b64_decoder.Finish(); derr != nil { return derr } else { if _, derr = d.dest.Write(chunk); derr != nil { return derr } } defer func() { d.dest.Close() d.dest = nil d.base_dir = d.base_dir.unref() }() if dest, ok := d.dest.(*bufferWriteCloser); ok { if d.item_type == 1 { if derr := utils.SymlinkAt(d.base_dir.handle, d.name, dest.String()); derr != nil { return derr } } else { // directory if f, derr := utils.CreateDirAt(d.base_dir.handle, d.name, 0o755); derr != nil { return derr } else { handle := new_dir_handle(f) defer handle.unref() s := utils.NewSeparatorScanner("", "\x00") for _, name := range uniqify_child_names(s.Split(dest.String()), is_case_sensitive_filesystem) { d.children = append(d.children, &remote_dir_entry{name: name, base_dir: handle.newref()}) } } } } } return nil } type parsed_uri_kind int const ( parsed_uri_file parsed_uri_kind = iota parsed_uri_data ) type parsed_uri struct { kind parsed_uri_kind path string // for file URIs: the local filesystem path (empty if not a valid file URI) mime string // for data URIs: the MIME type data []byte // for data URIs: the decoded payload } // ext_for_mime returns a file extension (with leading dot) for a MIME type. func ext_for_mime(mime string) string { for _, x := range utils.GuessFileExtensions(mime) { return x } return "" } // parse_data_uri decodes a data: URI and returns the MIME type and raw data. func parse_data_uri(uri string) (mime string, data []byte, err error) { rest := strings.TrimPrefix(uri, "data:") comma_idx := strings.Index(rest, ",") if comma_idx < 0 { err = fmt.Errorf("invalid data URI: no comma found") return } header := rest[:comma_idx] payload := rest[comma_idx+1:] is_base64 := strings.HasSuffix(header, ";base64") if is_base64 { header = header[:len(header)-7] } mime = strings.TrimSpace(header) if mime == "" { mime = "text/plain" } // Strip parameters (e.g. ;charset=UTF-8) so the MIME type is clean. mime, _, _ = strings.Cut(mime, ";") if is_base64 { payload = strings.NewReplacer("\r", "", "\n", "", " ", "").Replace(payload) data, err = base64.StdEncoding.DecodeString(payload) if err != nil { data, err = base64.RawURLEncoding.DecodeString(payload) } } else { var unescaped string unescaped, err = url.PathUnescape(payload) if err != nil { return } data = []byte(unescaped) } return } func parse_uri_list(src string) (ans []parsed_uri, err error) { for _, line := range utils.NewSeparatorScanner("", "\r\n").Split(src) { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "file://") { p, err := url.Parse(line) if err != nil { return nil, err } ans = append(ans, parsed_uri{kind: parsed_uri_file, path: filepath.Clean(p.Path)}) } else if strings.HasPrefix(line, "data:") { m, d, derr := parse_data_uri(line) if derr != nil { // Invalid data URI: treat as an unrecognised entry (empty path) so the caller // skips it without failing, matching the behaviour for unknown URI schemes. ans = append(ans, parsed_uri{kind: parsed_uri_file}) } else { ans = append(ans, parsed_uri{kind: parsed_uri_data, mime: m, data: d}) } } else { ans = append(ans, parsed_uri{kind: parsed_uri_file}) // unknown URI scheme: empty entry } } return } type drop_status struct { offered_mimes []string accepted_mimes []string uri_list []parsed_uri cell_x, cell_y int action int in_window bool reading_data bool is_remote_client bool source_allowed_ops int dropping_to *dir_handle root_remote_dir *remote_dir_entry open_remote_dir *remote_dir_entry current_remote_entry *remote_dir_entry // used for m=1 only pending_remote_dirs []*remote_dir_entry data_requests struct { pending []DC in_flight_count int } local_copy struct { ctx context.Context cancel_ctx context.CancelFunc completion chan error } } func (d *drop_status) reset() { if d.local_copy.ctx != nil { d.local_copy.cancel_ctx() <-d.local_copy.completion } if d.dropping_to != nil { d.dropping_to = d.dropping_to.unref() } *d = drop_status{cell_x: -1, cell_y: -1} } func (d *drop_dest) reset() { if d.dest != nil && d.dest != os.Stdout { if d.close_on_finish { d.dest.Close() d.dest = nil } else { d.dest.(*bufferWriteCloser).Reset() } } d.completed = false d.close_on_finish = false d.b64_decoder = streaming_base64.StreamingBase64Decoder{} } func (dnd *dnd) reset_drop() { if dnd.drop_status.root_remote_dir != nil { dnd.drop_status.root_remote_dir.close_tree() dnd.drop_status.root_remote_dir = nil } dnd.drop_status.reset() for _, x := range dnd.drop_dests { x.reset() } } func (root *remote_dir_entry) close_tree() { if root.base_dir != nil { root.base_dir = root.base_dir.unref() } for _, child := range root.children { child.close_tree() } } func (dnd *dnd) end_drop(success bool) { if dnd.drop_status.reading_data { dnd.lp.QueueDnDData(DC{ Type: 'r', Operation: utils.IfElse(success, dnd.drop_status.action, 0)}) // end drop if success && dnd.drop_status.action == move_on_drop && !dnd.drop_status.is_remote_client { for _, u := range dnd.drop_status.uri_list { if u.kind != parsed_uri_file || u.path == "" { continue } st, err := os.Lstat(u.path) if err != nil { continue } if st.IsDir() { os.RemoveAll(u.path) } else { os.Remove(u.path) } } } } dnd.reset_drop() if success && dnd.has_exit_on("drop-finish") { dnd.lp.Quit(0) } } func (dnd *dnd) count_files_in_drop() int { n := 0 for _, u := range dnd.drop_status.uri_list { if (u.kind == parsed_uri_file && u.path != "") || u.kind == parsed_uri_data { n++ } } for mime, d := range dnd.drop_dests { if mime != "text/uri-list" && d.completed { n++ } } return n } func (dnd *dnd) all_drop_data_received() (err error) { dnd.num_dropped_files = dnd.count_files_in_drop() var staging_dir *os.File if dnd.drop_status.dropping_to != nil { staging_dir = dnd.drop_status.dropping_to.handle dnd.drop_status.dropping_to = nil } defer func() { if err == nil { if len(dnd.confirm_drop.overwrites) == 0 { dnd.end_drop(true) } err = dnd.render_screen() } else { dnd.end_drop(false) } }() if staging_dir != nil { if dnd.opts.ConfirmDropOverwrite { overwrites, err := find_overwrites(staging_dir, dnd.drop_output_dir) if err != nil { return err } if len(overwrites) > 0 { dnd.confirm_drop.overwrites = overwrites dnd.confirm_drop.staging_dir = staging_dir return dnd.render_screen() } } err := rename_contents(staging_dir, dnd.drop_output_dir) staging_dir.Close() if err != nil { return err } return nil } return nil } func (dnd *dnd) drop_on_wakeup() error { if dnd.drop_status.local_copy.completion == nil { return nil } select { case err := <-dnd.drop_status.local_copy.completion: dnd.drop_status.local_copy.ctx = nil dnd.drop_status.local_copy.completion = nil if err != nil { return err } return dnd.all_drop_data_received() default: return nil } } func (dnd *dnd) new_tdir() (dir_file *os.File, err error) { dnd.tdir_counter++ name := strconv.Itoa(dnd.tdir_counter) return utils.CreateDirAt(dnd.base_tempdir, name, 0o700) } func (dnd *dnd) all_mime_data_dropped() (err error) { drop_status := &dnd.drop_status if len(drop_status.uri_list) == 0 { dnd.num_dropped_files = dnd.count_files_in_drop() dnd.end_drop(true) return dnd.render_screen() } f, err := dnd.new_tdir() if err != nil { return err } dnd.drop_status.dropping_to = new_dir_handle(f) // Write data URIs directly into the staging directory (applies to both local and remote). data_uri_counter := 0 for _, u := range drop_status.uri_list { if u.kind == parsed_uri_data { data_uri_counter++ fname := fmt.Sprintf("data-uri-%d%s", data_uri_counter, ext_for_mime(u.mime)) if werr := os.WriteFile(filepath.Join(f.Name(), fname), u.data, 0o644); werr != nil { return werr } } } if drop_status.is_remote_client { drop_status.root_remote_dir = &remote_dir_entry{} seen := utils.NewSet[string](len(drop_status.uri_list)) idx := slices.Index(drop_status.offered_mimes, "text/uri-list") for i, u := range drop_status.uri_list { var c *remote_dir_entry if u.kind == parsed_uri_data || u.path == "" { c = &remote_dir_entry{} drop_status.root_remote_dir.num_children_finished++ } else { name := filepath.Base(u.path) if !dnd.is_case_sensitive_filesystem { key := strings.ToLower(name) for q := 0; seen.Has(key); q++ { name = fmt.Sprintf(case_conflict_template, q+1, filepath.Base(u.path)) key = strings.ToLower(name) } seen.Add(key) } c = &remote_dir_entry{base_dir: dnd.drop_status.dropping_to.newref(), name: name} dnd.queue_data_request(DC{Type: 'r', X: idx + 1, Y: i + 1}) } drop_status.root_remote_dir.children = append(drop_status.root_remote_dir.children, c) } // If all children were pre-finished (e.g., all data URIs), complete the drop immediately. if drop_status.root_remote_dir.num_children_finished >= len(drop_status.root_remote_dir.children) { return dnd.all_drop_data_received() } drop_status.open_remote_dir = drop_status.root_remote_dir } else { // Build a file-path slice for the goroutine; data URI entries are already written above. file_paths := make([]string, len(drop_status.uri_list)) for i, u := range drop_status.uri_list { if u.kind == parsed_uri_file { file_paths[i] = u.path } } drop_status.local_copy.ctx, drop_status.local_copy.cancel_ctx = context.WithCancel(context.Background()) drop_status.local_copy.completion = make(chan error, 1) go do_local_copy_in_goroutine(drop_status.local_copy.ctx, f, drop_status.local_copy.completion, file_paths, func() { dnd.lp.WakeupMainThread() }) } return } func (dnd *dnd) request_mime_data() { accepted := utils.NewSetWithItems(dnd.drop_status.accepted_mimes...) seen := utils.NewSet[string](len(dnd.drop_status.accepted_mimes)) for idx, m := range dnd.drop_status.offered_mimes { if accepted.Has(m) && !seen.Has(m) { dnd.queue_data_request(DC{Type: 'r', X: idx + 1}) seen.Add(m) } } } var offered_mimes_buf strings.Builder func (dnd *dnd) on_drop_move(cell_x, cell_y int, has_more bool, offered_mimes string, is_drop bool, source_allowed_ops int) (needs_rerender bool) { prev_status := dnd.drop_status dnd.drop_status.cell_x, dnd.drop_status.cell_y = cell_x, cell_y if offered_mimes != "" { offered_mimes_buf.WriteString(offered_mimes) if has_more { return } offered_mimes := offered_mimes_buf.String() dnd.drop_status.offered_mimes = strings.Fields(offered_mimes) dnd.drop_status.accepted_mimes = make([]string, 0, len(dnd.drop_status.offered_mimes)) seen := utils.NewSet[string](len(dnd.drop_status.offered_mimes)) for _, x := range dnd.drop_status.offered_mimes { if _, found := dnd.drop_dests[x]; found && !seen.Has(x) { dnd.drop_status.accepted_mimes = append(dnd.drop_status.accepted_mimes, x) seen.Add(x) } } } offered_mimes_buf.Reset() if source_allowed_ops != 0 { dnd.drop_status.source_allowed_ops = source_allowed_ops } if dnd.copy_button_region.has(cell_x, cell_y) { dnd.drop_status.action = copy_on_drop } else if dnd.move_button_region.has(cell_x, cell_y) { dnd.drop_status.action = move_on_drop } else { switch dnd.opts.DropAnywhere { case "disallowed": dnd.drop_status.action = 0 dnd.drop_status.accepted_mimes = nil case "copy": dnd.drop_status.action = copy_on_drop case "move": dnd.drop_status.action = move_on_drop } } // Restrict to operations allowed by the drag source. if allowed := dnd.drop_status.source_allowed_ops; allowed != 0 && dnd.drop_status.action != 0 { if allowed&dnd.drop_status.action == 0 { dnd.drop_status.action = 0 dnd.drop_status.accepted_mimes = nil } } dnd.drop_status.in_window = cell_x > -1 && cell_y > -1 if !dnd.drop_status.in_window || dnd.drag_status.active { // disallow self drag and drop dnd.reset_drop() } mimes_changed := !slices.Equal(prev_status.accepted_mimes, dnd.drop_status.accepted_mimes) needs_rerender = prev_status.action != dnd.drop_status.action || mimes_changed if needs_rerender && !is_drop { c := DC{Type: 'm', Operation: dnd.drop_status.action} if dnd.drop_status.action != 0 && len(dnd.drop_status.accepted_mimes) > 0 { c.Payload = utils.UnsafeStringToBytes(strings.Join(dnd.drop_status.accepted_mimes, " ")) } dnd.lp.QueueDnDData(c) } needs_rerender = needs_rerender || dnd.drop_status.in_window != prev_status.in_window if is_drop { needs_rerender = true if dnd.drop_status.action == 0 || len(dnd.drop_status.accepted_mimes) == 0 || dnd.drag_status.active { dnd.end_drop(false) return } dnd.drop_status.reading_data = true dnd.request_mime_data() } return } var drop_buf []byte const max_inflight_data_rquests = 64 func (dnd *dnd) queue_data_request(cmd DC) { if dnd.drop_status.data_requests.in_flight_count < max_inflight_data_rquests { dnd.lp.QueueDnDData(cmd) dnd.drop_status.data_requests.in_flight_count++ } else { dnd.drop_status.data_requests.pending = append(dnd.drop_status.data_requests.pending, cmd) } } func (dnd *dnd) data_request_completed() { dnd.drop_status.data_requests.in_flight_count-- if len(dnd.drop_status.data_requests.pending) > 0 && dnd.drop_status.data_requests.in_flight_count < max_inflight_data_rquests { dnd.lp.QueueDnDData(dnd.drop_status.data_requests.pending[0]) dnd.drop_status.data_requests.in_flight_count++ dnd.drop_status.data_requests.pending = dnd.drop_status.data_requests.pending[1:] } } func (dnd *dnd) on_remote_drop_data(cmd DC) (err error) { drop_status := &dnd.drop_status if drop_status.open_remote_dir == nil { return fmt.Errorf("got a remote data response form the terminal without an open remote dir") } if cmd.X == 0 && cmd.Y == 0 && cmd.Yp == 0 { if drop_status.current_remote_entry == nil { return fmt.Errorf("got a remote data response form the terminal without a current remote entry") } } else { num := utils.IfElse(cmd.Yp != 0 && cmd.Yp != 1, cmd.X, cmd.Y) - 1 if num < 0 || num >= len(drop_status.open_remote_dir.children) { return fmt.Errorf("got a remote data response from the terminal for an entry that does not exist") } drop_status.current_remote_entry = drop_status.open_remote_dir.children[num] } e := drop_status.current_remote_entry if e.dest == nil { e.item_type = cmd.Xp switch cmd.Xp { case 0: f, err := utils.CreateExclusiveAt(e.base_dir.handle, e.name, 0o666) if err != nil { return err } e.dest = f default: e.dest = &bufferWriteCloser{&bytes.Buffer{}} } } if sz := max(4096, len(cmd.Payload)+4); len(drop_buf) < sz { drop_buf = make([]byte, sz) } if err = e.add_remote_data(cmd.Payload, drop_buf, cmd.Has_more, dnd.is_case_sensitive_filesystem); err != nil { return err } if e.dest == nil { // received all data for this entry drop_status.current_remote_entry = nil parent := drop_status.open_remote_dir parent.num_children_finished++ dnd.data_request_completed() if parent.num_children_finished >= len(parent.children) { // parent is finished drop_status.open_remote_dir = nil if parent.base_dir != nil { parent.base_dir = parent.base_dir.unref() } if parent.item_type != 0 { dnd.lp.QueueDnDData(DC{Type: 'r', Yp: parent.item_type}) // close directory in terminal } for _, c := range parent.children { is_pending := false if c.item_type != 0 && c.item_type != 1 { if len(c.children) > 0 { dnd.drop_status.pending_remote_dirs = append(dnd.drop_status.pending_remote_dirs, c) is_pending = true } } if !is_pending { if c.base_dir != nil { c.base_dir = c.base_dir.unref() } } } if len(drop_status.pending_remote_dirs) > 0 { drop_status.open_remote_dir = drop_status.pending_remote_dirs[0] drop_status.pending_remote_dirs = drop_status.pending_remote_dirs[1:] for i := range drop_status.open_remote_dir.children { dnd.queue_data_request(DC{Type: 'r', X: i + 1, Yp: drop_status.open_remote_dir.item_type}) } } else { return dnd.all_drop_data_received() } } } return nil } func (dnd *dnd) on_drop_data(cmd DC) error { drop_status := &dnd.drop_status if drop_status.root_remote_dir != nil { return dnd.on_remote_drop_data(cmd) } idx := cmd.X - 1 if idx < 0 || idx > len(drop_status.offered_mimes) { return fmt.Errorf("terminal sent drop data for an index (%d) outside the list of accepted MIMEs (length: %d)", idx, len(drop_status.offered_mimes)) } mime := drop_status.offered_mimes[idx] dest := dnd.drop_dests[mime] if cmd.Xp == 1 && mime == "text/uri-list" { drop_status.is_remote_client = true } if !cmd.Has_more && len(cmd.Payload) == 0 { if err := dest.finish(); err != nil { return err } if mime == "text/uri-list" { b := dest.dest.(*bufferWriteCloser) var err error if drop_status.uri_list, err = parse_uri_list(b.String()); err != nil { return err } } pending := false expecting := utils.NewSetWithItems(drop_status.accepted_mimes...) for _, d := range dnd.drop_dests { if !d.completed && expecting.Has(d.mime_type) { pending = true break } } if !pending { return dnd.all_mime_data_dropped() } return nil } if sz := max(4096, len(cmd.Payload)+4); len(drop_buf) < sz { drop_buf = make([]byte, sz) } return dest.add_data(cmd.Payload, drop_buf, cmd.Has_more) } func (dnd *dnd) drop_confirm(accepted bool) error { staging_dir := dnd.confirm_drop.staging_dir dnd.confirm_drop.overwrites = nil dnd.confirm_drop.staging_dir = nil defer staging_dir.Close() if !accepted { dnd.num_dropped_files = 0 } if accepted { if err := rename_contents(staging_dir, dnd.drop_output_dir); err != nil { dnd.end_drop(false) return err } dnd.end_drop(true) } else { dnd.end_drop(false) } return dnd.render_screen() }