From 01e453a0483d7d076b999bea9ea2ff92d7305d04 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Apr 2026 20:16:02 +0530 Subject: [PATCH] More work on DnD kitten --- kittens/dnd/main.go | 69 +++++++++++++++++++++++++++------------ kitty/dnd.c | 10 ++++++ kitty_tests/dnd_kitten.py | 9 +++-- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/kittens/dnd/main.go b/kittens/dnd/main.go index 047b747c1..39acb9b44 100644 --- a/kittens/dnd/main.go +++ b/kittens/dnd/main.go @@ -51,6 +51,7 @@ type drop_dest struct { human_name, path string dest io.WriteCloser mime_type string + completed bool } type button_region struct { @@ -76,14 +77,14 @@ func truncate_at_space(text string, width int) (string, string) { } type drop_status struct { - offered_mimes []string - accepted_mimes []string - cell_x, cell_y int - action int - in_window bool - reading_data bool - requesting_mime_idx_plus_one int - is_remote_client bool + offered_mimes []string + accepted_mimes []string + cell_x, cell_y int + action int + in_window bool + reading_data bool + is_remote_client bool + remote_phase_started bool } func paragraph_as_lines(text string, width int) (ans []string) { @@ -96,7 +97,7 @@ func paragraph_as_lines(text string, width int) (ans []string) { return } -func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[string]drag_source, uri_list_buffer *bytes.Buffer) (err error) { +func run_loop(opts *Options, drop_dests map[string]*drop_dest, drag_sources map[string]drag_source, uri_list_buffer *bytes.Buffer) (err error) { allow_drops, allow_drags := len(drop_dests) > 0, len(drag_sources) > 0 data_has_been_dropped := false drag_started := false @@ -235,7 +236,7 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s render_screen() } - all_mime_data_dropped := func() { + all_mime_data_dropped := func() error { if _, found := drop_dests["text/uri-list"]; found && drop_status.is_remote_client { // TODO: Handle remote client } else { @@ -243,16 +244,13 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s data_has_been_dropped = true render_screen() } + return nil } request_mime_data := func() { - drop_status.requesting_mime_idx_plus_one++ - idx := drop_status.requesting_mime_idx_plus_one - 1 - if idx >= len(drop_status.accepted_mimes) { - all_mime_data_dropped() - return + for idx := range drop_status.accepted_mimes { + lp.QueueDnDData(DC{Type: 'r', X: idx + 1}) } - 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) { @@ -316,7 +314,38 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s return } + on_remote_drop_data := func(cmd DC) error { + // TODO: Implement this + return nil + } + on_drop_data := func(cmd DC) error { + if drop_status.remote_phase_started { + return on_remote_drop_data(cmd) + } + if cmd.X < 0 || cmd.X > len(drop_status.accepted_mimes) { + return fmt.Errorf("terminal sent drop data for a index outside the list of accepted MIMEs") + } + mime := drop_status.accepted_mimes[cmd.X] + dest := 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 { + dest.completed = true + pending := false + for _, d := range drop_dests { + if !d.completed { + pending = true + break + } + } + if !pending { + return all_mime_data_dropped() + } + return nil + } + // TODO: Implement this return nil } // }}} @@ -449,12 +478,12 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s } func dnd_main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) { - drop_dests := make(map[string]drop_dest) + drop_dests := make(map[string]*drop_dest) if os.Stdout != nil && !tty.IsTerminal(os.Stdout.Fd()) { - drop_dests["text/plain"] = drop_dest{human_name: "STDOUT", dest: os.Stdout, mime_type: "text/plain"} + drop_dests["text/plain"] = &drop_dest{human_name: "STDOUT", dest: os.Stdout, mime_type: "text/plain"} } uri_list_buffer := &bytes.Buffer{} - drop_dests["text/uri-list"] = drop_dest{ + drop_dests["text/uri-list"] = &drop_dest{ human_name: "Files", mime_type: "text/uri-list", dest: &bufferWriteCloser{uri_list_buffer}} for _, spec := range opts.Drop { mime, dest, _ := strings.Cut(spec, ":") @@ -465,7 +494,7 @@ func dnd_main(cmd *cli.Command, opts *Options, args []string) (rc int, err error if err != nil { return 1, err } - drop_dests[mime] = drop_dest{human_name: dest, path: path, mime_type: mime} + drop_dests[mime] = &drop_dest{human_name: dest, path: path, mime_type: mime} } } drag_sources := make(map[string]drag_source) diff --git a/kitty/dnd.c b/kitty/dnd.c index 357552517..9c349b3c6 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -2206,6 +2206,16 @@ dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) { if (w->drop.accepted_mimes == NULL) return PyUnicode_FromString(""); return PyUnicode_FromStringAndSize(w->drop.accepted_mimes, w->drop.accepted_mimes_sz); } + if (strcmp(q, "drop_data_requests") == 0) { + PyObject *ans = PyTuple_New(w->drop.num_data_requests); + for (size_t i = 0; i < w->drop.num_data_requests; i++) { +#define item w->drop.data_requests[i] + PyObject *x = Py_BuildValue("iii", item.cell_x, item.cell_y, item.pixel_y); + PyTuple_SET_ITEM(ans, i, x); +#undef item + } + return ans; + } Py_RETURN_NONE; } diff --git a/kitty_tests/dnd_kitten.py b/kitty_tests/dnd_kitten.py index 203d9c950..985a6a2a5 100644 --- a/kitty_tests/dnd_kitten.py +++ b/kitty_tests/dnd_kitten.py @@ -67,10 +67,11 @@ class TestDnDKitten(BaseTest): self.pty.write_to_child(chunk) self.pty.write_to_child(b'\x1b\\', flush=is_last and flush) - def finish_setup(self, remote_client: bool = False): + def finish_setup(self, remote_client: bool = False, cli_args = ()): cmd = [kitten_exe(), 'dnd'] if remote_client: cmd.append('--machine-id=remote-client-for-test') + cmd += list(cli_args) self.pty = self.enterContext(PTY(argv=cmd, cwd=self.kitten_wd, rows=25, columns=80, window_id=self.capture.window_id)) self.capture.pty = self.pty self.pty.callbacks.printbuf = self @@ -128,14 +129,13 @@ class TestDnDKitten(BaseTest): self.pty = None def test_dnd_kitten_drop(self): - self.finish_setup(remote_client=False) self.dnd_kitten_drop(False) def test_dnd_kitten_drop_remote(self): - self.finish_setup(remote_client=True) self.dnd_kitten_drop(True) def dnd_kitten_drop(self, remote_client): + self.finish_setup(remote_client=remote_client, cli_args=('--drop=image/png:images/image.png',)) copy, move = self.get_button_geometry() all_mimes = 'text/uri-list a/b c/d' for b, expected in ((copy, GLFW_DRAG_OPERATION_COPY), (move, GLFW_DRAG_OPERATION_MOVE)): @@ -153,11 +153,14 @@ class TestDnDKitten(BaseTest): self.wait_for_state('drop_action', GLFW_DRAG_OPERATION_COPY) self.send_dnd_command_to_kitten('DROP_MIMES') self.wait_for_responses(large_mimes) + del large_mimes dnd_test_fake_drop_event(self.capture.window_id, False) self.send_dnd_command_to_kitten('DROP_MIMES') self.wait_for_responses('') + all_mimes += ' image/png' dnd_test_fake_drop_event(self.capture.window_id, False, all_mimes.split(), copy[0] + 1, copy[1] + 1) self.wait_for_state('drop_action', GLFW_DRAG_OPERATION_COPY) dnd_test_fake_drop_event(self.capture.window_id, True, all_mimes.split(), copy[0] + 1, copy[1] + 1) self.send_dnd_command_to_kitten('DROP_MIMES') self.wait_for_responses(all_mimes) + self.wait_for_state('drop_data_requests', ((1,0,0), (2,0,0)))