More work on DnD kitten

This commit is contained in:
Kovid Goyal
2026-04-20 20:16:02 +05:30
parent a899d24b64
commit 01e453a048
3 changed files with 65 additions and 23 deletions

View File

@@ -51,6 +51,7 @@ type drop_dest struct {
human_name, path string human_name, path string
dest io.WriteCloser dest io.WriteCloser
mime_type string mime_type string
completed bool
} }
type button_region struct { type button_region struct {
@@ -82,8 +83,8 @@ type drop_status struct {
action int action int
in_window bool in_window bool
reading_data bool reading_data bool
requesting_mime_idx_plus_one int
is_remote_client bool is_remote_client bool
remote_phase_started bool
} }
func paragraph_as_lines(text string, width int) (ans []string) { 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 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 allow_drops, allow_drags := len(drop_dests) > 0, len(drag_sources) > 0
data_has_been_dropped := false data_has_been_dropped := false
drag_started := 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() 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 { if _, found := drop_dests["text/uri-list"]; found && drop_status.is_remote_client {
// TODO: Handle remote client // TODO: Handle remote client
} else { } else {
@@ -243,17 +244,14 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
data_has_been_dropped = true data_has_been_dropped = true
render_screen() render_screen()
} }
return nil
} }
request_mime_data := func() { request_mime_data := func() {
drop_status.requesting_mime_idx_plus_one++ for idx := range drop_status.accepted_mimes {
idx := drop_status.requesting_mime_idx_plus_one - 1
if idx >= len(drop_status.accepted_mimes) {
all_mime_data_dropped()
return
}
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) { 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 prev_status := drop_status
@@ -316,7 +314,38 @@ func run_loop(opts *Options, drop_dests map[string]drop_dest, drag_sources map[s
return return
} }
on_remote_drop_data := func(cmd DC) error {
// TODO: Implement this
return nil
}
on_drop_data := func(cmd DC) error { 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 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) { 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()) { 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{} 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}} human_name: "Files", mime_type: "text/uri-list", dest: &bufferWriteCloser{uri_list_buffer}}
for _, spec := range opts.Drop { for _, spec := range opts.Drop {
mime, dest, _ := strings.Cut(spec, ":") 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 { if err != nil {
return 1, err 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) drag_sources := make(map[string]drag_source)

View File

@@ -2206,6 +2206,16 @@ dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) {
if (w->drop.accepted_mimes == NULL) return PyUnicode_FromString(""); if (w->drop.accepted_mimes == NULL) return PyUnicode_FromString("");
return PyUnicode_FromStringAndSize(w->drop.accepted_mimes, w->drop.accepted_mimes_sz); 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; Py_RETURN_NONE;
} }

View File

@@ -67,10 +67,11 @@ class TestDnDKitten(BaseTest):
self.pty.write_to_child(chunk) self.pty.write_to_child(chunk)
self.pty.write_to_child(b'\x1b\\', flush=is_last and flush) 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'] cmd = [kitten_exe(), 'dnd']
if remote_client: if remote_client:
cmd.append('--machine-id=remote-client-for-test') 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.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.capture.pty = self.pty
self.pty.callbacks.printbuf = self self.pty.callbacks.printbuf = self
@@ -128,14 +129,13 @@ class TestDnDKitten(BaseTest):
self.pty = None self.pty = None
def test_dnd_kitten_drop(self): def test_dnd_kitten_drop(self):
self.finish_setup(remote_client=False)
self.dnd_kitten_drop(False) self.dnd_kitten_drop(False)
def test_dnd_kitten_drop_remote(self): def test_dnd_kitten_drop_remote(self):
self.finish_setup(remote_client=True)
self.dnd_kitten_drop(True) self.dnd_kitten_drop(True)
def dnd_kitten_drop(self, remote_client): 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() copy, move = self.get_button_geometry()
all_mimes = 'text/uri-list a/b c/d' all_mimes = 'text/uri-list a/b c/d'
for b, expected in ((copy, GLFW_DRAG_OPERATION_COPY), (move, GLFW_DRAG_OPERATION_MOVE)): 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.wait_for_state('drop_action', GLFW_DRAG_OPERATION_COPY)
self.send_dnd_command_to_kitten('DROP_MIMES') self.send_dnd_command_to_kitten('DROP_MIMES')
self.wait_for_responses(large_mimes) self.wait_for_responses(large_mimes)
del large_mimes
dnd_test_fake_drop_event(self.capture.window_id, False) dnd_test_fake_drop_event(self.capture.window_id, False)
self.send_dnd_command_to_kitten('DROP_MIMES') self.send_dnd_command_to_kitten('DROP_MIMES')
self.wait_for_responses('') 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) 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) 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) 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.send_dnd_command_to_kitten('DROP_MIMES')
self.wait_for_responses(all_mimes) self.wait_for_responses(all_mimes)
self.wait_for_state('drop_data_requests', ((1,0,0), (2,0,0)))