Harden input ordering, portal activation and portable Linux builds

This commit is contained in:
Peterino2 2026-09-18 14:12:08 -07:00
parent 5623376284
commit c1c2de8c4b
9 changed files with 218 additions and 36 deletions

View File

@ -323,10 +323,6 @@ pub fn forget(w: *native.Window) void {
}
}
}
pub fn focusLost() void {
if (s().selection) |offer_| unref(offer_);
s().selection = null;
}
pub fn releaseSeat() void {
while (s().transfers) |t| finish(t, abi.CANCELLED);
s().selection = null;

View File

@ -9,6 +9,15 @@ const Keyboard = @import("Keyboard.zig");
const Data = @import("Data.zig");
const Text = @import("Text.zig");
const Key = struct {
code: u32,
down: bool,
repeated: bool,
sym: u32,
modifiers: u32,
text: [256]u8 = undefined,
len: usize = 0,
};
pub const State = struct {
seat: ?*c.struct_wl_seat = null,
seat_name: u32 = 0,
@ -29,6 +38,8 @@ pub const State = struct {
compose: ?*c.struct_xkb_compose_state = null,
modifiers: u32 = 0,
active: bool = false,
pending_key: ?Key = null,
key_sync: ?*c.struct_wl_callback = null,
serial: u32 = 0,
serial_time: u64 = 0,
pointer_serial: u32 = 0,
@ -137,6 +148,7 @@ fn seatCapabilities(_: ?*anyopaque, seat: ?*c.struct_wl_seat, caps: u32) callcon
fn seatName(_: ?*anyopaque, _: ?*c.struct_wl_seat, _: [*c]const u8) callconv(.c) void {}
const seat_listener: c.struct_wl_seat_listener = .{ .capabilities = seatCapabilities, .name = seatName };
fn keymap(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, format: u32, fd: i32, size: u32) callconv(.c) void {
flushKey();
defer _ = c.close(fd);
const s = state();
s.repeat_key = null;
@ -182,31 +194,66 @@ fn modifiers(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, _: u32, depressed: u32,
const s = state();
if (s.state != null) _ = c.z_xkb_state_update_mask.?(s.state, depressed, latched, locked, 0, 0, group);
updateModifiers();
flushKey();
}
fn key(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, serial: u32, _: u32, code: u32, pressed: u32) callconv(.c) void {
const s = state();
flushKey();
if (code > 0xffff) return;
provenance(serial);
if (code < s.held.len) s.held[code] = pressed != 0;
// The compositor supplies the authoritative modifier/group mask. Running
// XKB key actions locally as well would apply latch/lock actions twice.
deliverKey(code, pressed != 0, false);
s.pending_key = prepareKey(code, pressed != 0, false);
// wl_keyboard has no frame event. A sync boundary lets the following
// authoritative modifiers event supply the public post-event flags even
// when the key and modifier messages arrive in separate socket reads.
if (s.key_sync == null) {
s.key_sync = c.wl_display_sync(b().display);
if (s.key_sync != null) _ = c.wl_callback_add_listener(s.key_sync, &key_sync_listener, null) else flushKey();
}
if (pressed != 0 and s.repeat_rate > 0 and s.keymap != null and c.z_xkb_keymap_key_repeats.?(s.keymap, code + 8) != 0) {
s.repeat_key = code;
s.repeat_at = runtime.now(b().app.io) + @as(u64, s.repeat_delay) * std.time.ns_per_ms;
} else if (s.repeat_key == code) s.repeat_key = null;
}
fn prepareKey(code: u32, down: bool, repeated: bool) Key {
const s = state();
var value: Key = .{ .code = code, .down = down, .repeated = repeated, .sym = if (s.state != null) c.z_xkb_state_key_get_one_sym.?(s.state, code + 8) else 0, .modifiers = s.modifiers };
if (down and s.state != null) {
const length = c.z_xkb_state_key_get_utf8.?(s.state, code + 8, &value.text, value.text.len);
if (length > 0 and length < value.text.len) value.len = @intCast(length);
}
return value;
}
fn deliverKey(code: u32, down: bool, repeated: bool) void {
emitKey(prepareKey(code, down, repeated));
}
fn emitKey(value: Key) void {
const s = state();
const w = s.key_focus orelse return;
const sym = if (s.state != null) c.z_xkb_state_key_get_one_sym.?(s.state, code + 8) else 0;
var event = std.mem.zeroes(n.z_event);
event.type = n.UHDK_EVENT_KEY;
event.input_id = id();
event.data.key = .{ .physical_key = Keyboard.physical(code), .logical_key = if (s.state != null) Keyboard.logical(sym) else 0, .modifiers = s.modifiers, .pressed = @intFromBool(down), .repeat = @intFromBool(repeated) };
event.data.key = .{ .physical_key = Keyboard.physical(value.code), .logical_key = if (s.state != null) Keyboard.logical(value.sym) else 0, .modifiers = s.modifiers, .pressed = @intFromBool(value.down), .repeat = @intFromBool(value.repeated) };
send(w, &event);
if (down and w.text.enabled) Text.key(w, code, sym, event.data.key.logical_key, event.input_id);
// Resolve symbols/text before consuming a latched modifier, while reporting
// the compositor's post-event flags in the observational raw event.
if (value.down and w.text.enabled) Text.key(w, value.sym, event.data.key.logical_key, event.input_id, value.modifiers, value.text[0..value.len]);
}
pub fn flushKey() void {
if (state().pending_key) |value| {
state().pending_key = null;
emitKey(value);
}
}
fn keySynced(_: ?*anyopaque, callback: ?*c.struct_wl_callback, _: u32) callconv(.c) void {
c.wl_callback_destroy(callback);
state().key_sync = null;
flushKey();
}
const key_sync_listener: c.struct_wl_callback_listener = .{ .done = keySynced };
fn repeatInfo(_: ?*anyopaque, _: ?*c.struct_wl_keyboard, rate: i32, delay: i32) callconv(.c) void {
state().repeat_rate = @intCast(std.math.clamp(rate, 0, 1000));
state().repeat_delay = @intCast(@max(0, delay));
@ -216,7 +263,6 @@ const keyboard_listener: c.struct_wl_keyboard_listener = .{ .keymap = keymap, .e
pub fn tick() i32 {
const s = state();
const active = s.key_focus != null;
if (!active) Data.focusLost();
if (active != s.active) {
s.active = active;
var event = std.mem.zeroes(n.z_event);
@ -224,6 +270,7 @@ pub fn tick() i32 {
event.data.active = @intFromBool(active);
n.z_emit(runtime.toAppHandle(b().app), &event);
}
if (s.pending_key != null) return -1;
if (s.repeat_key) |code| {
const now = runtime.now(b().app.io);
if (now >= s.repeat_at) {
@ -235,7 +282,9 @@ pub fn tick() i32 {
return -1;
}
fn leaveKeyboard() void {
flushKey();
const s = state();
s.modifiers = 0;
s.repeat_key = null;
if (s.key_focus) |w| {
for (&s.held, 0..) |*held, code| if (held.*) {
@ -402,6 +451,8 @@ pub fn remove(name: u32) void {
fn releaseSeat() void {
Data.releaseSeat();
const s = state();
if (s.key_sync != null) c.wl_callback_destroy(s.key_sync);
s.key_sync = null;
leaveKeyboard();
leavePointer();
if (s.text_focus) |w| Text.forget(w);

View File

@ -6,6 +6,7 @@ const n = @import("native");
const native = @import("native.zig");
const Bus = @import("Bus.zig");
const Files = @import("Files.zig");
const Input = @import("Input.zig");
const runtime = @import("../runtime.zig");
const abi = @import("../abi.zig");
const desktop = "org.freedesktop.portal.Desktop";
@ -24,6 +25,7 @@ const Pending = struct {
handle: [:0]u8,
token: [:0]u8,
exported: ?*c.struct_zxdg_exported_v2 = null,
activation: ?*c.struct_xdg_activation_token_v1 = null,
deadline: u64,
};
pub const State = struct {
@ -309,7 +311,18 @@ pub fn request(cmd: *const n.z_command, id: u64) u32 {
return abi.OOM;
};
_ = c.zxdg_exported_v2_add_listener(p.exported, &export_listener, p);
} else submit(p, "") catch {
} else if (cmd.op == n.Z_URL and b().desktop.activation != null and Input.state().key_focus != null and
Input.state().serial != 0 and runtime.now(b().app.io) -| Input.state().serial_time <= 5 * std.time.ns_per_s)
{
p.activation = c.xdg_activation_v1_get_activation_token(b().desktop.activation) orelse {
destroy(p);
return abi.OOM;
};
_ = c.xdg_activation_token_v1_add_listener(p.activation, &activation_listener, p);
c.xdg_activation_token_v1_set_serial(p.activation, Input.state().serial, Input.state().seat);
c.xdg_activation_token_v1_set_surface(p.activation, Input.state().key_focus.?.surface);
c.xdg_activation_token_v1_commit(p.activation);
} else submit(p, "", null) catch {
destroy(p);
return abi.OOM;
};
@ -336,13 +349,21 @@ fn exported(raw: ?*anyopaque, _: ?*c.struct_zxdg_exported_v2, handle: [*c]const
return;
};
defer b().app.gpa.free(parent);
submit(p, parent) catch {
submit(p, parent, null) catch {
complete(p, abi.OOM, null);
destroy(p);
};
}
const export_listener: c.struct_zxdg_exported_v2_listener = .{ .handle = exported };
fn submit(p: *Pending, parent: [*:0]const u8) !void {
fn activated(raw: ?*anyopaque, _: ?*c.struct_xdg_activation_token_v1, token: [*c]const u8) callconv(.c) void {
const p: *Pending = @ptrCast(@alignCast(raw.?));
submit(p, "", token) catch {
complete(p, abi.OOM, null);
destroy(p);
};
}
const activation_listener: c.struct_xdg_activation_token_v1_listener = .{ .done = activated };
fn submit(p: *Pending, parent: [*:0]const u8, activation_token: ?[*:0]const u8) !void {
const cmd = p.command.?;
const dialog = cmd.op == n.Z_DIALOG;
const message = try Bus.method(desktop, path, if (dialog) chooser else "org.freedesktop.portal.OpenURI", if (!dialog) "OpenURI" else if (cmd.dialog.kind == n.UHDK_DIALOG_SAVE_FILE) "SaveFile" else "OpenFile");
@ -356,6 +377,7 @@ fn submit(p: *Pending, parent: [*:0]const u8) !void {
var options = try Bus.open(&args, c.DBUS_TYPE_ARRAY, "{sv}");
errdefer c.z_dbus_message_iter_abandon_container_if_open.?(&args, &options);
try Bus.option(&options, "handle_token", "s", p.token.ptr);
if (activation_token) |token| try Bus.option(&options, "activation_token", "s", token);
if (dialog) {
try Bus.option(&options, "modal", "b", @as(u32, @intFromBool(cmd.dialog.parent != null)));
if (cmd.dialog.kind != n.UHDK_DIALOG_SAVE_FILE) {
@ -485,6 +507,7 @@ fn destroy(p: *Pending) void {
while (link.* != p) link = &link.*.?.next;
link.* = p.next;
if (p.exported != null) c.zxdg_exported_v2_destroy(p.exported);
if (p.activation != null) c.xdg_activation_token_v1_destroy(p.activation);
b().app.gpa.free(p.handle);
b().app.gpa.free(p.token);
b().app.gpa.destroy(p);

View File

@ -114,7 +114,10 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
- One active seat owns keyboard, pointer, and clipboard state. The keyboard uses
the compositor's XKB layout/masks, USB HID physical usages, client-side repeat,
and locale-aware compose tables. Focus loss releases tracked keys and stops repeat.
and locale-aware compose tables. Raw events use the post-event modifier mask;
text lookup retains the pre-event state for latched modifiers. A Wayland sync
boundary flushes keys without modifier changes. Focus loss releases tracked
keys and stops repeat.
- Text input uses text-input-v3 when available and local XKB composition for
forwarded keys. IME batches retain their surrounding-text revision and UTF-8
offsets until `done`. Surrounding text is cropped to the protocol's 4000-byte
@ -138,7 +141,8 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
A private D-Bus connection subscribes before submission, correlates early
responses, closes cancelled requests, and handles portal-owner changes.
Portal discovery is asynchronous, so its capability can appear after `READY`.
Connection/method setup is bounded; human interaction has no timeout.
Connection/method setup is bounded; human interaction has no timeout. URL
handoff includes an activation token when recent focused input permits it.
- Portal Settings supplies appearance, contrast and reduced-motion preferences
and live changes. Unknown/unavailable values retain neutral defaults. See the
[Settings contract](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html)
@ -178,9 +182,9 @@ does not stop or modify the desktop compositor.
The service fixtures own their compositor socket and D-Bus daemon. They exercise
keys/repeat/dead-key composition, IME revisions/deletion/preedit, scrolling,
clipboard size limits and early reader exit, URI drops, activation, portal
filters/results, response-before-reply, cancellation with a late alternate
handle, parent destruction and portal restart. They never open a real chooser
clipboard size limits/cancellation/invalid UTF-8/early reader exit, URI drops,
activation, portal open/save options, live settings, response-before-reply,
cancellation with a late alternate handle, parent destruction and portal restart. They never open a real chooser
or browser or inject input into the user's desktop. The live service mode checks
portal discovery, icon submission and atomic-save watch behavior without touching
the user's clipboard. These fixtures supplement live desktop testing; they do

View File

@ -80,6 +80,7 @@ fn deleted(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, before: u32, after:
w.text.after = after;
}
fn done(_: ?*anyopaque, _: ?*c.struct_zwp_text_input_v3, serial: u32) callconv(.c) void {
Input.flushKey();
const w = Input.state().text_focus orelse return;
const t = &w.text;
defer clearPending(w);
@ -276,25 +277,26 @@ pub fn deinit(w: *native.Window) void {
forget(w);
w.owner.gpa.free(w.text.surrounding);
}
pub fn key(w: *native.Window, code: u32, sym: u32, logical: u32, input_id: u64) void {
pub fn key(w: *native.Window, sym: u32, logical: u32, input_id: u64, modifiers: u32, plain: []const u8) void {
const s = Input.state();
const t = &w.text;
if (t.ime_composing or s.state == null) return;
const edit = Keyboard.edit(logical, s.modifiers);
const edit = Keyboard.edit(logical, modifiers);
if (edit != 0) {
if (s.compose != null) c.z_xkb_compose_state_reset.?(s.compose);
endAt(w, input_id);
var e = std.mem.zeroes(n.z_event);
e.type = n.UHDK_EVENT_EDIT_COMMAND;
e.input_id = input_id;
e.data.edit = .{ .command = edit, .extend_selection = @intFromBool(s.modifiers & n.UHDK_MOD_SHIFT != 0) };
e.data.edit = .{ .command = edit, .extend_selection = @intFromBool(modifiers & n.UHDK_MOD_SHIFT != 0) };
t.last_edit = input_id;
Input.send(w, &e);
return;
}
if (s.modifiers & (n.UHDK_MOD_CONTROL | n.UHDK_MOD_ALT | n.UHDK_MOD_SUPER) != 0) return;
if (modifiers & (n.UHDK_MOD_CONTROL | n.UHDK_MOD_ALT | n.UHDK_MOD_SUPER) != 0) return;
var buffer: [256]u8 = undefined;
var length: c_int = 0;
var length: c_int = @intCast(plain.len);
@memcpy(buffer[0..plain.len], plain);
if (s.compose != null) {
_ = c.z_xkb_compose_state_feed.?(s.compose, sym);
switch (c.z_xkb_compose_state_get_status.?(s.compose)) {
@ -312,9 +314,9 @@ pub fn key(w: *native.Window, code: u32, sym: u32, logical: u32, input_id: u64)
c.z_xkb_compose_state_reset.?(s.compose);
endAt(w, input_id);
},
else => length = c.z_xkb_state_key_get_utf8.?(s.state, code + 8, &buffer, buffer.len),
else => {},
}
}
} else length = c.z_xkb_state_key_get_utf8.?(s.state, code + 8, &buffer, buffer.len);
if (length <= 0 or length >= buffer.len or buffer[0] < 32 or buffer[0] == 127) return;
var e = std.mem.zeroes(n.z_event);
e.type = n.UHDK_EVENT_TEXT_COMMIT;

View File

@ -8,7 +8,6 @@
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
#include "wayland-client-core.h"
#include "wayland-egl-core.h"

View File

@ -37,7 +37,10 @@ async def main(binary):
async def delayed_reply(message, handle, delay, is_url=False):
await asyncio.sleep(delay)
bus.send(Message.new_method_return(message, 'o', [handle]))
if is_url: response(handle)
if is_url:
for key, value in [('color-scheme', 2), ('contrast', 0), ('reduced-motion', 0)]:
bus.send(Message.new_signal(PATH, 'org.freedesktop.portal.Settings', 'SettingChanged', 'ssv', ['org.freedesktop.appearance', key, Variant('u', value)]))
response(handle)
async def restart():
await bus.release_name(DESKTOP)
@ -65,11 +68,13 @@ async def main(binary):
handle_path = PATH + '/request/' + message.sender[1:].replace('.', '_') + '/' + token
if message.member == 'OpenURI':
assert title == 'https://example.org/test'
assert options['activation_token'].value == 'fixture-token'
schedule(delayed_reply(message, handle_path, .35, True))
seen.add('url')
return True
seen.add(title)
assert message.signature == 'ssa{sv}'
if message.member == 'OpenFile':
assert options['multiple'].value and not options['directory'].value
assert options['filters'].value == [['Text', [[0, '*.txt'], [0, '*.md']]]]
if title == 'success':
@ -79,6 +84,13 @@ async def main(binary):
# must already be subscribed using its chosen handle token.
response(handle_path, results={'uris': Variant('as', ['file:///tmp/a%20b%25%FF.txt', 'file:///tmp/second'])})
bus.send(Message.new_method_return(message, 'o', [handle_path]))
elif title == 'save':
assert message.member == 'SaveFile'
assert options['current_name'].value == 'book.txt'
assert options['accept_label'].value == 'Store'
assert bytes(options['current_folder'].value) == b'/tmp\0'
bus.send(Message.new_method_return(message, 'o', [handle_path]))
response(handle_path, results={'uris': Variant('as', ['file:///tmp/save.txt'])})
elif title == 'cancel':
# Return a different handle after local cancellation; both
# the predicted and returned paths need Close calls.
@ -102,7 +114,7 @@ async def main(binary):
bus.add_message_handler(handle)
await asyncio.to_thread(run, binary, 'portal', dict(os.environ, DBUS_SESSION_BUS_ADDRESS=address))
await asyncio.sleep(.1)
assert {'success', 'cancel', 'url', 'restart', 'dismiss'} <= seen, seen
assert {'success', 'save', 'cancel', 'url', 'restart', 'dismiss'} <= seen, seen
assert any(p.endswith('_late') for p in closed), closed
assert len(closed) >= 2, closed
print('private portal protocol/cancellation/restart fixture: PASS')

View File

@ -162,6 +162,28 @@ static void portals(uhdk_app *app) {
files->ptr[0], UHDK_FILE_NATIVE_PATH, bytes, sizeof(bytes), &len
));
assert(len == 14 && memcmp(bytes, "/tmp/a b%\xff.txt", 14) == 0);
uhdk_preferences prefs = {0};
prefs.struct_size = sizeof(prefs);
OK(uhdk_app_get_preferences(app, &prefs));
assert(prefs.appearance == UHDK_APPEARANCE_DARK && prefs.reduced_motion && prefs.high_contrast);
uhdk_file *directory;
OK(
uhdk_file_create_from_native_path(app, (uhdk_bytes){(const uint8_t *)"/tmp", 4}, &directory)
);
desc.title = text("save");
desc.kind = UHDK_DIALOG_SAVE_FILE;
desc.suggested_name = text("book.txt");
desc.accept_label = text("Store");
desc.initial_directory = directory;
OK(uhdk_file_dialog_show_async(app, &desc, &id));
r = wait_result(app, id);
OK(r.status);
assert(((const uhdk_file_list *)r.data)->len == 1);
uhdk_file_release(directory);
desc.kind = UHDK_DIALOG_OPEN_FILES;
desc.initial_directory = NULL;
desc.suggested_name = text("");
desc.accept_label = text("");
desc.title = text("cancel");
OK(uhdk_file_dialog_show_async(app, &desc, &id));
// Allow method submission, then cancel while the mock withholds its reply.
@ -169,12 +191,17 @@ static void portals(uhdk_app *app) {
OK(uhdk_app_poll_events(app, 100000000, &events));
OK(uhdk_request_cancel(app, id));
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
OK(uhdk_shell_open_url_async(app, text("https://example.org/test"), &id));
OK(wait_result(app, id).status);
OK(uhdk_app_get_preferences(app, &prefs));
assert(
prefs.appearance == UHDK_APPEARANCE_LIGHT && !prefs.reduced_motion && !prefs.high_contrast
);
desc.title = text("parent");
OK(uhdk_file_dialog_show_async(app, &desc, &id));
uhdk_window_destroy(w);
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
OK(uhdk_shell_open_url_async(app, text("https://example.org/test"), &id));
OK(wait_result(app, id).status);
desc.parent = NULL;
desc.title = text("restart");
OK(uhdk_file_dialog_show_async(app, &desc, &id));
@ -200,19 +227,28 @@ static void input(uhdk_app *app) {
state.selection_len = 1;
OK(uhdk_window_set_text_state(w, &state));
unsigned key = 0, repeat = 0, commit = 0, preedit = 0, pointer = 0, scroll = 0, dropped = 0,
copy = 0, compose = 0;
copy = 0, compose = 0, captured = 0, released = 0;
uint64_t acknowledged = 0, capture_request = 0;
int published = 0;
uint64_t end = deadline(app);
while (uhdk_app_get_time_ns(app) < end && !(key && repeat && commit >= 2 && preedit &&
pointer && scroll && dropped && copy && compose)) {
while (uhdk_app_get_time_ns(app) < end &&
!(key && repeat && commit >= 2 && preedit && pointer && scroll && dropped && copy &&
compose && captured && released)) {
uhdk_event_list events;
OK(uhdk_app_poll_events(app, 50000000, &events));
for (uint64_t i = 0; i < events.len; ++i) {
const uhdk_event *e = &events.ptr[i];
if (e->input_id > acknowledged)
acknowledged = e->input_id;
switch (e->type) {
case UHDK_EVENT_KEY: {
const uhdk_key_event *k = e->data;
if (k->physical_key == 4 && k->logical_key == 'a' && k->pressed)
key++;
if (k->physical_key == 224)
assert(!!(k->modifiers & UHDK_MOD_CONTROL) == !!k->pressed);
if (k->logical_key == 'A' && k->pressed)
assert(!(k->modifiers & UHDK_MOD_SHIFT));
if (k->repeat)
repeat++;
break;
@ -241,6 +277,22 @@ static void input(uhdk_app *app) {
preedit++;
break;
}
case UHDK_EVENT_POINTER_BUTTON:
if (((const uhdk_pointer_event *)e->data)->pressed)
OK(uhdk_window_set_pointer_mode_async(
w, UHDK_POINTER_CAPTURE, &capture_request
));
break;
case UHDK_EVENT_POINTER_MODE_CHANGED:
if (((const uhdk_pointer_mode_event *)e->data)->mode == UHDK_POINTER_CAPTURE)
captured++;
else
released++;
break;
case UHDK_EVENT_REQUEST_DONE:
if (((const uhdk_request_result *)e->data)->id == capture_request)
OK(((const uhdk_request_result *)e->data)->status);
break;
case UHDK_EVENT_POINTER_MOVE: {
const uhdk_pointer_event *p = e->data;
if (p->position.x == 12 && p->position.y == 18)
@ -263,6 +315,13 @@ static void input(uhdk_app *app) {
break;
}
}
if (repeat && !published) {
state.surrounding_text = text("abcd");
state.revision = 78;
state.acknowledged_input_id = acknowledged;
OK(uhdk_window_set_text_state(w, &state));
published = 1;
}
}
fprintf(
stderr,
@ -286,6 +345,17 @@ static void input(uhdk_app *app) {
OK(r.status);
const uhdk_text *t = r.data;
assert(t->len == 11 && memcmp(t->ptr, "hello café", 11) == 0);
OK(uhdk_clipboard_read_text_async(app, 100, &id));
uhdk_event_list pause;
OK(uhdk_app_poll_events(app, 100000000, &pause));
OK(uhdk_request_cancel(app, id));
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
OK(uhdk_clipboard_read_text_async(app, 100, &id));
assert(wait_result(app, id).status == UHDK_STATUS_INVALID_ARGUMENT);
OK(uhdk_clipboard_read_text_async(app, 0, &id));
r = wait_result(app, id);
OK(r.status);
assert(((const uhdk_text *)r.data)->len == 0);
OK(uhdk_clipboard_write_text_async(app, text("local copy"), &id));
OK(wait_result(app, id).status);
OK(uhdk_window_request_focus_async(w, &id));

View File

@ -73,6 +73,8 @@ class Compositor:
self.tokens = False
self.finished_drop = False
self.outgoing = None
self.receives = 0
self.held = []
def event(self, obj, name, *values):
interface = INTERFACES[self.objects[obj]]
@ -176,7 +178,15 @@ class Compositor:
elif iface == 'wl_data_offer' and name == 'receive':
mime, fd = args
assert not fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_NONBLOCK
os.write(fd, b'file:///tmp/drop%20file\r\n' if mime == 'text/uri-list' else 'hello café'.encode())
if mime == 'text/uri-list':
payload = b'file:///tmp/drop%20file\r\n'
else:
self.receives += 1
if self.receives == 3:
self.held.append(fd)
return
payload = b'\xff' if self.receives == 4 else b'' if self.receives == 5 else 'hello café'.encode()
os.write(fd, payload)
os.close(fd)
elif iface == 'wl_data_offer' and name == 'finish':
self.finished_drop = True
@ -207,6 +217,7 @@ class Compositor:
self.event(keyboard, 'key', 203, 3, 48, 1)
self.event(pointer, 'enter', 204, surface, 10 * 256, 10 * 256)
self.event(pointer, 'motion', 4, 12 * 256, 18 * 256)
self.event(pointer, 'button', 204, 4, 0x110, 1)
self.event(pointer, 'axis_source', 0)
self.event(pointer, 'axis', 5, 0, 5 * 256)
self.event(pointer, 'axis_value120', 0, 60)
@ -225,15 +236,29 @@ class Compositor:
text = self.ids.get('zwp_text_input_v3')
if text and self.surfaces and not getattr(self, 'text_entered', False):
self.text_entered = True
self.event(text, 'enter', next(iter(self.surfaces)))
surface = next(iter(self.surfaces))
self.event(text, 'enter', surface)
if not self.inject:
keyboard = self.ids['wl_keyboard']
self.event(keyboard, 'enter', 190, surface, b'')
self.event(keyboard, 'key', 191, 1, 30, 1)
self.event(keyboard, 'key', 192, 2, 30, 0)
if self.later and time.monotonic() >= self.later[0]:
_, text, serial = self.later; self.later = None
assert self.text_serial > serial, (self.text_serial, serial)
self.event(self.ids['wl_pointer'], 'button', 206, 6, 0x110, 0)
keyboard = self.ids['wl_keyboard']
self.event(keyboard, 'key', 206, 6, 48, 0)
self.event(keyboard, 'key', 207, 7, 29, 1)
self.event(keyboard, 'modifiers', 207, 4, 0, 0, 0)
self.event(keyboard, 'key', 208, 7, 46, 1)
self.event(keyboard, 'key', 209, 8, 46, 0)
self.event(keyboard, 'key', 210, 9, 29, 0)
self.event(keyboard, 'modifiers', 210, 0, 0, 0, 0)
self.event(keyboard, 'modifiers', 210, 0, 1, 0, 0)
self.event(keyboard, 'key', 210, 9, 30, 1)
self.event(keyboard, 'modifiers', 210, 0, 0, 0, 0)
self.event(keyboard, 'key', 210, 9, 30, 0)
self.event(keyboard, 'key', 211, 9, 40, 1)
self.event(keyboard, 'key', 212, 10, 40, 0)
self.event(keyboard, 'key', 213, 11, 18, 1)
@ -266,7 +291,7 @@ def run(binary, mode, env=None):
finally:
if process.poll() is None: process.kill(); process.wait()
server.close()
for fd in compositor.fds: os.close(fd)
for fd in compositor.fds + compositor.held: os.close(fd)
if compositor.outgoing is not None: os.close(compositor.outgoing)