Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271const std = @import("std");const builtin = @import("builtin");const host = @import("host.zig");const native_secret_store = @import("native_secret_store.zig");const debug_trace = @import("../shared/debug_trace.zig");const io_mod = @import("../shared/io.zig");
pub const clipboard = host.Clipboard{ .copy_fn = copyToClipboard, .copy_file_fn = copy_file_to_clipboard,};
pub const secret_store = native_secret_store.provider;
fn copyToClipboard(_: ?*anyopaque, text: []const u8) host.ClipboardError!bool { const argv = clipboardCommand(builtin.os.tag) orelse return false; const io = io_mod.getIo(); var child = std.process.spawn(io, .{ .argv = argv, .stdin = .pipe, .stdout = .ignore, .stderr = .ignore, }) catch |err| { debug_trace.logf("host", "clipboard copy spawn failed err={s}", .{@errorName(err)}); return error.CopyFailed; }; defer child.kill(io);
if (child.stdin) |*stdin| { stdin.writeStreamingAll(io, text) catch |err| { stdin.close(io); child.stdin = null; debug_trace.logf("host", "clipboard copy write failed err={s}", .{@errorName(err)}); return error.CopyFailed; }; stdin.close(io); child.stdin = null; } else { debug_trace.logf("host", "clipboard copy failed reason=stdin_unavailable", .{}); return error.CopyFailed; }
const term = child.wait(io) catch |err| { debug_trace.logf("host", "clipboard copy wait failed err={s}", .{@errorName(err)}); return error.CopyFailed; }; if (!copySucceeded(term)) { logUnsuccessfulTerm(term); return error.CopyFailed; } return true;}
const ClipboardProcessResult = struct { term: std.process.Child.Term, stderr: []u8,};
fn collect_clipboard_process_output( alloc: std.mem.Allocator, child: *std.process.Child, deadline: std.Io.Clock.Timestamp,) std.process.RunError![]u8 { var multi_reader_buffer: std.Io.File.MultiReader.Buffer(2) = undefined; var multi_reader: std.Io.File.MultiReader = undefined; multi_reader.init(alloc, io_mod.getIo(), multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? }); defer multi_reader.deinit();
const stdout_reader = multi_reader.reader(0); const stderr_reader = multi_reader.reader(1); while (multi_reader.fill(64, .{ .deadline = deadline })) |_| { if (stdout_reader.buffered().len > 1024 or stderr_reader.buffered().len > 4096) { return error.StreamTooLong; } } else |err| switch (err) { error.EndOfStream => {}, else => |fill_err| return fill_err, } try multi_reader.checkAnyError();
return multi_reader.toOwnedSlice(1);}
fn clipboard_process_term(status: c_int) std.process.Child.Term { const raw_status: u32 = @bitCast(status); return if (std.c.W.IFEXITED(raw_status)) .{ .exited = std.c.W.EXITSTATUS(raw_status) } else if (std.c.W.IFSIGNALED(raw_status)) .{ .signal = std.c.W.TERMSIG(raw_status) } else if (std.c.W.IFSTOPPED(raw_status)) .{ .stopped = std.c.W.STOPSIG(raw_status) } else .{ .unknown = raw_status };}
fn close_clipboard_process_streams(child: *std.process.Child) void { const io = io_mod.getIo(); if (child.stdin) |stdin| stdin.close(io); if (child.stdout) |stdout| stdout.close(io); if (child.stderr) |stderr| stderr.close(io); child.stdin = null; child.stdout = null; child.stderr = null;}
fn try_reap_clipboard_process(child: *std.process.Child) error{WaitFailed}!?std.process.Child.Term { const pid = child.id orelse return error.WaitFailed; var status: c_int = undefined; const waited = std.c.waitpid(pid, &status, std.c.W.NOHANG); if (waited == 0) return null; if (waited != pid) return error.WaitFailed;
child.id = null; return clipboard_process_term(status);}
fn kill_and_wait_clipboard_process(child: *std.process.Child) !std.process.Child.Term { const pid = child.id orelse return error.WaitFailed; std.posix.kill(pid, .KILL) catch |err| switch (err) { error.ProcessNotFound => {}, else => |kill_err| return kill_err, }; return child.wait(io_mod.getIo());}
fn wait_for_clipboard_process( child: *std.process.Child, deadline: std.Io.Clock.Timestamp,) !std.process.Child.Term { const io = io_mod.getIo(); while (true) { if (try try_reap_clipboard_process(child)) |term| return term;
const now = std.Io.Clock.Timestamp.now(io, .awake); if (!std.Io.Clock.Timestamp.compare(now, .lt, deadline)) { _ = try kill_and_wait_clipboard_process(child); return error.Timeout; } try std.Io.sleep(io, .fromMilliseconds(10), .awake); }}
fn run_clipboard_process( alloc: std.mem.Allocator, argv: []const []const u8, deadline: std.Io.Clock.Timestamp,) !ClipboardProcessResult { var child = try std.process.spawn(io_mod.getIo(), .{ .argv = argv, .stdin = .ignore, .stdout = .pipe, .stderr = .pipe, }); defer child.kill(io_mod.getIo()); defer close_clipboard_process_streams(&child);
const term = wait_for_clipboard_process(&child, deadline) catch |err| { if (child.id != null) { _ = kill_and_wait_clipboard_process(&child) catch |cleanup_err| { debug_trace.logf("host", "clipboard file copy cleanup failed err={s}", .{@errorName(cleanup_err)}); }; } return err; }; const stderr = try collect_clipboard_process_output(alloc, &child, deadline); return .{ .term = term, .stderr = stderr, };}
// Publish eager file representations so the pasteboard server owns them after// this short-lived process exits.fn copy_file_to_clipboard(_: ?*anyopaque, alloc: std.mem.Allocator, path: []const u8) host.ClipboardError!bool { if (comptime builtin.os.tag != .macos) return false;
const script = \\function run(argv) { \\ ObjC.import("AppKit"); \\ var url = $.NSURL.fileURLWithPath(argv[0]).standardizedURL; \\ var expected = ObjC.unwrap(url.absoluteString); \\ var pb = $.NSPasteboard.generalPasteboard; \\ pb.clearContents; \\ if (!pb.setStringForType(expected, "public.file-url")) { \\ throw new Error("public.file-url materialization failed"); \\ } \\ var files = $.NSArray.arrayWithObject(argv[0]); \\ if (!pb.setPropertyListForType(files, "NSFilenamesPboardType")) { \\ throw new Error("NSFilenamesPboardType materialization failed"); \\ } \\ var copiedValue = pb.stringForType("public.file-url"); \\ if (!copiedValue) throw new Error("public.file-url readback missing"); \\ var copied = ObjC.unwrap(copiedValue); \\ if (copied !== expected) throw new Error("public.file-url readback mismatch"); \\ var copiedFiles = ObjC.deepUnwrap(pb.propertyListForType("NSFilenamesPboardType")); \\ if (!copiedFiles || copiedFiles.length !== 1 || copiedFiles[0] !== argv[0]) { \\ throw new Error("NSFilenamesPboardType readback mismatch"); \\ } \\} ; const argv: []const []const u8 = &.{ "osascript", "-l", "JavaScript", "-e", script, path }; const started_ms = io_mod.milliTimestamp(); const deadline = std.Io.Clock.Timestamp.fromNow(io_mod.getIo(), .{ .clock = .awake, .raw = .fromSeconds(5), }); const result = run_clipboard_process(alloc, argv, deadline) catch |err| { const finished_ms = io_mod.milliTimestamp(); const elapsed_ms = if (finished_ms >= started_ms) finished_ms - started_ms else 0; debug_trace.logf("host", "clipboard file copy process failed err={s} elapsed_ms={d}", .{ @errorName(err), elapsed_ms }); return error.CopyFailed; }; defer alloc.free(result.stderr);
const finished_ms = io_mod.milliTimestamp(); const elapsed_ms = if (finished_ms >= started_ms) finished_ms - started_ms else 0; switch (result.term) { .exited => |code| { if (code == 0) return true; debug_trace.logf("host", "clipboard file copy failed exit_code={d} elapsed_ms={d} stderr={s}", .{ code, elapsed_ms, result.stderr }); }, .signal => |signal| debug_trace.logf("host", "clipboard file copy failed term=signal signal={d} elapsed_ms={d} stderr={s}", .{ @intFromEnum(signal), elapsed_ms, result.stderr }), .stopped => |signal| debug_trace.logf("host", "clipboard file copy failed term=stopped signal={d} elapsed_ms={d} stderr={s}", .{ @intFromEnum(signal), elapsed_ms, result.stderr }), .unknown => |status| debug_trace.logf("host", "clipboard file copy failed term=unknown status={d} elapsed_ms={d} stderr={s}", .{ status, elapsed_ms, result.stderr }), } return error.CopyFailed;}
fn clipboardCommand(os_tag: std.Target.Os.Tag) ?[]const []const u8 { return switch (os_tag) { .macos => &.{"pbcopy"}, .linux => &.{ "xclip", "-selection", "clipboard" }, else => null, };}
fn copySucceeded(term: std.process.Child.Term) bool { return switch (term) { .exited => |code| code == 0, .signal, .stopped, .unknown => false, };}
fn logUnsuccessfulTerm(term: std.process.Child.Term) void { switch (term) { .exited => |code| debug_trace.logf("host", "clipboard copy failed exit_code={d}", .{code}), .signal => |signal| debug_trace.logf("host", "clipboard copy failed term=signal signal={d}", .{@intFromEnum(signal)}), .stopped => |signal| debug_trace.logf("host", "clipboard copy failed term=stopped signal={d}", .{@intFromEnum(signal)}), .unknown => |status| debug_trace.logf("host", "clipboard copy failed term=unknown status={d}", .{status}), }}
test "native clipboard selects the platform command" { try std.testing.expectEqualStrings("pbcopy", clipboardCommand(.macos).?[0]); try std.testing.expectEqualSlices( []const u8, &.{ "xclip", "-selection", "clipboard" }, clipboardCommand(.linux).?, ); try std.testing.expect(clipboardCommand(.windows) == null); try std.testing.expect(clipboardCommand(.wasi) == null);}
test "native clipboard accepts only a successful exit" { try std.testing.expect(copySucceeded(.{ .exited = 0 })); try std.testing.expect(!copySucceeded(.{ .exited = 1 })); try std.testing.expect(!copySucceeded(.{ .signal = .TERM })); try std.testing.expect(!copySucceeded(.{ .stopped = .STOP })); try std.testing.expect(!copySucceeded(.{ .unknown = 1 }));}