mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-10 22:24:24 +02:00
Implement writing of MIME data on drop
This commit is contained in:
@@ -4,6 +4,7 @@ package dnd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
"github.com/kovidgoyal/kitty/tools/tty"
|
"github.com/kovidgoyal/kitty/tools/tty"
|
||||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
|
"github.com/kovidgoyal/kitty/tools/utils/streaming_base64"
|
||||||
"github.com/kovidgoyal/kitty/tools/wcswidth"
|
"github.com/kovidgoyal/kitty/tools/wcswidth"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,6 +54,69 @@ type drop_dest struct {
|
|||||||
dest io.WriteCloser
|
dest io.WriteCloser
|
||||||
mime_type string
|
mime_type string
|
||||||
completed bool
|
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 {
|
type button_region struct {
|
||||||
@@ -322,20 +387,25 @@ func run_loop(opts *Options, drop_dests map[string]*drop_dest, drag_sources map[
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var drop_buf []byte
|
||||||
|
|
||||||
on_drop_data := func(cmd DC) error {
|
on_drop_data := func(cmd DC) error {
|
||||||
if drop_status.remote_phase_started {
|
if drop_status.remote_phase_started {
|
||||||
return on_remote_drop_data(cmd)
|
return on_remote_drop_data(cmd)
|
||||||
}
|
}
|
||||||
idx := cmd.X - 1
|
idx := cmd.X - 1
|
||||||
if idx < 0 || idx > len(drop_status.accepted_mimes) {
|
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")
|
return fmt.Errorf("terminal sent drop data for a index outside the list of accepted MIMEs")
|
||||||
}
|
}
|
||||||
mime := drop_status.accepted_mimes[idx]
|
mime := drop_status.offered_mimes[idx]
|
||||||
dest := drop_dests[mime]
|
dest := drop_dests[mime]
|
||||||
if cmd.Xp == 1 && mime == "text/uri-list" {
|
if cmd.Xp == 1 && mime == "text/uri-list" {
|
||||||
drop_status.is_remote_client = true
|
drop_status.is_remote_client = true
|
||||||
}
|
}
|
||||||
if !cmd.Has_more && len(cmd.Payload) == 0 {
|
if !cmd.Has_more && len(cmd.Payload) == 0 {
|
||||||
|
if err := dest.finish(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
dest.completed = true
|
dest.completed = true
|
||||||
pending := false
|
pending := false
|
||||||
for _, d := range drop_dests {
|
for _, d := range drop_dests {
|
||||||
@@ -349,8 +419,10 @@ func run_loop(opts *Options, drop_dests map[string]*drop_dest, drag_sources map[
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// TODO: Implement this
|
if sz := max(4096, len(cmd.Payload)+4); len(drop_buf) < sz {
|
||||||
return nil
|
drop_buf = make([]byte, sz)
|
||||||
|
}
|
||||||
|
return dest.add_data(cmd.Payload, drop_buf, cmd.Has_more)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
|
|||||||
@@ -2094,7 +2094,7 @@ dnd_test_fake_drop_data(PyObject *self UNUSED, PyObject *args) {
|
|||||||
const char *mime;
|
const char *mime;
|
||||||
RAII_PY_BUFFER(data);
|
RAII_PY_BUFFER(data);
|
||||||
int error_code = 0, no_eod = 0;
|
int error_code = 0, no_eod = 0;
|
||||||
if (!PyArg_ParseTuple(args, "Ksy*|ii", &window_id, &mime, &data, &error_code, &no_eod)) return NULL;
|
if (!PyArg_ParseTuple(args, "Ksy*|ip", &window_id, &mime, &data, &error_code, &no_eod)) return NULL;
|
||||||
Window *w = window_for_window_id((id_type)window_id);
|
Window *w = window_for_window_id((id_type)window_id);
|
||||||
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
|
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
|
||||||
if (error_code > 0) {
|
if (error_code > 0) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -37,7 +38,7 @@ def create_fs(base):
|
|||||||
if sz == 0:
|
if sz == 0:
|
||||||
sz = random.randint(5713, 9879)
|
sz = random.randint(5713, 9879)
|
||||||
with open(join(*path), 'wb') as f:
|
with open(join(*path), 'wb') as f:
|
||||||
f.write(os.urandom(sz))
|
f.write(b'x' * sz)
|
||||||
os.makedirs(join('d1', 'sd', 'ssd'))
|
os.makedirs(join('d1', 'sd', 'ssd'))
|
||||||
os.mkdir(join('d2'))
|
os.mkdir(join('d2'))
|
||||||
os.symlink('/does-not-exist', join('s1'))
|
os.symlink('/does-not-exist', join('s1'))
|
||||||
@@ -160,7 +161,8 @@ class TestDnDKitten(BaseTest):
|
|||||||
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',))
|
img_drop_path = 'images/image.png'
|
||||||
|
self.finish_setup(remote_client=remote_client, cli_args=(f'--drop=image/png:{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)):
|
||||||
@@ -200,5 +202,24 @@ class TestDnDKitten(BaseTest):
|
|||||||
uri_list.insert(3, 'ignore://me')
|
uri_list.insert(3, 'ignore://me')
|
||||||
dnd_test_fake_drop_data(self.capture.window_id, 'text/uri-list', '\r\n'.join(uri_list).encode())
|
dnd_test_fake_drop_data(self.capture.window_id, 'text/uri-list', '\r\n'.join(uri_list).encode())
|
||||||
self.assertEqual('image/png', self.probe_state('drop_getting_data_for_mime'))
|
self.assertEqual('image/png', self.probe_state('drop_getting_data_for_mime'))
|
||||||
|
|
||||||
|
def send_file_in_chunks(f, mime, sz=3072):
|
||||||
|
while True:
|
||||||
|
chunk = f.read(sz)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
dnd_test_fake_drop_data(self.capture.window_id, mime, chunk, 0, True)
|
||||||
|
dnd_test_fake_drop_data(self.capture.window_id, mime, b'')
|
||||||
|
|
||||||
|
with open(os.path.join(self.src_data_dir, 'some-image.png'), 'rb') as f:
|
||||||
|
send_file_in_chunks(f, 'image/png', 1117)
|
||||||
|
|
||||||
self.send_dnd_command_to_kitten('DROP_IS_REMOTE')
|
self.send_dnd_command_to_kitten('DROP_IS_REMOTE')
|
||||||
self.wait_for_responses(str(remote_client))
|
self.wait_for_responses(str(remote_client))
|
||||||
|
jn = os.path.join
|
||||||
|
self.assert_files_have_same_content(jn(self.src_data_dir, 'some-image.png'), jn(self.kitten_wd, img_drop_path))
|
||||||
|
shutil.rmtree(os.path.dirname(jn(self.kitten_wd, img_drop_path)))
|
||||||
|
|
||||||
|
def assert_files_have_same_content(self, a, b):
|
||||||
|
with open(a, 'rb') as fa, open(b, 'rb') as fb:
|
||||||
|
self.assertEqual(fa.read(), fb.read(), f'{a} ({os.path.getsize(a)}) != {b} ({os.path.getsize(b)})')
|
||||||
|
|||||||
Reference in New Issue
Block a user