Skip to content

Commit df20d4a

Browse files
committed
Add machine id and stub for t=k transfers
1 parent cfa9f1b commit df20d4a

9 files changed

Lines changed: 97 additions & 38 deletions

File tree

docs/dnd-protocol.rst

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,12 @@ Terminal programs can inform the terminal emulator that they
232232
are willing to act as a source of drag data by sending the
233233
sending the escape code::
234234

235-
OSC _dnd_code ; t=o ST
235+
OSC _dnd_code ; t=o:x=1 ; optional machine id ST
236236

237237
On exit, or if the program no longer is willing to start drag gestures, it must
238-
send ``t=O`` to the terminal to indicate it no longer wants to offer drag data.
238+
send ``t=o:x=2`` to the terminal to indicate it no longer wants to offer drag data.
239+
The ``machine id`` is optional and is used to enable dragging from remote
240+
machines. See :ref:`below <machine_id>` for its semantics.
239241

240242
When the user performs the platform specific gesture to start a drag operation,
241243
the terminal will send the same escape code back to the terminal program
@@ -356,7 +358,9 @@ Dragging to remote machines
356358
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
357359

358360
To support dragging files to remote machines, when requesting the data for the
359-
``text/uri-list`` MIME type, terminal emulators can add the ``Y=1`` key. On
361+
``text/uri-list`` MIME type, terminal emulators can add the ``Y=1`` key.
362+
Terminals can examine the :ref:`machine_id` sent with the enable drag offers
363+
``t=o`` escape code to decide whether to use ``Y=1`` or not. On
360364
receipt of this key, the client should first send the ``text/uri-list`` as
361365
normal and then a series of responses for every ``file://`` URL type in the
362366
list of the form::
@@ -366,12 +370,12 @@ list of the form::
366370
OSC _dnd_code ; t=k:x=idx:X=handle:m=0 or 1 ; base64 encoded null separated list of directory entries ST
367371

368372
These represent possibly chunked data for files, symlinks and directories, as
369-
denoted by the ``X`` key. As always, end of data is indicated by an escape code
370-
with ``m=0`` and no payload. ``idx`` is the one based index into the list of
371-
entries in the ``text/uri-list`` MIME type. ``file://`` URLs that point to
372-
symlinks must be resolved to files or directories and sent. So actual symlinks
373-
will appear only when recursing through directories as described below. Only
374-
regular files should be sent.
373+
denoted by the ``X`` key. As above, end of data for an individual entry is
374+
indicated by an escape code with ``m=0`` and no payload. ``idx`` is the one
375+
based index into the list of entries in the ``text/uri-list`` MIME type.
376+
``file://`` URLs that point to symlinks must be resolved to files or
377+
directories and sent. So actual symlinks will appear only when recursing
378+
through directories as described below. Only regular files should be sent.
375379

376380
Terminals should write the transmitted data into a temporary directory
377381
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
384388
in breadth first order. The children of a directory are reported by
385389
adding ``Y=parent-handle:y=num`` to the escape codes above. Here
386390
``parent-handle`` is the handle of the directory being traversed and ``num``
387-
is the one based index into the list of entries in the directory.
391+
is the one based index into the list of entries in the directory. Thus, the
392+
set of keys ``x, y, Y`` uniquely determine an entry.
388393

389394
Once all data is transmitted, the client informs the terminal emulator of
390395
completion with::
391396

392397
OSC _dnd_code ; t=k ; ST
393398

399+
At this point, the terminal should send the modified data for ``text/uri-list``
400+
to the drop destination.
401+
394402
If any error occurs in the client while reading the data, it can inform
395403
the terminal using::
396404

@@ -432,8 +440,7 @@ Key Value Default Description
432440
``M`` - a drop dropped event
433441
``r`` - request dropped data
434442
``R`` - report an error
435-
``o`` - start offering drags
436-
``O`` - stop offering drags
443+
``o`` - start offering drags or start a drag
437444
``p`` - present data for drag offers
438445
``P`` - Change drag image or start drag
439446
``e`` - a drag offer event occurred
@@ -460,4 +467,16 @@ Key Value Default Description
460467
======= ==================== ========= =================
461468

462469

470+
.. _machine_id:
471+
472+
Machine id
473+
-----------------
463474

475+
The machine id is used to detect when a drag is started on a remote machine. It
476+
is of the form: ``version:ASCII printable chars``. The leading ``version`` field
477+
allows for changing the format or semantics of this field in the future. The
478+
actual id is the machine id (the contents of :file:`/etc/machine-id` on
479+
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>`
480+
with :rfc:`SHA-256 <6234>` as the digest algorithm and the key being the ASCII bytes:
481+
``tty-dnd-protocol-machine-id``. The hashing is done so as to not easily leak the
482+
actual machine id and to ensure that the value is of fixed size.

gen/apc_parsers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def parsers() -> None:
333333
write_header(text, 'kitty/parse-multicell-command.h')
334334

335335
keymap = {
336-
't': ('type', flag('aAmMrRoOpPeEk')),
336+
't': ('type', flag('aAmMrRopPeEk')),
337337
'm': ('more', 'uint'),
338338
'i': ('client_id', 'uint'),
339339
'o': ('operation', 'uint'),

kitty/dnd.c

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,7 @@ drag_free_built_data(Window *w) {
10671067
free(ds.items);
10681068
ds.items = NULL;
10691069
}
1070+
ds.num_mimes = 0;
10701071
for (size_t i = 0; i < arraysz(ds.images); i++) {
10711072
if (ds.images[i].data) free(ds.images[i].data);
10721073
zero_at_ptr(ds.images + i);
@@ -1075,11 +1076,10 @@ drag_free_built_data(Window *w) {
10751076

10761077
void
10771078
drag_free_offer(Window *w) {
1078-
free(ds.mimes_buf); ds.mimes_buf = NULL;
1079+
free(ds.mimes_buf); ds.mimes_buf = NULL; ds.bufsz = 0;
10791080
drag_free_built_data(w);
10801081
ds.allowed_operations = 0;
10811082
ds.state = DRAG_SOURCE_NONE;
1082-
ds.num_mimes = 0;
10831083
ds.pre_sent_total_sz = 0;
10841084
ds.images_sent_total_sz = 0;
10851085
}
@@ -1100,12 +1100,31 @@ cancel_drag(Window *w, int error_code) {
11001100
drag_free_offer(w);
11011101
}
11021102

1103+
#define abrt(code) { cancel_drag(w, code); return; }
1104+
1105+
void
1106+
drag_start_offerring(Window *w, const char *client_machine_id, size_t sz) {
1107+
ds.can_offer = true; ds.is_remote_client = false;
1108+
if (sz && client_machine_id) {
1109+
const char *host_machine_id = machine_id();
1110+
if (host_machine_id) {
1111+
size_t hsz = strlen(host_machine_id);
1112+
if (hsz != sz || memcmp(host_machine_id, client_machine_id, sz) != 0) ds.is_remote_client = true;
1113+
}
1114+
}
1115+
}
1116+
1117+
void
1118+
drag_stop_offerring(Window *w) {
1119+
drag_free_offer(w);
1120+
ds.can_offer = false; ds.is_remote_client = false;
1121+
}
1122+
11031123
void
11041124
drag_add_mimes(Window *w, int allowed_operations, uint32_t client_id, const char *data, size_t sz, bool has_more) {
1105-
#define abrt(code) { cancel_drag(w, code); return; }
1106-
if (allowed_operations && ds.state != DRAG_SOURCE_NONE) cancel_drag(w, 0);
1125+
if (!ds.can_offer) abrt(EINVAL);
11071126
if (allowed_operations && !ds.allowed_operations) ds.allowed_operations = allowed_operations;
1108-
if (!ds.allowed_operations) { abrt(EINVAL); }
1127+
if (!ds.allowed_operations || ds.state > DRAG_SOURCE_BEING_BUILT) abrt(EINVAL);
11091128
ds.state = DRAG_SOURCE_BEING_BUILT;
11101129
ds.client_id = client_id;
11111130
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) {
13481367
}
13491368
// No fd yet, request data from the client
13501369
char buf[128];
1351-
int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu", DND_CODE, DRAG_NOTIFY_FINISHED + 2, i);
1370+
int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu:Y=%d",
1371+
DND_CODE, DRAG_NOTIFY_FINISHED + 2, i, ds.is_remote_client);
13521372
queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false);
13531373
*err_code = EAGAIN;
13541374
return NULL;
@@ -1449,6 +1469,14 @@ drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *paylo
14491469
if (ret) cancel_drag(w, ret);
14501470
}
14511471
}
1472+
1473+
void
1474+
drag_remote_file_data(
1475+
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
1476+
) {
1477+
(void)w; (void)x; (void)y; (void)X; (void)Y; (void)has_more; (void)payload; (void)payload_sz;
1478+
// TODO: Implement this
1479+
}
14521480
#undef img
14531481
#undef abrt
14541482
#undef ds

kitty/dnd.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ void drag_notify(Window *w, DragNotifyType type);
3535
int drag_free_data(Window *w, const char *mime_type, const char* data, size_t sz);
3636
const char* drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code);
3737
void drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *payload, size_t payload_sz);
38+
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);
39+
void drag_start_offerring(Window *w, const char *client_machine_id, size_t sz);
40+
void drag_stop_offerring(Window *w);

kitty/parse-dnd-command.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,9 @@ static inline void parse_dnd_code(PS *self, uint8_t *parser_buf,
8686

8787
case type: {
8888
g.type = parser_buf[pos++];
89-
if (g.type != 'A' && g.type != 'E' && g.type != 'M' && g.type != 'O' &&
90-
g.type != 'P' && g.type != 'R' && g.type != 'a' && g.type != 'e' &&
91-
g.type != 'k' && g.type != 'm' && g.type != 'o' && g.type != 'p' &&
92-
g.type != 'r') {
89+
if (g.type != 'A' && g.type != 'E' && g.type != 'M' && g.type != 'P' &&
90+
g.type != 'R' && g.type != 'a' && g.type != 'e' && g.type != 'k' &&
91+
g.type != 'm' && g.type != 'o' && g.type != 'p' && g.type != 'r') {
9392
REPORT_ERROR("Malformed DnDCommand control block, unknown flag value "
9493
"for type: 0x%x",
9594
g.type);

kitty/screen.c

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,14 +1535,13 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa
15351535
drop_enqueue_request(w, cmd->cell_x, cmd->cell_y, cmd->pixel_y);
15361536
} break;
15371537
case 'o': {
1538-
if (cmd->payload_sz > 0) drag_add_mimes(w, (int)cmd->operation, cmd->client_id, (const char*)payload, cmd->payload_sz, cmd->more);
1539-
else w->drag_source.can_offer = true;
1540-
} break;
1541-
case 'O': {
1542-
drag_free_offer(w);
1543-
w->drag_source.can_offer = false;
1544-
if (global_state.drag_source.is_active && global_state.drag_source.from_window == w->id) {
1545-
cancel_current_drag_source();
1538+
switch (cmd->cell_x) {
1539+
case 1: drag_start_offerring(w, (const char*)payload, cmd->payload_sz); break;
1540+
case 2: drag_stop_offerring(w); break;
1541+
case 0:
1542+
drag_add_mimes(
1543+
w, (int)cmd->operation, cmd->client_id, (const char*)payload, cmd->payload_sz, cmd->more);
1544+
break;
15461545
}
15471546
} break;
15481547
case 'p': {
@@ -1564,6 +1563,10 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa
15641563
}
15651564
} else drag_process_item_data(w, cmd->cell_y, -1, payload, cmd->payload_sz);
15661565
} break;
1566+
case 'k': {
1567+
drag_remote_file_data(
1568+
w, cmd->cell_x, cmd->cell_y, cmd->pixel_x, cmd->pixel_y, cmd->more != 0, payload, cmd->payload_sz);
1569+
} break;
15671570
}
15681571
}
15691572

kitty/state.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ update_os_window_title(OSWindow *os_window) {
353353
static void
354354
destroy_window(Window *w) {
355355
drop_free_data(w);
356+
drag_free_offer(w);
356357
free(w->pending_clicks.clicks); zero_at_ptr(&w->pending_clicks);
357358
free(w->buffered_keys.key_data); zero_at_ptr(&w->buffered_keys);
358359
Py_CLEAR(w->render_data.screen); Py_CLEAR(w->title);

kitty/state.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ typedef struct Window {
296296
int32_t current_request_x, current_request_y, current_request_Y;
297297
} drop;
298298
struct {
299-
bool can_offer;
299+
bool can_offer, is_remote_client;
300300
struct { index_type x, y; bool active; } potential_url_drag;
301301
struct { double x, y; monotonic_t at; } initial_left_press;
302302
char *mimes_buf; size_t num_mimes, bufsz;

kitty_tests/dnd.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,15 @@ def client_dir_read(handle_id: int, entry_num: int | None = None, client_id: int
9393

9494
def client_drag_register(client_id: int = 0) -> bytes:
9595
"""Escape code a client sends to start offering drags (t=o, no payload)."""
96-
meta = f'{DND_CODE};t=o'
96+
meta = f'{DND_CODE};t=o:x=1'
9797
if client_id:
9898
meta += f':i={client_id}'
9999
return _osc(meta)
100100

101101

102102
def client_drag_unregister(client_id: int = 0) -> bytes:
103103
"""Escape code a client sends to stop offering drags (t=O)."""
104-
meta = f'{DND_CODE};t=O'
104+
meta = f'{DND_CODE};t=o:x=2'
105105
if client_id:
106106
meta += f':i={client_id}'
107107
return _osc(meta)
@@ -1303,6 +1303,7 @@ def test_window_close_during_transfer_no_leak(self) -> None:
13031303

13041304
def _setup_drag_offer(self, screen, wid, cap, mimes: str = 'text/plain', operations: int = 1, client_id: int = 0):
13051305
"""Send t=o with operations and payload to set up a drag offer being built."""
1306+
parse_bytes(screen, client_drag_register())
13061307
parse_bytes(screen, client_drag_offer_mimes(operations, mimes, client_id=client_id))
13071308
cap.consume(wid) # discard any output
13081309

@@ -1320,42 +1321,45 @@ def test_drag_register_and_unregister(self) -> None:
13201321
def test_drag_offer_single_mime(self) -> None:
13211322
"""Client can offer a drag with a single MIME type."""
13221323
with dnd_test_window() as (osw, wid, screen, cap):
1324+
parse_bytes(screen, client_drag_register())
13231325
parse_bytes(screen, client_drag_offer_mimes(1, 'text/plain'))
13241326
# No error expected – the offer is being built.
13251327
self._assert_no_output(cap, wid)
13261328

13271329
def test_drag_offer_multiple_mimes(self) -> None:
13281330
"""Client can offer a drag with multiple MIME types."""
13291331
with dnd_test_window() as (osw, wid, screen, cap):
1332+
parse_bytes(screen, client_drag_register())
13301333
parse_bytes(screen, client_drag_offer_mimes(3, 'text/plain text/uri-list application/json'))
13311334
self._assert_no_output(cap, wid)
13321335

13331336
def test_drag_offer_no_operations_returns_einval(self) -> None:
13341337
"""Offering MIME types with operations=0 (no valid operations) returns EINVAL."""
13351338
with dnd_test_window() as (osw, wid, screen, cap):
1339+
parse_bytes(screen, client_drag_register())
13361340
# First need a valid offer to set allowed_operations, but if we pass o=0
13371341
# directly and there's no prior offer, drag_add_mimes should abort with EINVAL.
13381342
parse_bytes(screen, client_drag_offer_mimes(0, 'text/plain'))
1339-
events = self._get_events(cap, wid)
1340-
self.assertEqual(len(events), 1, events)
1341-
self.ae(events[0]['type'], 'E')
1342-
self.ae(events[0]['payload'].strip(), b'EINVAL')
1343+
self.assert_error(cap, wid)
13431344

13441345
def test_drag_offer_copy_only(self) -> None:
13451346
"""Offering with operations=1 (copy only) is accepted."""
13461347
with dnd_test_window() as (osw, wid, screen, cap):
1348+
parse_bytes(screen, client_drag_register())
13471349
parse_bytes(screen, client_drag_offer_mimes(1, 'text/plain'))
13481350
self._assert_no_output(cap, wid)
13491351

13501352
def test_drag_offer_move_only(self) -> None:
13511353
"""Offering with operations=2 (move only) is accepted."""
13521354
with dnd_test_window() as (osw, wid, screen, cap):
1355+
parse_bytes(screen, client_drag_register())
13531356
parse_bytes(screen, client_drag_offer_mimes(2, 'text/plain'))
13541357
self._assert_no_output(cap, wid)
13551358

13561359
def test_drag_offer_copy_and_move(self) -> None:
13571360
"""Offering with operations=3 (copy+move) is accepted."""
13581361
with dnd_test_window() as (osw, wid, screen, cap):
1362+
parse_bytes(screen, client_drag_register())
13591363
parse_bytes(screen, client_drag_offer_mimes(3, 'text/plain text/html'))
13601364
self._assert_no_output(cap, wid)
13611365

@@ -1556,6 +1560,7 @@ def test_drag_client_id_propagated(self) -> None:
15561560
"""The client_id (i=…) set during drag offer is echoed in error replies."""
15571561
client_id = 99
15581562
with dnd_test_window() as (osw, wid, screen, cap):
1563+
parse_bytes(screen, client_drag_register())
15591564
parse_bytes(screen, client_drag_offer_mimes(1, 'text/plain', client_id=client_id))
15601565
self._assert_no_output(cap, wid)
15611566
# Starting the drag will fail (no real window), producing an error with client_id
@@ -1582,6 +1587,7 @@ def test_drag_chunked_mime_offer(self) -> None:
15821587
"""A large MIME list can be sent in chunks using m=1."""
15831588
with dnd_test_window() as (osw, wid, screen, cap):
15841589
# First chunk with m=1 (more coming)
1590+
parse_bytes(screen, client_drag_register())
15851591
parse_bytes(screen, client_drag_offer_mimes(1, 'text/plain ', more=True))
15861592
self._assert_no_output(cap, wid)
15871593

0 commit comments

Comments
 (0)