From 04b80e49b02ad776c0a3ef354c9be25850d21861 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 23 Apr 2026 09:15:54 +0530 Subject: [PATCH] Move drop code into its own file --- kittens/dnd/drop.go | 426 ++++++++++++++++++++++++++++++++++++++ kittens/dnd/main.go | 472 ++---------------------------------------- kittens/dnd/render.go | 30 +++ 3 files changed, 469 insertions(+), 459 deletions(-) diff --git a/kittens/dnd/drop.go b/kittens/dnd/drop.go index c82e81167..18e4db564 100644 --- a/kittens/dnd/drop.go +++ b/kittens/dnd/drop.go @@ -1,10 +1,436 @@ package dnd import ( + "bytes" + "errors" "fmt" + "io" + "net/url" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + + "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 { + 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 (d *remote_dir_entry) add_remote_data(data []byte, output_buf []byte, has_more bool, parent *remote_dir_entry, 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() + parent.num_children_finished++ + }() + 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 derr := utils.MkdirAt(d.base_dir.handle, d.name, 0o755); derr != nil { + return derr + } + if f, derr := utils.OpenAt(d.base_dir.handle, d.name); 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 +} + +func parse_uri_list(src string) (ans []string, err error) { + for _, line := range utils.NewSeparatorScanner("", "\r\n").Split(src) { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "#") { + continue + } + if !strings.HasPrefix(line, "file://") { + ans = append(ans, "") + continue + } + p, err := url.Parse(line) + if err != nil { + return nil, err + } + ans = append(ans, filepath.Clean(p.Path)) + } + return +} + +type drop_status struct { + offered_mimes []string + accepted_mimes []string + uri_list []string + cell_x, cell_y int + action int + in_window bool + reading_data bool + is_remote_client bool + + root_remote_dir *remote_dir_entry + open_remote_dir *remote_dir_entry + pending_remote_dirs []*remote_dir_entry +} + +var reset_drop_status = drop_status{cell_x: -1, cell_y: -1} + +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() { + dnd.lp.QueueDnDData(DC{Type: 'r'}) // end 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_drop_status + dnd.render_screen() +} + +func (dnd *dnd) new_tdir() (dir_file *os.File, err error) { + dnd.tdir_counter++ + name := strconv.Itoa(dnd.tdir_counter) + if err = utils.MkdirAt(dnd.base_tempdir, name, 0o700); err != nil { + return nil, err + } + dir_file, err = utils.OpenAt(dnd.base_tempdir, name) + return +} + +func (dnd *dnd) all_mime_data_dropped() (err error) { + drop_status := &dnd.drop_status + if s, found := dnd.drop_dests["text/uri-list"]; found { + b := s.dest.(*bufferWriteCloser) + if drop_status.uri_list, err = parse_uri_list(b.String()); err != nil { + return err + } + } + if len(drop_status.uri_list) == 0 { + *drop_status = reset_drop_status + dnd.data_has_been_dropped = true + dnd.render_screen() + return + } + f, err := dnd.new_tdir() + if err != nil { + return err + } + rd := new_dir_handle(f) + defer rd.unref() + drop_status.root_remote_dir = &remote_dir_entry{} + if drop_status.is_remote_client { + seen := utils.NewSet[string](len(drop_status.uri_list)) + idx := slices.Index(drop_status.offered_mimes, "text/uri-list") + for i, x := range drop_status.uri_list { + var c *remote_dir_entry + if x == "" { + c = &remote_dir_entry{} + } else { + name := filepath.Base(x) + 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(x)) + key = strings.ToLower(name) + } + seen.Add(key) + } + c = &remote_dir_entry{base_dir: rd.newref(), name: name} + dnd.lp.QueueDnDData(DC{Type: 'r', X: idx + 1, Y: i + 1}) + } + drop_status.root_remote_dir.children = append(drop_status.root_remote_dir.children, c) + } + drop_status.open_remote_dir = drop_status.root_remote_dir + } else { + // TODO: copy URLs + } + return +} + +func (dnd *dnd) request_mime_data() { + accepted := utils.NewSetWithItems(dnd.drop_status.accepted_mimes...) + for idx, m := range dnd.drop_status.offered_mimes { + if accepted.Has(m) { + dnd.lp.QueueDnDData(DC{Type: 'r', X: idx + 1}) + } + } +} + +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) (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 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 + } + } + dnd.drop_status.in_window = cell_x > -1 && cell_y > -1 + if !dnd.drop_status.in_window || dnd.drag_started { // disallow self drag and drop + dnd.drop_status = reset_drop_status + } + 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_started { + dnd.end_drop() + return + } + dnd.drop_status.reading_data = true + dnd.request_mime_data() + } + return +} + +var current_remote_entry *remote_dir_entry +var drop_buf []byte + +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 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") + } + current_remote_entry = drop_status.open_remote_dir.children[num] + } + if current_remote_entry.dest == nil { + current_remote_entry.item_type = cmd.Xp + switch cmd.Xp { + case 0: + f, err := utils.CreateAt(drop_status.open_remote_dir.base_dir.handle, current_remote_entry.name) + if err != nil { + return err + } + current_remote_entry.dest = f + default: + current_remote_entry.dest = &bufferWriteCloser{&bytes.Buffer{}} + } + } + if sz := max(4096, len(cmd.Payload)+4); len(drop_buf) < sz { + drop_buf = make([]byte, sz) + } + if err = current_remote_entry.add_remote_data(cmd.Payload, drop_buf, cmd.Has_more, drop_status.open_remote_dir, dnd.is_case_sensitive_filesystem); err != nil { + return err + } + 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 a index outside the list of accepted 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 + } + dest.completed = true + pending := false + for _, d := range dnd.drop_dests { + if !d.completed { + 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) +} diff --git a/kittens/dnd/main.go b/kittens/dnd/main.go index 912e9d001..108f62300 100644 --- a/kittens/dnd/main.go +++ b/kittens/dnd/main.go @@ -4,7 +4,6 @@ package dnd import ( "bytes" - "errors" "fmt" "io" "maps" @@ -13,7 +12,6 @@ import ( "path/filepath" "runtime" "slices" - "strconv" "strings" "sync/atomic" @@ -21,8 +19,6 @@ import ( "github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/tui/loop" "github.com/kovidgoyal/kitty/tools/utils" - "github.com/kovidgoyal/kitty/tools/utils/streaming_base64" - "github.com/kovidgoyal/kitty/tools/wcswidth" ) var _ = fmt.Append @@ -51,98 +47,8 @@ func (bwc *bufferWriteCloser) Close() error { return nil } -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 { - 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 button_region struct { - left, width, top, height int -} - -func (r button_region) has(x, y int) bool { - return r.left <= x && x < r.left+r.width && r.top <= y && y < r.top+r.height -} - type DC = loop.DndCommand -func truncate_at_space(text string, width int) (string, string) { - truncated, p := wcswidth.TruncateToVisualLengthWithWidth(text, width) - if len(truncated) == len(text) { - return text, "" - } - i := strings.LastIndexByte(truncated, ' ') - if i > 0 && p-i < 12 { - p = i + 1 - } - return text[:p], text[p:] -} - type dir_handle struct { handle *os.File refcnt int32 @@ -165,129 +71,6 @@ func (d *dir_handle) unref() *dir_handle { 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 (d *remote_dir_entry) add_remote_data(data []byte, output_buf []byte, has_more bool, parent *remote_dir_entry, 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() - parent.num_children_finished++ - }() - 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 derr := utils.MkdirAt(d.base_dir.handle, d.name, 0o755); derr != nil { - return derr - } - if f, derr := utils.OpenAt(d.base_dir.handle, d.name); 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 drop_status struct { - offered_mimes []string - accepted_mimes []string - uri_list []string - cell_x, cell_y int - action int - in_window bool - reading_data bool - is_remote_client bool - - root_remote_dir *remote_dir_entry - open_remote_dir *remote_dir_entry - pending_remote_dirs []*remote_dir_entry -} - -func paragraph_as_lines(text string, width int) (ans []string) { - for text != "" { - var line string - if line, text = truncate_at_space(text, width); line != "" { - ans = append(ans, line) - } - } - return -} - -func parse_uri_list(src string) (ans []string, err error) { - for _, line := range utils.NewSeparatorScanner("", "\r\n").Split(src) { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "#") { - continue - } - if !strings.HasPrefix(line, "file://") { - ans = append(ans, "") - continue - } - p, err := url.Parse(line) - if err != nil { - return nil, err - } - ans = append(ans, filepath.Clean(p.Path)) - } - return -} - type drag_status struct { active bool } @@ -301,6 +84,7 @@ type dnd struct { lp *loop.Loop drop_status drop_status base_tempdir *os.File + tdir_counter int is_case_sensitive_filesystem bool data_has_been_dropped bool drag_started bool @@ -335,247 +119,16 @@ func (dnd *dnd) run_loop() (err error) { if err != nil { return } + dnd.base_tempdir = base_tdir_f if _, serr := os.Stat(filepath.Join(base_dir, strings.ToUpper(filepath.Base(base_tdir)))); serr == nil { dnd.is_case_sensitive_filesystem = false } - tdir_counter := 0 - new_tdir := func() (dir_file *os.File, err error) { - tdir_counter++ - name := strconv.Itoa(tdir_counter) - if err = utils.MkdirAt(base_tdir_f, name, 0o700); err != nil { - return nil, err - } - dir_file, err = utils.OpenAt(base_tdir_f, name) - return - } dnd.allow_drops, dnd.allow_drags = len(dnd.drop_dests) > 0, len(dnd.drag_sources) > 0 if dnd.lp, err = loop.New(); err != nil { return err } - - drop_status := drop_status{cell_x: -1, cell_y: -1} - reset_drop_status := drop_status - drop_status.cell_x, drop_status.cell_y = -1, -1 - - var offered_mimes_buf strings.Builder - - // Drop handling {{{ - var close_remote_tree func(*remote_dir_entry) - close_remote_tree = func(root *remote_dir_entry) { - if root.base_dir != nil { - root.base_dir = root.base_dir.unref() - } - for _, child := range root.children { - close_remote_tree(child) - } - } - - end_drop := func() { - dnd.lp.QueueDnDData(DC{Type: 'r'}) // end drop - if drop_status.root_remote_dir != nil { - close_remote_tree(drop_status.root_remote_dir) - drop_status.root_remote_dir = nil - } - drop_status = reset_drop_status - dnd.render_screen() - } - - all_mime_data_dropped := func() (err error) { - if s, found := dnd.drop_dests["text/uri-list"]; found { - b := s.dest.(*bufferWriteCloser) - if drop_status.uri_list, err = parse_uri_list(b.String()); err != nil { - return err - } - } - if len(drop_status.uri_list) == 0 { - drop_status = reset_drop_status - dnd.data_has_been_dropped = true - dnd.render_screen() - return - } - f, err := new_tdir() - if err != nil { - return err - } - rd := new_dir_handle(f) - defer rd.unref() - drop_status.root_remote_dir = &remote_dir_entry{} - if drop_status.is_remote_client { - seen := utils.NewSet[string](len(drop_status.uri_list)) - idx := slices.Index(drop_status.offered_mimes, "text/uri-list") - for i, x := range drop_status.uri_list { - var c *remote_dir_entry - if x == "" { - c = &remote_dir_entry{} - } else { - name := filepath.Base(x) - 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(x)) - key = strings.ToLower(name) - } - seen.Add(key) - } - c = &remote_dir_entry{base_dir: rd.newref(), name: name} - dnd.lp.QueueDnDData(DC{Type: 'r', X: idx + 1, Y: i + 1}) - } - drop_status.root_remote_dir.children = append(drop_status.root_remote_dir.children, c) - } - drop_status.open_remote_dir = drop_status.root_remote_dir - } else { - // TODO: copy URLs - } - return - } - - request_mime_data := func() { - accepted := utils.NewSetWithItems(drop_status.accepted_mimes...) - for idx, m := range drop_status.offered_mimes { - if accepted.Has(m) { - dnd.lp.QueueDnDData(DC{Type: 'r', X: idx + 1}) - } - } - } - - on_drop_move := func(cell_x, cell_y int, has_more bool, offered_mimes string, is_drop bool) (needs_rerender bool) { - prev_status := drop_status - drop_status.cell_x, 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() - drop_status.offered_mimes = strings.Fields(offered_mimes) - drop_status.accepted_mimes = make([]string, 0, len(drop_status.offered_mimes)) - seen := utils.NewSet[string](len(drop_status.offered_mimes)) - for _, x := range drop_status.offered_mimes { - if _, found := dnd.drop_dests[x]; found && !seen.Has(x) { - drop_status.accepted_mimes = append(drop_status.accepted_mimes, x) - seen.Add(x) - } - } - } - offered_mimes_buf.Reset() - if dnd.copy_button_region.has(cell_x, cell_y) { - drop_status.action = copy_on_drop - } else if dnd.move_button_region.has(cell_x, cell_y) { - drop_status.action = move_on_drop - } else { - switch dnd.opts.DropAnywhere { - case "disallowed": - drop_status.action = 0 - drop_status.accepted_mimes = nil - case "copy": - drop_status.action = copy_on_drop - case "move": - drop_status.action = move_on_drop - } - } - drop_status.in_window = cell_x > -1 && cell_y > -1 - if !drop_status.in_window || dnd.drag_started { // disallow self drag and drop - drop_status = reset_drop_status - } - mimes_changed := !slices.Equal(prev_status.accepted_mimes, drop_status.accepted_mimes) - needs_rerender = prev_status.action != drop_status.action || mimes_changed - if needs_rerender && !is_drop { - c := DC{Type: 'm', Operation: drop_status.action} - if drop_status.action != 0 && len(drop_status.accepted_mimes) > 0 { - c.Payload = utils.UnsafeStringToBytes(strings.Join(drop_status.accepted_mimes, " ")) - } - dnd.lp.QueueDnDData(c) - } - needs_rerender = needs_rerender || drop_status.in_window != prev_status.in_window - if is_drop { - needs_rerender = true - if drop_status.action == 0 || len(drop_status.accepted_mimes) == 0 || dnd.drag_started { - end_drop() - return - } - drop_status.reading_data = true - request_mime_data() - } - return - } - - var current_remote_entry *remote_dir_entry - var drop_buf []byte - - on_remote_drop_data := func(cmd DC) error { - 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 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") - } - current_remote_entry = drop_status.open_remote_dir.children[num] - } - if current_remote_entry.dest == nil { - current_remote_entry.item_type = cmd.Xp - switch cmd.Xp { - case 0: - f, err := utils.CreateAt(drop_status.open_remote_dir.base_dir.handle, current_remote_entry.name) - if err != nil { - return err - } - current_remote_entry.dest = f - default: - current_remote_entry.dest = &bufferWriteCloser{&bytes.Buffer{}} - } - } - if sz := max(4096, len(cmd.Payload)+4); len(drop_buf) < sz { - drop_buf = make([]byte, sz) - } - if err = current_remote_entry.add_remote_data(cmd.Payload, drop_buf, cmd.Has_more, drop_status.open_remote_dir, dnd.is_case_sensitive_filesystem); err != nil { - return err - } - return nil - } - - on_drop_data := func(cmd DC) error { - if drop_status.root_remote_dir != nil { - return 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 a index outside the list of accepted 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 - } - dest.completed = true - pending := false - for _, d := range dnd.drop_dests { - if !d.completed { - pending = true - break - } - } - if !pending { - return 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) - } - // }}} + dnd.drop_status = reset_drop_status dnd.lp.OnInitialize = func() (string, error) { dnd.lp.AllowLineWrapping(false) @@ -650,15 +203,15 @@ func (dnd *dnd) run_loop() (err error) { case "GEOMETRY": dnd.send_test_response(fmt.Sprintf("GEOMETRY:%d:%d:%d:%d:%d:%d:%d:%d", dnd.copy_button_region.left, dnd.copy_button_region.top, dnd.copy_button_region.width, dnd.copy_button_region.height, dnd.move_button_region.left, dnd.move_button_region.top, dnd.move_button_region.width, dnd.move_button_region.height)) case "DROP_MIMES": - if drop_status.offered_mimes != nil { - dnd.send_test_response(strings.Join(drop_status.offered_mimes, " ")) + if dnd.drop_status.offered_mimes != nil { + dnd.send_test_response(strings.Join(dnd.drop_status.offered_mimes, " ")) } else { dnd.send_test_response("") } case "DROP_IS_REMOTE": - dnd.send_test_response(utils.IfElse(drop_status.is_remote_client, "True", "False")) + dnd.send_test_response(utils.IfElse(dnd.drop_status.is_remote_client, "True", "False")) case "DROP_URI_LIST": - dnd.send_test_response(strings.Join(drop_status.uri_list, "|")) + dnd.send_test_response(strings.Join(dnd.drop_status.uri_list, "|")) default: dnd.send_test_response("UNKNOWN TEST COMMAND: " + string(cmd.Payload)) } @@ -668,22 +221,21 @@ func (dnd *dnd) run_loop() (err error) { if cmd.Payload != nil { payload = utils.UnsafeBytesToString(cmd.Payload) } - if on_drop_move(cmd.X, cmd.Y, cmd.Has_more, payload, false) { + if dnd.on_drop_move(cmd.X, cmd.Y, cmd.Has_more, payload, false) { dnd.render_screen() } case 'M': - if on_drop_move(cmd.X, cmd.Y, cmd.Has_more, utils.UnsafeBytesToString(cmd.Payload), true) { + if dnd.on_drop_move(cmd.X, cmd.Y, cmd.Has_more, utils.UnsafeBytesToString(cmd.Payload), true) { dnd.render_screen() } case 'R': return fmt.Errorf("error from the terminal while reading dropped data: %s", string(cmd.Payload)) case 'r': - err := on_drop_data(cmd) - dnd.render_screen() - return err + return dnd.on_drop_data(cmd) } return nil } + dnd.lp.OnKeyEvent = func(e *loop.KeyEvent) (err error) { e.Handled = true if e.MatchesPressOrRepeat("ctrl+c") || e.MatchesPressOrRepeat("esc") { @@ -692,9 +244,11 @@ func (dnd *dnd) run_loop() (err error) { } return nil } + dnd.lp.OnResize = func(old_size loop.ScreenSize, new_size loop.ScreenSize) error { return dnd.render_screen() } + err = dnd.lp.Run() if err != nil { return diff --git a/kittens/dnd/render.go b/kittens/dnd/render.go index b80ed54bc..74487f72d 100644 --- a/kittens/dnd/render.go +++ b/kittens/dnd/render.go @@ -10,6 +10,36 @@ import ( var _ = fmt.Print +type button_region struct { + left, width, top, height int +} + +func (r button_region) has(x, y int) bool { + return r.left <= x && x < r.left+r.width && r.top <= y && y < r.top+r.height +} + +func truncate_at_space(text string, width int) (string, string) { + truncated, p := wcswidth.TruncateToVisualLengthWithWidth(text, width) + if len(truncated) == len(text) { + return text, "" + } + i := strings.LastIndexByte(truncated, ' ') + if i > 0 && p-i < 12 { + p = i + 1 + } + return text[:p], text[p:] +} + +func paragraph_as_lines(text string, width int) (ans []string) { + for text != "" { + var line string + if line, text = truncate_at_space(text, width); line != "" { + ans = append(ans, line) + } + } + return +} + func (dnd *dnd) render_screen() error { lp := dnd.lp if !dnd.in_test_mode {