mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 16:37:27 +00:00
Add machine id and stub for t=k transfers
This commit is contained in:
parent
cfa9f1bada
commit
df20d4aa7c
9 changed files with 97 additions and 38 deletions
|
|
@ -232,10 +232,12 @@ Terminal programs can inform the terminal emulator that they
|
|||
are willing to act as a source of drag data by sending the
|
||||
sending the escape code::
|
||||
|
||||
OSC _dnd_code ; t=o ST
|
||||
OSC _dnd_code ; t=o:x=1 ; optional machine id ST
|
||||
|
||||
On exit, or if the program no longer is willing to start drag gestures, it must
|
||||
send ``t=O`` to the terminal to indicate it no longer wants to offer drag data.
|
||||
send ``t=o:x=2`` to the terminal to indicate it no longer wants to offer drag data.
|
||||
The ``machine id`` is optional and is used to enable dragging from remote
|
||||
machines. See :ref:`below <machine_id>` for its semantics.
|
||||
|
||||
When the user performs the platform specific gesture to start a drag operation,
|
||||
the terminal will send the same escape code back to the terminal program
|
||||
|
|
@ -356,7 +358,9 @@ Dragging to remote machines
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To support dragging files to remote machines, when requesting the data for the
|
||||
``text/uri-list`` MIME type, terminal emulators can add the ``Y=1`` key. On
|
||||
``text/uri-list`` MIME type, terminal emulators can add the ``Y=1`` key.
|
||||
Terminals can examine the :ref:`machine_id` sent with the enable drag offers
|
||||
``t=o`` escape code to decide whether to use ``Y=1`` or not. On
|
||||
receipt of this key, the client should first send the ``text/uri-list`` as
|
||||
normal and then a series of responses for every ``file://`` URL type in the
|
||||
list of the form::
|
||||
|
|
@ -366,12 +370,12 @@ list of the form::
|
|||
OSC _dnd_code ; t=k:x=idx:X=handle:m=0 or 1 ; base64 encoded null separated list of directory entries ST
|
||||
|
||||
These represent possibly chunked data for files, symlinks and directories, as
|
||||
denoted by the ``X`` key. As always, end of data is indicated by an escape code
|
||||
with ``m=0`` and no payload. ``idx`` is the one based index into the list of
|
||||
entries in the ``text/uri-list`` MIME type. ``file://`` URLs that point to
|
||||
symlinks must be resolved to files or directories and sent. So actual symlinks
|
||||
will appear only when recursing through directories as described below. Only
|
||||
regular files should be sent.
|
||||
denoted by the ``X`` key. As above, end of data for an individual entry is
|
||||
indicated by an escape code with ``m=0`` and no payload. ``idx`` is the one
|
||||
based index into the list of entries in the ``text/uri-list`` MIME type.
|
||||
``file://`` URLs that point to symlinks must be resolved to files or
|
||||
directories and sent. So actual symlinks will appear only when recursing
|
||||
through directories as described below. Only regular files should be sent.
|
||||
|
||||
Terminals should write the transmitted data into a temporary directory
|
||||
and replace the entries in the ``text/uri-list`` data with the transmitted
|
||||
|
|
@ -384,13 +388,17 @@ that serves as an identifier for the directory. Directories must be traversed
|
|||
in breadth first order. The children of a directory are reported by
|
||||
adding ``Y=parent-handle:y=num`` to the escape codes above. Here
|
||||
``parent-handle`` is the handle of the directory being traversed and ``num``
|
||||
is the one based index into the list of entries in the directory.
|
||||
is the one based index into the list of entries in the directory. Thus, the
|
||||
set of keys ``x, y, Y`` uniquely determine an entry.
|
||||
|
||||
Once all data is transmitted, the client informs the terminal emulator of
|
||||
completion with::
|
||||
|
||||
OSC _dnd_code ; t=k ; ST
|
||||
|
||||
At this point, the terminal should send the modified data for ``text/uri-list``
|
||||
to the drop destination.
|
||||
|
||||
If any error occurs in the client while reading the data, it can inform
|
||||
the terminal using::
|
||||
|
||||
|
|
@ -432,8 +440,7 @@ Key Value Default Description
|
|||
``M`` - a drop dropped event
|
||||
``r`` - request dropped data
|
||||
``R`` - report an error
|
||||
``o`` - start offering drags
|
||||
``O`` - stop offering drags
|
||||
``o`` - start offering drags or start a drag
|
||||
``p`` - present data for drag offers
|
||||
``P`` - Change drag image or start drag
|
||||
``e`` - a drag offer event occurred
|
||||
|
|
@ -460,4 +467,16 @@ Key Value Default Description
|
|||
======= ==================== ========= =================
|
||||
|
||||
|
||||
.. _machine_id:
|
||||
|
||||
Machine id
|
||||
-----------------
|
||||
|
||||
The machine id is used to detect when a drag is started on a remote machine. It
|
||||
is of the form: ``version:ASCII printable chars``. The leading ``version`` field
|
||||
allows for changing the format or semantics of this field in the future. The
|
||||
actual id is the machine id (the contents of :file:`/etc/machine-id` on
|
||||
Linux/BSD and :file:`HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid` on Windows and ``IOPlatformUUID`` on macOS). This machine id is then hashed using a :rfc:`HMAC <2104>`
|
||||
with :rfc:`SHA-256 <6234>` as the digest algorithm and the key being the ASCII bytes:
|
||||
``tty-dnd-protocol-machine-id``. The hashing is done so as to not easily leak the
|
||||
actual machine id and to ensure that the value is of fixed size.
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ def parsers() -> None:
|
|||
write_header(text, 'kitty/parse-multicell-command.h')
|
||||
|
||||
keymap = {
|
||||
't': ('type', flag('aAmMrRoOpPeEk')),
|
||||
't': ('type', flag('aAmMrRopPeEk')),
|
||||
'm': ('more', 'uint'),
|
||||
'i': ('client_id', 'uint'),
|
||||
'o': ('operation', 'uint'),
|
||||
|
|
|
|||
40
kitty/dnd.c
40
kitty/dnd.c
|
|
@ -1067,6 +1067,7 @@ drag_free_built_data(Window *w) {
|
|||
free(ds.items);
|
||||
ds.items = NULL;
|
||||
}
|
||||
ds.num_mimes = 0;
|
||||
for (size_t i = 0; i < arraysz(ds.images); i++) {
|
||||
if (ds.images[i].data) free(ds.images[i].data);
|
||||
zero_at_ptr(ds.images + i);
|
||||
|
|
@ -1075,11 +1076,10 @@ drag_free_built_data(Window *w) {
|
|||
|
||||
void
|
||||
drag_free_offer(Window *w) {
|
||||
free(ds.mimes_buf); ds.mimes_buf = NULL;
|
||||
free(ds.mimes_buf); ds.mimes_buf = NULL; ds.bufsz = 0;
|
||||
drag_free_built_data(w);
|
||||
ds.allowed_operations = 0;
|
||||
ds.state = DRAG_SOURCE_NONE;
|
||||
ds.num_mimes = 0;
|
||||
ds.pre_sent_total_sz = 0;
|
||||
ds.images_sent_total_sz = 0;
|
||||
}
|
||||
|
|
@ -1100,12 +1100,31 @@ cancel_drag(Window *w, int error_code) {
|
|||
drag_free_offer(w);
|
||||
}
|
||||
|
||||
#define abrt(code) { cancel_drag(w, code); return; }
|
||||
|
||||
void
|
||||
drag_start_offerring(Window *w, const char *client_machine_id, size_t sz) {
|
||||
ds.can_offer = true; ds.is_remote_client = false;
|
||||
if (sz && client_machine_id) {
|
||||
const char *host_machine_id = machine_id();
|
||||
if (host_machine_id) {
|
||||
size_t hsz = strlen(host_machine_id);
|
||||
if (hsz != sz || memcmp(host_machine_id, client_machine_id, sz) != 0) ds.is_remote_client = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
drag_stop_offerring(Window *w) {
|
||||
drag_free_offer(w);
|
||||
ds.can_offer = false; ds.is_remote_client = false;
|
||||
}
|
||||
|
||||
void
|
||||
drag_add_mimes(Window *w, int allowed_operations, uint32_t client_id, const char *data, size_t sz, bool has_more) {
|
||||
#define abrt(code) { cancel_drag(w, code); return; }
|
||||
if (allowed_operations && ds.state != DRAG_SOURCE_NONE) cancel_drag(w, 0);
|
||||
if (!ds.can_offer) abrt(EINVAL);
|
||||
if (allowed_operations && !ds.allowed_operations) ds.allowed_operations = allowed_operations;
|
||||
if (!ds.allowed_operations) { abrt(EINVAL); }
|
||||
if (!ds.allowed_operations || ds.state > DRAG_SOURCE_BEING_BUILT) abrt(EINVAL);
|
||||
ds.state = DRAG_SOURCE_BEING_BUILT;
|
||||
ds.client_id = client_id;
|
||||
size_t new_sz = ds.bufsz + sz;
|
||||
|
|
@ -1348,7 +1367,8 @@ drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) {
|
|||
}
|
||||
// No fd yet, request data from the client
|
||||
char buf[128];
|
||||
int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu", DND_CODE, DRAG_NOTIFY_FINISHED + 2, i);
|
||||
int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu:Y=%d",
|
||||
DND_CODE, DRAG_NOTIFY_FINISHED + 2, i, ds.is_remote_client);
|
||||
queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false);
|
||||
*err_code = EAGAIN;
|
||||
return NULL;
|
||||
|
|
@ -1449,6 +1469,14 @@ drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *paylo
|
|||
if (ret) cancel_drag(w, ret);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
drag_remote_file_data(
|
||||
Window *w, int32_t x, int32_t y, int32_t X, int32_t Y, bool has_more, const uint8_t *payload, size_t payload_sz
|
||||
) {
|
||||
(void)w; (void)x; (void)y; (void)X; (void)Y; (void)has_more; (void)payload; (void)payload_sz;
|
||||
// TODO: Implement this
|
||||
}
|
||||
#undef img
|
||||
#undef abrt
|
||||
#undef ds
|
||||
|
|
|
|||
|
|
@ -35,3 +35,6 @@ void drag_notify(Window *w, DragNotifyType type);
|
|||
int drag_free_data(Window *w, const char *mime_type, const char* data, size_t sz);
|
||||
const char* drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code);
|
||||
void drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *payload, size_t payload_sz);
|
||||
void drag_remote_file_data(Window *w, int32_t x, int32_t y, int32_t X, int32_t Y, bool has_more, const uint8_t *payload, size_t payload_sz);
|
||||
void drag_start_offerring(Window *w, const char *client_machine_id, size_t sz);
|
||||
void drag_stop_offerring(Window *w);
|
||||
|
|
|
|||
|
|
@ -86,10 +86,9 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
|
|||
|
||||
case type: {
|
||||
g.type = parser_buf[pos++];
|
||||
if (g.type != 'A' && g.type != 'E' && g.type != 'M' && g.type != 'O' &&
|
||||
g.type != 'P' && g.type != 'R' && g.type != 'a' && g.type != 'e' &&
|
||||
g.type != 'k' && g.type != 'm' && g.type != 'o' && g.type != 'p' &&
|
||||
g.type != 'r') {
|
||||
if (g.type != 'A' && g.type != 'E' && g.type != 'M' && g.type != 'P' &&
|
||||
g.type != 'R' && g.type != 'a' && g.type != 'e' && g.type != 'k' &&
|
||||
g.type != 'm' && g.type != 'o' && g.type != 'p' && g.type != 'r') {
|
||||
REPORT_ERROR("Malformed DnDCommand control block, unknown flag value "
|
||||
"for type: 0x%x",
|
||||
g.type);
|
||||
|
|
|
|||
|
|
@ -1535,14 +1535,13 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa
|
|||
drop_enqueue_request(w, cmd->cell_x, cmd->cell_y, cmd->pixel_y);
|
||||
} break;
|
||||
case 'o': {
|
||||
if (cmd->payload_sz > 0) drag_add_mimes(w, (int)cmd->operation, cmd->client_id, (const char*)payload, cmd->payload_sz, cmd->more);
|
||||
else w->drag_source.can_offer = true;
|
||||
} break;
|
||||
case 'O': {
|
||||
drag_free_offer(w);
|
||||
w->drag_source.can_offer = false;
|
||||
if (global_state.drag_source.is_active && global_state.drag_source.from_window == w->id) {
|
||||
cancel_current_drag_source();
|
||||
switch (cmd->cell_x) {
|
||||
case 1: drag_start_offerring(w, (const char*)payload, cmd->payload_sz); break;
|
||||
case 2: drag_stop_offerring(w); break;
|
||||
case 0:
|
||||
drag_add_mimes(
|
||||
w, (int)cmd->operation, cmd->client_id, (const char*)payload, cmd->payload_sz, cmd->more);
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
case 'p': {
|
||||
|
|
@ -1564,6 +1563,10 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa
|
|||
}
|
||||
} else drag_process_item_data(w, cmd->cell_y, -1, payload, cmd->payload_sz);
|
||||
} break;
|
||||
case 'k': {
|
||||
drag_remote_file_data(
|
||||
w, cmd->cell_x, cmd->cell_y, cmd->pixel_x, cmd->pixel_y, cmd->more != 0, payload, cmd->payload_sz);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ update_os_window_title(OSWindow *os_window) {
|
|||
static void
|
||||
destroy_window(Window *w) {
|
||||
drop_free_data(w);
|
||||
drag_free_offer(w);
|
||||
free(w->pending_clicks.clicks); zero_at_ptr(&w->pending_clicks);
|
||||
free(w->buffered_keys.key_data); zero_at_ptr(&w->buffered_keys);
|
||||
Py_CLEAR(w->render_data.screen); Py_CLEAR(w->title);
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ typedef struct Window {
|
|||
int32_t current_request_x, current_request_y, current_request_Y;
|
||||
} drop;
|
||||
struct {
|
||||
bool can_offer;
|
||||
bool can_offer, is_remote_client;
|
||||
struct { index_type x, y; bool active; } potential_url_drag;
|
||||
struct { double x, y; monotonic_t at; } initial_left_press;
|
||||
char *mimes_buf; size_t num_mimes, bufsz;
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ def client_dir_read(handle_id: int, entry_num: int | None = None, client_id: int
|
|||
|
||||
def client_drag_register(client_id: int = 0) -> bytes:
|
||||
"""Escape code a client sends to start offering drags (t=o, no payload)."""
|
||||
meta = f'{DND_CODE};t=o'
|
||||
meta = f'{DND_CODE};t=o:x=1'
|
||||
if client_id:
|
||||
meta += f':i={client_id}'
|
||||
return _osc(meta)
|
||||
|
|
@ -101,7 +101,7 @@ def client_drag_register(client_id: int = 0) -> bytes:
|
|||
|
||||
def client_drag_unregister(client_id: int = 0) -> bytes:
|
||||
"""Escape code a client sends to stop offering drags (t=O)."""
|
||||
meta = f'{DND_CODE};t=O'
|
||||
meta = f'{DND_CODE};t=o:x=2'
|
||||
if client_id:
|
||||
meta += f':i={client_id}'
|
||||
return _osc(meta)
|
||||
|
|
@ -1303,6 +1303,7 @@ class TestDnDProtocol(BaseTest):
|
|||
|
||||
def _setup_drag_offer(self, screen, wid, cap, mimes: str = 'text/plain', operations: int = 1, client_id: int = 0):
|
||||
"""Send t=o with operations and payload to set up a drag offer being built."""
|
||||
parse_bytes(screen, client_drag_register())
|
||||
parse_bytes(screen, client_drag_offer_mimes(operations, mimes, client_id=client_id))
|
||||
cap.consume(wid) # discard any output
|
||||
|
||||
|
|
@ -1320,6 +1321,7 @@ class TestDnDProtocol(BaseTest):
|
|||
def test_drag_offer_single_mime(self) -> None:
|
||||
"""Client can offer a drag with a single MIME type."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_drag_register())
|
||||
parse_bytes(screen, client_drag_offer_mimes(1, 'text/plain'))
|
||||
# No error expected – the offer is being built.
|
||||
self._assert_no_output(cap, wid)
|
||||
|
|
@ -1327,35 +1329,37 @@ class TestDnDProtocol(BaseTest):
|
|||
def test_drag_offer_multiple_mimes(self) -> None:
|
||||
"""Client can offer a drag with multiple MIME types."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_drag_register())
|
||||
parse_bytes(screen, client_drag_offer_mimes(3, 'text/plain text/uri-list application/json'))
|
||||
self._assert_no_output(cap, wid)
|
||||
|
||||
def test_drag_offer_no_operations_returns_einval(self) -> None:
|
||||
"""Offering MIME types with operations=0 (no valid operations) returns EINVAL."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_drag_register())
|
||||
# First need a valid offer to set allowed_operations, but if we pass o=0
|
||||
# directly and there's no prior offer, drag_add_mimes should abort with EINVAL.
|
||||
parse_bytes(screen, client_drag_offer_mimes(0, 'text/plain'))
|
||||
events = self._get_events(cap, wid)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'E')
|
||||
self.ae(events[0]['payload'].strip(), b'EINVAL')
|
||||
self.assert_error(cap, wid)
|
||||
|
||||
def test_drag_offer_copy_only(self) -> None:
|
||||
"""Offering with operations=1 (copy only) is accepted."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_drag_register())
|
||||
parse_bytes(screen, client_drag_offer_mimes(1, 'text/plain'))
|
||||
self._assert_no_output(cap, wid)
|
||||
|
||||
def test_drag_offer_move_only(self) -> None:
|
||||
"""Offering with operations=2 (move only) is accepted."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_drag_register())
|
||||
parse_bytes(screen, client_drag_offer_mimes(2, 'text/plain'))
|
||||
self._assert_no_output(cap, wid)
|
||||
|
||||
def test_drag_offer_copy_and_move(self) -> None:
|
||||
"""Offering with operations=3 (copy+move) is accepted."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_drag_register())
|
||||
parse_bytes(screen, client_drag_offer_mimes(3, 'text/plain text/html'))
|
||||
self._assert_no_output(cap, wid)
|
||||
|
||||
|
|
@ -1556,6 +1560,7 @@ class TestDnDProtocol(BaseTest):
|
|||
"""The client_id (i=…) set during drag offer is echoed in error replies."""
|
||||
client_id = 99
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
parse_bytes(screen, client_drag_register())
|
||||
parse_bytes(screen, client_drag_offer_mimes(1, 'text/plain', client_id=client_id))
|
||||
self._assert_no_output(cap, wid)
|
||||
# Starting the drag will fail (no real window), producing an error with client_id
|
||||
|
|
@ -1582,6 +1587,7 @@ class TestDnDProtocol(BaseTest):
|
|||
"""A large MIME list can be sent in chunks using m=1."""
|
||||
with dnd_test_window() as (osw, wid, screen, cap):
|
||||
# First chunk with m=1 (more coming)
|
||||
parse_bytes(screen, client_drag_register())
|
||||
parse_bytes(screen, client_drag_offer_mimes(1, 'text/plain ', more=True))
|
||||
self._assert_no_output(cap, wid)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue