aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/kitty/dnd_command.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/kitty/dnd_command.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/kitty/dnd_command.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/kitty/dnd_command.zig436
1 files changed, 436 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/kitty/dnd_command.zig b/vendor/libghostty-vt/src/terminal/kitty/dnd_command.zig
new file mode 100644
index 00000000..b5f63d02
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/kitty/dnd_command.zig
@@ -0,0 +1,436 @@
+const std = @import("std");
+
+/// Decoded OSC 72 metadata.
+pub const Metadata = struct {
+ /// Event type (`t`). Null when the metadata had no `t` key; such
+ /// commands parse successfully but are ignored, matching kitty.
+ type: ?EventType = null,
+
+ /// Chunking flag (`m`): true when more chunks follow.
+ more: bool = false,
+
+ /// Multiplexer client ID (`i`), echoed in every response so a
+ /// terminal multiplexer can route responses to the correct client.
+ client_id: u32 = 0,
+
+ /// Operation (`o`): meaning depends on the event type, commonly
+ /// 0=none/reject, 1=copy, 2=move, 3=copy or move.
+ operation: u32 = 0,
+
+ /// `x`, `y`, `X`, `Y` keys.
+ cell_x: i32 = 0,
+ cell_y: i32 = 0,
+ pixel_x: i32 = 0,
+ pixel_y: i32 = 0,
+
+ /// Parse raw OSC 72 metadata (the part before the first `;`).
+ /// Returns null when malformed; callers should ignore the command,
+ /// matching kitty which rejects the entire command on any error.
+ pub fn parse(raw: []const u8) ?Metadata {
+ var result: Metadata = .{};
+ var pos: usize = 0;
+ // The continue expression consumes the ':' separating a field
+ // from the next; the body advances past the field itself.
+ while (pos < raw.len) : (pos += 1) {
+ // Single-character key.
+ const key = raw[pos];
+ pos += 1;
+ switch (key) {
+ 't', 'm', 'i', 'o', 'x', 'y', 'X', 'Y' => {},
+ else => return null,
+ }
+
+ // '=' separator.
+ if (pos >= raw.len) return null;
+ if (raw[pos] != '=') return null;
+ pos += 1;
+
+ // Value.
+ if (pos >= raw.len) return null;
+ switch (key) {
+ 't' => {
+ result.type = std.enums.fromInt(
+ EventType,
+ raw[pos],
+ ) orelse return null;
+ pos += 1;
+ },
+
+ 'm', 'i', 'o' => {
+ const v = parseUnsigned(raw, &pos) orelse return null;
+ switch (key) {
+ 'm' => result.more = v != 0,
+ 'i' => result.client_id = v,
+ 'o' => result.operation = v,
+ else => unreachable,
+ }
+ },
+
+ 'x', 'y', 'X', 'Y' => {
+ const negative = raw[pos] == '-';
+ if (negative) pos += 1;
+ const unsigned = parseUnsigned(raw, &pos) orelse return null;
+ // Matches kitty's cast of the u32 magnitude to i32,
+ // which wraps rather than erroring on overflow.
+ const magnitude: i32 = @bitCast(unsigned);
+ const v = if (negative) 0 -% magnitude else magnitude;
+ switch (key) {
+ 'x' => result.cell_x = v,
+ 'y' => result.cell_y = v,
+ 'X' => result.pixel_x = v,
+ 'Y' => result.pixel_y = v,
+ else => unreachable,
+ }
+ },
+
+ else => unreachable,
+ }
+
+ // Values are separated by ':'.
+ if (pos >= raw.len) break;
+ if (raw[pos] != ':') return null;
+ }
+
+ return result;
+ }
+
+ /// Parse an unsigned decimal value at `pos`, advancing it. At most
+ /// 10 digits and at most maxInt(u32), matching kitty. Returns null
+ /// when there are no digits or the value is too large.
+ fn parseUnsigned(raw: []const u8, pos: *usize) ?u32 {
+ const start = pos.*;
+ var acc: u64 = 0;
+ var i = start;
+ while (i < raw.len and i < start + 10) : (i += 1) {
+ const d = raw[i] -% '0';
+ if (d > 9) break;
+ acc = acc * 10 + d;
+ }
+ if (i == start) return null;
+ pos.* = i;
+ return std.math.cast(u32, acc) orelse null;
+ }
+};
+
+/// The event type, i.e. values for the `t` metadata key. A single OSC 72
+/// code is used for both directions of the protocol, so most types have
+/// one meaning when received by the terminal from a client and another
+/// when sent by the terminal to a client.
+///
+/// The `drop` and `request_response` types are only ever sent by the
+/// terminal; kitty parses but ignores them when received and we do the
+/// same.
+pub const EventType = enum(u8) {
+ /// 'a': (recv) client registers to accept drops. With x=1 the payload
+ /// is the client's machine ID for remote drop support instead.
+ register = 'a',
+
+ /// 'A': (recv) client unregisters from accepting drops.
+ unregister = 'A',
+
+ /// 'm': (recv) client reports acceptance status for the drag currently
+ /// over the terminal: `o` is the chosen operation and the payload is
+ /// the accepted MIME list. (send) pointer moved over the terminal
+ /// during a drag, or with x=-1,y=-1 the drag left the window.
+ status = 'm',
+
+ /// 'M': (send only) items were dropped onto the terminal.
+ drop = 'M',
+
+ /// 'r': (recv) client requests drop data, or with x=y=Y=0 concludes
+ /// the drop with `o` as the performed operation. (send) drop data
+ /// response chunks.
+ request = 'r',
+
+ /// 'R': (send only) error response to a data request.
+ request_error = 'R',
+
+ /// 'o': (recv) drag source control: x=1 enables offering drags (payload
+ /// optionally the client machine ID), x=2 disables, x=0 offers a MIME
+ /// list for a new drag. (send) request that the client start a drag at
+ /// the given position.
+ offer = 'o',
+
+ /// 'p': (recv) pre-sent data for an offered drag: x>=0 is a 0-based
+ /// MIME index, x<0 attaches drag image -x.
+ present = 'p',
+
+ /// 'P': (recv) x=-1 starts the offered drag, x>=0 changes the drag
+ /// image mid-drag.
+ start_drag = 'P',
+
+ /// 'e': (recv) drag data for MIME index `y` of an in-progress drag.
+ /// (send) drag status events (accepted, dropped, finished, ...).
+ drag_event = 'e',
+
+ /// 'E': (recv) client aborts the whole drag (y=-1) or reports an error
+ /// for MIME index `y`. (send) drag start response (OK or error).
+ drag_error = 'E',
+
+ /// 'k': (recv) remote file data for a drag. (send) request for remote
+ /// file data.
+ remote_data = 'k',
+
+ /// 'q': (recv) query protocol support. (send) the query response.
+ query = 'q',
+};
+
+/// A drop operation. Values match the protocol's `o` key.
+pub const Operation = enum(u2) {
+ none = 0,
+ copy = 1,
+ move = 2,
+
+ /// Convert a protocol `o` value the way kitty does: anything other
+ /// than copy or move means none.
+ pub fn fromProtocol(v: u32) Operation {
+ return switch (v) {
+ 1 => .copy,
+ 2 => .move,
+ else => .none,
+ };
+ }
+};
+
+/// The set of operations allowed by a drag source, sent as a bitmask in
+/// the `o` key of move and drop events.
+pub const Operations = packed struct(u2) {
+ copy: bool = false,
+ move: bool = false,
+
+ pub fn protocolValue(self: Operations) u2 {
+ return @bitCast(self);
+ }
+};
+
+/// A decoded `t=r` data request. The request form is disambiguated by
+/// which keys are non-zero, mirroring kitty's drop_process_queue.
+pub const Request = union(enum) {
+ /// x=y=Y=0: the drop is concluded with the given operation.
+ conclude: Operation,
+
+ /// Y=0, y=0, x!=0: request data for the 1-based MIME index x.
+ mime: i32,
+
+ /// Y=0, y!=0: request the contents of the y'th (1-based) file in the
+ /// text/uri-list MIME at 1-based index x. Remote drops only.
+ uri: struct {
+ mime_idx: i32,
+ uri_idx: i32,
+ },
+
+ /// Y!=0: request entry x (1-based) of directory handle Y, or close
+ /// the handle when x=0. Remote drops only.
+ dir: struct {
+ handle: i32,
+ entry: i32,
+ },
+
+ pub fn init(meta: Metadata) Request {
+ if (meta.pixel_y != 0) return .{ .dir = .{
+ .handle = meta.pixel_y,
+ .entry = meta.cell_x,
+ } };
+ if (meta.cell_y != 0) return .{ .uri = .{
+ .mime_idx = meta.cell_x,
+ .uri_idx = meta.cell_y,
+ } };
+ if (meta.cell_x != 0) return .{ .mime = meta.cell_x };
+ return .{ .conclude = .fromProtocol(meta.operation) };
+ }
+};
+
+/// Chunk reassembly state.
+///
+/// While a chunked command is in progress, the metadata of the first
+/// chunk is reused for all subsequent chunks; only the `more` flag is
+/// taken from each continuation.
+pub const Chunking = struct {
+ active: bool = false,
+ metadata: Metadata = .{},
+
+ /// Returns the effective metadata for a received chunk and updates
+ /// the reassembly state.
+ pub fn apply(self: *Chunking, meta: Metadata) Metadata {
+ if (self.active) {
+ var copy = self.metadata;
+ copy.more = meta.more;
+ self.active = meta.more;
+ return copy;
+ }
+
+ if (meta.more) {
+ self.active = true;
+ self.metadata = meta;
+ }
+
+ return meta;
+ }
+};
+
+test "Metadata: empty" {
+ const testing = std.testing;
+ const meta = Metadata.parse("").?;
+ try testing.expect(meta.type == null);
+ try testing.expect(!meta.more);
+ try testing.expectEqual(@as(u32, 0), meta.client_id);
+}
+
+test "Metadata: all keys" {
+ const testing = std.testing;
+ const meta = Metadata.parse("t=m:m=1:i=3:o=2:x=10:y=5:X=320:Y=200").?;
+ try testing.expectEqual(EventType.status, meta.type.?);
+ try testing.expect(meta.more);
+ try testing.expectEqual(@as(u32, 3), meta.client_id);
+ try testing.expectEqual(@as(u32, 2), meta.operation);
+ try testing.expectEqual(@as(i32, 10), meta.cell_x);
+ try testing.expectEqual(@as(i32, 5), meta.cell_y);
+ try testing.expectEqual(@as(i32, 320), meta.pixel_x);
+ try testing.expectEqual(@as(i32, 200), meta.pixel_y);
+}
+
+test "Metadata: all event types" {
+ const testing = std.testing;
+ const cases = .{
+ .{ "t=a", EventType.register },
+ .{ "t=A", EventType.unregister },
+ .{ "t=m", EventType.status },
+ .{ "t=M", EventType.drop },
+ .{ "t=r", EventType.request },
+ .{ "t=R", EventType.request_error },
+ .{ "t=o", EventType.offer },
+ .{ "t=p", EventType.present },
+ .{ "t=P", EventType.start_drag },
+ .{ "t=e", EventType.drag_event },
+ .{ "t=E", EventType.drag_error },
+ .{ "t=k", EventType.remote_data },
+ .{ "t=q", EventType.query },
+ };
+ inline for (cases) |case| {
+ try testing.expectEqual(case[1], Metadata.parse(case[0]).?.type.?);
+ }
+}
+
+test "Metadata: case-sensitive coordinate keys" {
+ const testing = std.testing;
+ const meta = Metadata.parse("x=10:Y=200").?;
+ try testing.expectEqual(@as(i32, 10), meta.cell_x);
+ try testing.expectEqual(@as(i32, 0), meta.cell_y);
+ try testing.expectEqual(@as(i32, 0), meta.pixel_x);
+ try testing.expectEqual(@as(i32, 200), meta.pixel_y);
+}
+
+test "Metadata: negative coordinates" {
+ const testing = std.testing;
+ const meta = Metadata.parse("t=m:x=-1:y=-1").?;
+ try testing.expectEqual(@as(i32, -1), meta.cell_x);
+ try testing.expectEqual(@as(i32, -1), meta.cell_y);
+}
+
+test "Metadata: malformed inputs rejected" {
+ const testing = std.testing;
+ // Unknown event type.
+ try testing.expect(Metadata.parse("t=z") == null);
+ // Unknown key.
+ try testing.expect(Metadata.parse("z=1") == null);
+ // Missing '=' mid-stream.
+ try testing.expect(Metadata.parse("x10") == null);
+ // No digits.
+ try testing.expect(Metadata.parse("x=notanumber") == null);
+ try testing.expect(Metadata.parse("x=-") == null);
+ // Too large.
+ try testing.expect(Metadata.parse("i=4294967296") == null);
+ try testing.expect(Metadata.parse("i=99999999999") == null);
+ // Garbage after value.
+ try testing.expect(Metadata.parse("x=1z") == null);
+ // No whitespace tolerance, matching kitty.
+ try testing.expect(Metadata.parse("t=a: x=1") == null);
+ try testing.expect(Metadata.parse("t = a") == null);
+ // Truncated mid-construct, matching kitty's final-state check.
+ // Nothing state-changing may be parsed out of these: e.g. "t=r:x="
+ // must not be treated as a drop conclusion.
+ try testing.expect(Metadata.parse("t") == null);
+ try testing.expect(Metadata.parse("t=") == null);
+ try testing.expect(Metadata.parse("x=") == null);
+ try testing.expect(Metadata.parse("t=r:x=") == null);
+ try testing.expect(Metadata.parse("x=1:t=") == null);
+}
+
+test "Metadata: trailing separator accepted" {
+ const testing = std.testing;
+
+ // Kitty's state machine accepts the metadata ending right after a
+ // value separator.
+ const meta = Metadata.parse("t=a:").?;
+ try testing.expectEqual(EventType.register, meta.type.?);
+}
+
+test "Metadata: u32 boundary accepted" {
+ const testing = std.testing;
+ const meta = Metadata.parse("i=4294967295").?;
+ try testing.expectEqual(@as(u32, std.math.maxInt(u32)), meta.client_id);
+}
+
+test "Request: classification" {
+ const testing = std.testing;
+
+ // Conclude.
+ {
+ const r: Request = .init(Metadata.parse("t=r:o=2").?);
+ try testing.expectEqual(Operation.move, r.conclude);
+ }
+ // Conclude with unknown operation is none (canceled).
+ {
+ const r: Request = .init(Metadata.parse("t=r:o=9").?);
+ try testing.expectEqual(Operation.none, r.conclude);
+ }
+ // MIME data request.
+ {
+ const r: Request = .init(Metadata.parse("t=r:x=2").?);
+ try testing.expectEqual(@as(i32, 2), r.mime);
+ }
+ // URI file request.
+ {
+ const r: Request = .init(Metadata.parse("t=r:x=1:y=3").?);
+ try testing.expectEqual(@as(i32, 1), r.uri.mime_idx);
+ try testing.expectEqual(@as(i32, 3), r.uri.uri_idx);
+ }
+ // Directory handle request.
+ {
+ const r: Request = .init(Metadata.parse("t=r:Y=2:x=1").?);
+ try testing.expectEqual(@as(i32, 2), r.dir.handle);
+ try testing.expectEqual(@as(i32, 1), r.dir.entry);
+ }
+}
+
+test "Chunking: reassembly reuses first chunk metadata" {
+ const testing = std.testing;
+ var chunking: Chunking = .{};
+
+ // First chunk starts reassembly.
+ const first = chunking.apply(Metadata.parse("t=m:o=1:m=1").?);
+ try testing.expect(chunking.active);
+ try testing.expectEqual(EventType.status, first.type.?);
+ try testing.expect(first.more);
+
+ // Continuation metadata is ignored except for `more`.
+ const second = chunking.apply(Metadata.parse("t=q:o=2:m=1").?);
+ try testing.expect(chunking.active);
+ try testing.expectEqual(EventType.status, second.type.?);
+ try testing.expectEqual(@as(u32, 1), second.operation);
+ try testing.expect(second.more);
+
+ // Final chunk ends reassembly.
+ const last = chunking.apply(Metadata.parse("t=q:m=0").?);
+ try testing.expect(!chunking.active);
+ try testing.expectEqual(EventType.status, last.type.?);
+ try testing.expect(!last.more);
+}
+
+test "Chunking: unchunked commands pass through" {
+ const testing = std.testing;
+ var chunking: Chunking = .{};
+ const meta = chunking.apply(Metadata.parse("t=q").?);
+ try testing.expect(!chunking.active);
+ try testing.expectEqual(EventType.query, meta.type.?);
+}