Allow multiple drops on the dnd kitten

This commit is contained in:
Kovid Goyal
2026-04-26 18:42:14 +05:30
parent ab673768b3
commit 20bd31db0b
3 changed files with 83 additions and 38 deletions

View File

@@ -44,12 +44,15 @@ func open_file_for_writing(path string) (*os.File, error) {
func (d *drop_dest) write(chunk []byte) (err error) { func (d *drop_dest) write(chunk []byte) (err error) {
if d.dest == nil { if d.dest == nil {
if d.path == "" {
d.dest = &bufferWriteCloser{&bytes.Buffer{}}
} else {
d.dest, err = open_file_for_writing(d.path) d.dest, err = open_file_for_writing(d.path)
d.close_on_finish = true d.close_on_finish = true
if err != nil { if err != nil {
return return
} }
}
} }
_, err = d.dest.Write(chunk) _, err = d.dest.Write(chunk)
return return
@@ -205,6 +208,23 @@ func (d *drop_status) reset() {
*d = drop_status{cell_x: -1, cell_y: -1} *d = drop_status{cell_x: -1, cell_y: -1}
} }
func (d *drop_dest) reset() {
if d.dest != nil && d.dest != os.Stdout {
d.dest.Close()
d.dest = nil
}
d.completed = false
d.close_on_finish = false
d.b64_decoder = streaming_base64.StreamingBase64Decoder{}
}
func (dnd *dnd) reset_drop() {
dnd.drop_status.reset()
for _, x := range dnd.drop_dests {
x.reset()
}
}
func (root *remote_dir_entry) close_tree() { func (root *remote_dir_entry) close_tree() {
if root.base_dir != nil { if root.base_dir != nil {
root.base_dir = root.base_dir.unref() root.base_dir = root.base_dir.unref()
@@ -220,7 +240,7 @@ func (dnd *dnd) end_drop() {
dnd.drop_status.root_remote_dir.close_tree() dnd.drop_status.root_remote_dir.close_tree()
dnd.drop_status.root_remote_dir = nil dnd.drop_status.root_remote_dir = nil
} }
dnd.drop_status.reset() dnd.reset_drop()
dnd.render_screen() dnd.render_screen()
} }
@@ -239,7 +259,7 @@ func (dnd *dnd) all_mime_data_dropped() (err error) {
} }
} }
if len(drop_status.uri_list) == 0 { if len(drop_status.uri_list) == 0 {
dnd.drop_status.reset() dnd.reset_drop()
dnd.data_has_been_dropped = true dnd.data_has_been_dropped = true
dnd.render_screen() dnd.render_screen()
return return
@@ -328,7 +348,7 @@ func (dnd *dnd) on_drop_move(cell_x, cell_y int, has_more bool, offered_mimes st
} }
dnd.drop_status.in_window = cell_x > -1 && cell_y > -1 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 if !dnd.drop_status.in_window || dnd.drag_started { // disallow self drag and drop
dnd.drop_status.reset() dnd.reset_drop()
} }
mimes_changed := !slices.Equal(prev_status.accepted_mimes, dnd.drop_status.accepted_mimes) mimes_changed := !slices.Equal(prev_status.accepted_mimes, dnd.drop_status.accepted_mimes)
needs_rerender = prev_status.action != dnd.drop_status.action || mimes_changed needs_rerender = prev_status.action != dnd.drop_status.action || mimes_changed

View File

@@ -98,39 +98,50 @@ func (dnd *dnd) send_test_response(payload string) {
dnd.lp.DebugPrintln(payload) dnd.lp.DebugPrintln(payload)
} }
func (dnd *dnd) run_loop() (err error) { func (dnd *dnd) setup_base_dir(base_dir string) error {
base_dir, err := os.Getwd()
if err != nil {
return err
}
base_tdir, err := os.MkdirTemp(base_dir, ".dnd-kitten-drop-*") base_tdir, err := os.MkdirTemp(base_dir, ".dnd-kitten-drop-*")
if err != nil { if err != nil {
return err return err
} }
var base_tdir_f *os.File bf, err := os.Open(base_tdir)
defer func() {
if base_tdir_f != nil {
utils.RemoveChildren(base_tdir_f)
base_tdir_f.Close()
}
if terr := os.RemoveAll(base_tdir); terr != nil && err == nil {
err = terr
}
}()
base_tdir_f, err = os.Open(base_tdir)
if err != nil { if err != nil {
return os.RemoveAll(base_tdir)
return err
} }
dnd.base_tempdir = base_tdir_f dnd.base_tempdir = bf
if _, serr := os.Stat(filepath.Join(base_dir, strings.ToUpper(filepath.Base(base_tdir)))); serr == nil { if _, serr := os.Stat(filepath.Join(base_dir, strings.ToUpper(filepath.Base(base_tdir)))); serr == nil {
dnd.is_case_sensitive_filesystem = false dnd.is_case_sensitive_filesystem = false
} }
return nil
}
func (dnd *dnd) remove_tdir() error {
path := dnd.base_tempdir.Name()
dnd.base_tempdir.Close()
dnd.base_tempdir = nil
return os.RemoveAll(path)
}
func (dnd *dnd) run_loop() (err error) {
defer func() {
if dnd.in_test_mode && err != nil {
debugprintln("dnd kitten exiting with error: ", err)
}
}()
base_dir, err := os.Getwd()
if err != nil {
return err
}
if err = dnd.setup_base_dir(base_dir); err != nil {
return err
}
defer dnd.remove_tdir()
dnd.allow_drops, dnd.allow_drags = len(dnd.drop_dests) > 0, len(dnd.drag_sources) > 0 dnd.allow_drops, dnd.allow_drags = len(dnd.drop_dests) > 0, len(dnd.drag_sources) > 0
if dnd.lp, err = loop.New(); err != nil { if dnd.lp, err = loop.New(); err != nil {
return err return err
} }
dnd.drop_status.reset() dnd.reset_drop()
dnd.lp.OnInitialize = func() (string, error) { dnd.lp.OnInitialize = func() (string, error) {
dnd.lp.AllowLineWrapping(false) dnd.lp.AllowLineWrapping(false)
@@ -205,6 +216,8 @@ func (dnd *dnd) run_loop() (err error) {
dnd.drop_status.reset() dnd.drop_status.reset()
dnd.lp.StopAcceptingDrops() dnd.lp.StopAcceptingDrops()
dnd.lp.StopOfferingDrags() dnd.lp.StopOfferingDrags()
dnd.remove_tdir()
dnd.setup_base_dir(base_dir)
machine_id := "" machine_id := ""
if string(cmd.Payload) == "SETUP_REMOTE" { if string(cmd.Payload) == "SETUP_REMOTE" {
machine_id = "remote-client-for-test" machine_id = "remote-client-for-test"

View File

@@ -121,13 +121,21 @@ class TestDnDKitten(BaseTest):
self.capture.pty = self.pty self.capture.pty = self.pty
self.pty.callbacks.printbuf = self self.pty.callbacks.printbuf = self
self.screen = self.pty.screen self.screen = self.pty.screen
self.send_dnd_command_to_kitten('SETUP_REMOTE' if remote_client else 'SETUP_LOCAL') self.reset_kitten(remote_client, clear_tdir=False)
self.wait_for_responses('SETUP_DONE')
self.assertTrue(self.probe_state('drop_wanted')) self.assertTrue(self.probe_state('drop_wanted'))
self.assertEqual(remote_client, self.probe_state('drop_is_remote_client')) self.assertEqual(remote_client, self.probe_state('drop_is_remote_client'))
if self.probe_state('drag_can_offer'): if self.probe_state('drag_can_offer'):
self.assertEqual(remote_client, self.probe_state('drag_is_remote_client')) self.assertEqual(remote_client, self.probe_state('drag_is_remote_client'))
def reset_kitten(self, remote_client: bool, clear_tdir=True):
if clear_tdir:
shutil.rmtree(self.kitten_wd)
os.mkdir(self.kitten_wd)
shutil.rmtree(self.src_data_dir)
os.mkdir(self.src_data_dir)
self.send_dnd_command_to_kitten('SETUP_REMOTE' if remote_client else 'SETUP_LOCAL')
self.wait_for_responses('SETUP_DONE')
def get_button_geometry(self, are_present: bool = True): def get_button_geometry(self, are_present: bool = True):
self.send_dnd_command_to_kitten('GEOMETRY') self.send_dnd_command_to_kitten('GEOMETRY')
self.pty.wait_till(lambda: bool(self.messages_from_kitten)) self.pty.wait_till(lambda: bool(self.messages_from_kitten))
@@ -152,7 +160,7 @@ class TestDnDKitten(BaseTest):
def wait_till(): def wait_till():
return q == self.messages_from_kitten.strip() return q == self.messages_from_kitten.strip()
try: try:
self.pty.wait_till(wait_till, timeout, lambda: f'Responses so far: {self.messages_from_kitten!r}') self.pty.wait_till(wait_till, timeout, lambda: f'Responses so far: Expected:\n{q!r}\nActual:\n{self.messages_from_kitten.strip()!r} != {q!r}')
finally: finally:
self.messages_from_kitten = '' self.messages_from_kitten = ''
@@ -166,6 +174,10 @@ class TestDnDKitten(BaseTest):
self.send_dnd_command_to_kitten('PING') self.send_dnd_command_to_kitten('PING')
self.wait_for_responses('PONG') self.wait_for_responses('PONG')
def exit_kitten(self):
self.pty.write_to_child('\x1b[27u') # ]
self.pty.wait_till_child_exits(require_exit_code=0)
def tearDown(self): def tearDown(self):
dnd_set_test_write_func(None) dnd_set_test_write_func(None)
dnd_test_cleanup_fake_window(self.capture.os_window_id) dnd_test_cleanup_fake_window(self.capture.os_window_id)
@@ -174,14 +186,14 @@ class TestDnDKitten(BaseTest):
self.pty = None self.pty = None
def test_dnd_kitten_drop(self): def test_dnd_kitten_drop(self):
self.dnd_kitten_drop(False)
def test_dnd_kitten_drop_remote(self):
self.dnd_kitten_drop(True)
def dnd_kitten_drop(self, remote_client):
img_drop_path = 'images/image.png' img_drop_path = 'images/image.png'
self.finish_setup(remote_client=remote_client, cli_args=(f'--drop=image/png:{img_drop_path}',)) self.finish_setup(cli_args=(f'--drop=image/png:{img_drop_path}',))
self.dnd_kitten_drop(False, img_drop_path)
self.reset_kitten(True)
self.dnd_kitten_drop(True, img_drop_path)
self.exit_kitten()
def dnd_kitten_drop(self, remote_client, img_drop_path):
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)):
@@ -215,7 +227,7 @@ class TestDnDKitten(BaseTest):
self.assertEqual('text/uri-list', self.probe_state('drop_getting_data_for_mime')) self.assertEqual('text/uri-list', self.probe_state('drop_getting_data_for_mime'))
create_fs(self.src_data_dir) create_fs(self.src_data_dir)
uri_list, path_list = [], [] uri_list, path_list = [], []
for x in os.listdir(self.src_data_dir): for x in sorted(os.listdir(self.src_data_dir)):
uri_list.append(as_file_url(self.src_data_dir, x)) uri_list.append(as_file_url(self.src_data_dir, x))
uri_list = ['moose:cow', '# file:///frog/march'] + uri_list uri_list = ['moose:cow', '# file:///frog/march'] + uri_list
uri_list.insert(3, 'ignore://me') uri_list.insert(3, 'ignore://me')