port vinput clipboard backend to wayland

This commit is contained in:
LordMZTE 2023-10-25 19:44:27 +02:00
parent 2ee606169e
commit 6b5af21787
Signed by: LordMZTE
GPG key ID: B64802DC33A64FF6
4 changed files with 306 additions and 175 deletions

View file

@ -1,9 +1,14 @@
const std = @import("std");
const Scanner = @import("wayland").Scanner;
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
const mode = b.standardOptimizeOption(.{});
const scanner = Scanner.create(b, .{});
const wayland_mod = b.createModule(.{ .source_file = scanner.result });
const exe = b.addExecutable(.{
.name = "vinput",
.root_source_file = .{ .path = "src/main.zig" },
@ -11,11 +16,23 @@ pub fn build(b: *std.build.Builder) void {
.optimize = mode,
});
exe.addModule("wayland", wayland_mod);
scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
scanner.generate("wl_seat", 4);
scanner.generate("wl_data_device_manager", 3);
scanner.generate("wl_compositor", 4);
scanner.generate("wl_shm", 1);
scanner.generate("xdg_wm_base", 3);
exe.linkLibC();
exe.linkSystemLibrary("x11");
exe.linkSystemLibrary("wayland-client");
exe.strip = mode != .Debug;
scanner.addCSource(exe);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);

View file

@ -0,0 +1,12 @@
.{
.name = "vinput",
.version = "0.0.0",
.paths = .{""},
.dependencies = .{
.wayland = .{
.url = "https://git.mzte.de/LordMZTE/zig-wayland/archive/85722422985f928087e56d90c3617ecb04232486.tar.gz",
.hash = "1220d992b223e473988d203d66d262e54141b59559c09587eb00231c800d46f9b408",
}
}
}

View file

@ -1,196 +1,307 @@
const std = @import("std");
const ffi = @import("ffi.zig");
const c = ffi.c;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const xdg = wayland.client.xdg;
const log = std.log.scoped(.clipboard);
const log = std.log.scoped(.wayland_clipboard);
dpy: *c.Display,
win: c.Window,
display: *wl.Display,
shm: *wl.Shm,
seat: *wl.Seat,
compositor: *wl.Compositor,
data_device_manager: *wl.DataDeviceManager,
xdg_wm_base: *xdg.WmBase,
const ClipboardConnection = @This();
pub fn init() !ClipboardConnection {
const dpy = c.XOpenDisplay(
c.getenv("DISPLAY") orelse return error.DisplayNotSet,
) orelse return error.OpenDisplay;
errdefer _ = c.XCloseDisplay(dpy);
const GlobalCollector = struct {
seat: ?*wl.Seat,
shm: ?*wl.Shm,
compositor: ?*wl.Compositor,
data_device_manager: ?*wl.DataDeviceManager,
xdg_wm_base: ?*xdg.WmBase,
};
const screen_n = c.XDefaultScreen(dpy);
const screen = c.XScreenOfDisplay(dpy, screen_n);
const win = c.XCreateSimpleWindow(
dpy,
screen.*.root,
0,
0,
1,
1,
0,
screen.*.black_pixel,
screen.*.white_pixel,
);
_ = c.XStoreName(dpy, win, "vinput");
const PopupWindow = struct {
surface: *wl.Surface,
xdg_surface: *xdg.Surface,
xdg_toplevel: *xdg.Toplevel,
shm_pool: *wl.ShmPool,
shm_buf: *wl.Buffer,
fn show(cc: *ClipboardConnection) !PopupWindow {
const surf = try cc.compositor.createSurface();
errdefer surf.destroy();
const xdg_surface = try cc.xdg_wm_base.getXdgSurface(surf);
errdefer xdg_surface.destroy();
xdg_surface.setListener(*const void, xdgSurfaceConfigureListener, &{});
const xdg_toplevel = try xdg_surface.getToplevel();
errdefer xdg_toplevel.destroy();
xdg_toplevel.setTitle("vinput");
surf.commit();
try cc.roundtrip();
const width = 1;
const height = 1;
const stride = width * 4;
const size = stride * height; // 1x1x4 bytes
const memfd = try std.os.memfd_create("surface_shm", 0);
defer std.os.close(memfd);
try std.os.ftruncate(memfd, size);
const shm_pool = try cc.shm.createPool(memfd, size);
errdefer shm_pool.destroy();
const shm_buf = try shm_pool.createBuffer(0, width, height, stride, .argb8888);
errdefer shm_buf.destroy();
surf.attach(shm_buf, 0, 0);
surf.damage(0, 0, width, height);
surf.commit();
try cc.roundtrip();
return .{
.surface = surf,
.xdg_surface = xdg_surface,
.xdg_toplevel = xdg_toplevel,
.shm_pool = shm_pool,
.shm_buf = shm_buf,
};
}
fn deinit(self: PopupWindow) void {
self.shm_buf.destroy();
self.shm_pool.destroy();
self.xdg_toplevel.destroy();
self.xdg_surface.destroy();
self.surface.destroy();
}
};
pub fn init() !ClipboardConnection {
const dpy = try wl.Display.connect(null);
errdefer dpy.disconnect();
const registry = try dpy.getRegistry();
defer registry.destroy();
var globals = GlobalCollector{
.shm = null,
.seat = null,
.compositor = null,
.data_device_manager = null,
.xdg_wm_base = null,
};
registry.setListener(*GlobalCollector, registryListener, &globals);
log.info("beginning initial display roundtrip", .{});
if (dpy.roundtrip() != .SUCCESS) return error.RoundtripFail;
return .{
.dpy = dpy,
.win = win,
.display = dpy,
.shm = globals.shm orelse return error.MissingGlobal,
.seat = globals.seat orelse return error.MissingGlobal,
.compositor = globals.compositor orelse return error.MissingGlobal,
.data_device_manager = globals.data_device_manager orelse return error.MissingGlobal,
.xdg_wm_base = globals.xdg_wm_base orelse return error.MissingGlobal,
};
}
pub fn deinit(self: *ClipboardConnection) void {
_ = c.XDestroyWindow(self.dpy, self.win);
_ = c.XCloseDisplay(self.dpy);
self.shm.destroy();
self.seat.destroy();
self.compositor.destroy();
self.data_device_manager.destroy();
self.xdg_wm_base.destroy();
self.display.disconnect();
self.* = undefined;
}
pub fn provide(self: ClipboardConnection, data: []const u8) !void {
const selection = c.XInternAtom(self.dpy, "CLIPBOARD", 0);
const targets_atom = c.XInternAtom(self.dpy, "TARGETS", 0);
const text_atom = c.XInternAtom(self.dpy, "TEXT", 0);
var utf8_atom = c.XInternAtom(self.dpy, "UTF8_STRING", 1);
if (utf8_atom == c.None) {
utf8_atom = c.XA_STRING;
}
pub fn getContent(self: *ClipboardConnection, out_fd: std.os.fd_t) !void {
const DataDeviceListener = struct {
out_fd: std.os.fd_t,
display: *wl.Display,
_ = c.XSetSelectionOwner(self.dpy, selection, self.win, 0);
if (c.XGetSelectionOwner(self.dpy, selection) != self.win) {
return error.FailedToAquireSelection;
}
fn onEvent(_: *wl.DataDevice, ev: wl.DataDevice.Event, ddl: *@This()) void {
switch (ev) {
.data_offer => |offer| {
defer offer.id.destroy();
const MimeType = struct {
buf: [1024]u8 = undefined,
t: ?[:0]const u8 = null,
log.info("providing clipboard", .{});
fn offerListener(_: *wl.DataOffer, event: wl.DataOffer.Event, mt: *@This()) void {
const text_types = std.ComptimeStringMap(void, .{
.{ "TEXT", {} },
.{ "STRING", {} },
.{ "UTF8_STRING", {} },
});
var event: c.XEvent = undefined;
while (true) {
try ffi.checkXError(self.dpy, c.XNextEvent(self.dpy, &event));
switch (event.type) {
c.SelectionRequest => {
if (event.xselectionrequest.selection != selection)
continue;
switch (event) {
.offer => |o| {
if (mt.t) |current_type| {
var buf: [512]u8 = undefined;
const lower_type = std.ascii.lowerString(&buf, current_type);
const xsr = event.xselectionrequest;
if (std.mem.containsAtLeast(u8, lower_type, 1, "utf8") or
std.mem.containsAtLeast(u8, lower_type, 1, "utf-8"))
{
// GTK likes to mangle text when a MIME type without UTF-8
// is requested, thus we prefer it.
return;
}
}
var sent_data = false;
var r: c_int = 0;
if (xsr.target == targets_atom) {
r = c.XChangeProperty(
xsr.display,
xsr.requestor,
xsr.property,
c.XA_ATOM,
32,
c.PropModeReplace,
@ptrCast(&utf8_atom),
1,
);
} else if (xsr.target == c.XA_STRING or xsr.target == text_atom) {
r = c.XChangeProperty(
xsr.display,
xsr.requestor,
xsr.property,
c.XA_STRING,
8,
c.PropModeReplace,
data.ptr,
@intCast(data.len),
);
sent_data = true;
} else if (xsr.target == utf8_atom) {
r = c.XChangeProperty(
xsr.display,
xsr.requestor,
xsr.property,
utf8_atom,
8,
c.PropModeReplace,
data.ptr,
@intCast(data.len),
);
sent_data = true;
}
const mimetype = std.mem.span(o.mime_type);
if (text_types.has(mimetype) or
std.mem.startsWith(u8, mimetype, "text/"))
{
if (mimetype.len > mt.buf.len - 1) {
log.err("got humungous MIME type, skipping", .{});
return;
}
if ((r & 2) == 0) {
var ev = c.XSelectionEvent{
.type = c.SelectionNotify,
.display = xsr.display,
.requestor = xsr.requestor,
.selection = xsr.selection,
.time = xsr.time,
.target = xsr.target,
.property = xsr.property,
@memcpy(mt.buf[0..mimetype.len], mimetype);
mt.buf[mimetype.len] = 0;
mt.t = mt.buf[0..mimetype.len :0];
}
},
.serial = 0,
.send_event = 0,
else => {},
}
}
};
_ = c.XSendEvent(self.dpy, ev.requestor, 0, 0, @ptrCast(&ev));
if (sent_data) {
if (ffi.xGetWindowName(self.dpy, xsr.requestor)) |name| {
defer _ = c.XFree(name.ptr);
var mime = MimeType{};
log.info("sent clipboard to {s}", .{name});
} else {
log.info("sent clipboard to unknown window", .{});
}
offer.id.setListener(*MimeType, MimeType.offerListener, &mime);
if (ddl.display.dispatch() != .SUCCESS)
log.err("dispatch in data offer receive failed", .{});
if (mime.t) |mimetype| {
log.info("receiving data offer with MIME type {s}", .{mimetype});
offer.id.receive(mimetype, ddl.out_fd);
} else {
log.warn("got data offer with no text MIME type", .{});
}
}
},
c.SelectionClear => {
log.info("Selection cleared", .{});
break;
},
else => {},
},
else => {},
}
}
};
var ddl = DataDeviceListener{
.display = self.display,
.out_fd = out_fd,
};
const data_device = try self.data_device_manager.getDataDevice(self.seat);
defer data_device.release();
data_device.setListener(*DataDeviceListener, DataDeviceListener.onEvent, &ddl);
const popup = try PopupWindow.show(self);
popup.deinit();
try self.roundtrip();
}
pub fn serveContent(self: *ClipboardConnection, data: []const u8) !void {
const DataSender = struct {
data: []const u8,
data_source: *wl.DataSource,
device: *wl.DataDevice,
kb: *wl.Keyboard,
done: bool = false,
fn onEvent(_: *wl.DataSource, ev: wl.DataSource.Event, ds: *@This()) void {
switch (ev) {
.send => |send| {
log.info("sending data", .{});
var file = std.fs.File{ .handle = send.fd };
defer file.close();
file.writeAll(ds.data) catch |e| {
log.err("unable to send clipboard content: {}", .{e});
};
},
.cancelled => {
ds.done = true;
log.info("done serving data source", .{});
},
else => {},
}
}
fn keyboardListener(_: *wl.Keyboard, ev: wl.Keyboard.Event, ds: *@This()) void {
switch (ev) {
.enter => |enter| {
log.info("got keyboard enter event", .{});
ds.device.setSelection(ds.data_source, enter.serial);
},
else => {},
}
}
};
const device = try self.data_device_manager.getDataDevice(self.seat);
defer device.release();
const data_source = try self.data_device_manager.createDataSource();
defer data_source.destroy();
data_source.offer("text/plain");
data_source.offer("text/plain;charset=utf-8");
data_source.offer("TEXT");
data_source.offer("STRING");
data_source.offer("UTF8_STRING");
const kb = try self.seat.getKeyboard();
defer kb.destroy();
var data_sender = DataSender{
.data = data,
.data_source = data_source,
.device = device,
.kb = kb,
};
data_source.setListener(*DataSender, DataSender.onEvent, &data_sender);
kb.setListener(*DataSender, DataSender.keyboardListener, &data_sender);
// This generates a keyboard enter event, the serial of which we can use to set the selection.
const popup = try PopupWindow.show(self);
popup.deinit();
while (!data_sender.done) {
if (self.display.dispatch() != .SUCCESS) return error.DispatchFail;
}
}
/// Get the current text in the clipboard. Must be freed using XFree.
pub fn getText(self: ClipboardConnection) !?[]u8 {
log.info("reading clipboard", .{});
const utf8 = c.XInternAtom(self.dpy, "UTF8_STRING", 0);
if (try self.getContentForType(utf8)) |data| return data;
return try self.getContentForType(c.XA_STRING);
fn xdgSurfaceConfigureListener(xdg_surface: *xdg.Surface, ev: xdg.Surface.Event, _: *const void) void {
xdg_surface.ackConfigure(ev.configure.serial);
}
fn getContentForType(self: ClipboardConnection, t: c.Atom) !?[]u8 {
const selection = c.XInternAtom(self.dpy, "CLIPBOARD", 0);
//const utf8_atom = c.XInternAtom(self.dpy, "UTF8_STRING", 1);
const xsel_data_atom = c.XInternAtom(self.dpy, "XSEL_DATA", 0);
_ = c.XConvertSelection(self.dpy, selection, t, xsel_data_atom, self.win, c.CurrentTime);
_ = c.XSync(self.dpy, 0);
var event: c.XEvent = undefined;
try ffi.checkXError(self.dpy, c.XNextEvent(self.dpy, &event));
if (event.type != c.SelectionNotify)
return null;
const xsel = event.xselection;
// Wrong selection or conversion failed.
if (xsel.property == 0)
return null;
var target: c.Atom = undefined;
var data: ?[*]u8 = null;
var format: c_int = 0;
var size: c_ulong = 0;
var n: c_ulong = 0;
_ = c.XGetWindowProperty(
xsel.display,
xsel.requestor,
xsel.property,
0,
-1,
0,
c.AnyPropertyType,
&target,
&format,
&size,
&n,
&data,
);
defer _ = c.XDeleteProperty(xsel.display, xsel.requestor, xsel.property);
return (data orelse return null)[0..@intCast(size)];
fn registryListener(reg: *wl.Registry, event: wl.Registry.Event, globals: *GlobalCollector) void {
switch (event) {
.global => |glob| {
inline for (std.meta.fields(GlobalCollector)) |f| {
const Interface = @typeInfo(@typeInfo(f.type).Optional.child).Pointer.child;
if (std.mem.orderZ(u8, glob.interface, Interface.getInterface().name) == .eq) {
@field(globals, f.name) = reg.bind(
glob.name,
Interface,
Interface.generated_version,
) catch return;
return;
}
}
},
.global_remove => {},
}
}
fn roundtrip(self: *const ClipboardConnection) !void {
if (self.display.roundtrip() != .SUCCESS) return error.RoundtripFail;
}

View file

@ -30,20 +30,12 @@ pub fn main() !void {
var cp = try ClipboardConnection.init();
defer cp.deinit();
const cp_data = try cp.getText();
defer if (cp_data) |d| {
_ = c.XFree(d.ptr);
};
{
const file = try std.fs.createFileAbsolute(filename, .{});
defer file.close();
if (cp_data) |data| {
try file.writeAll(data);
} else {
std.log.info("clipboard empty", .{});
}
std.log.info("telling compositor to write clipboard content into tmpfile...", .{});
try cp.getContent(file.handle);
}
//const editor_argv = [_][]const u8{
@ -91,7 +83,6 @@ pub fn main() !void {
std.log.info("mmapping tempfile", .{});
// ooooh memmap, performance!
const fcontent = try std.os.mmap(
null,
stat.size,
@ -102,7 +93,7 @@ pub fn main() !void {
);
defer std.os.munmap(fcontent);
try cp.provide(std.mem.trim(u8, fcontent, " \n\r"));
try cp.serveContent(std.mem.trim(u8, fcontent, " \n\r"));
}
std.log.info("deleting tempfile {s}", .{filename});
try std.fs.deleteFileAbsolute(filename);