From bcf298696b1a6b691cf15fc8023ac9c4a15bfe85 Mon Sep 17 00:00:00 2001 From: Grace Yoder Date: Wed, 17 Jun 2026 09:47:42 -0600 Subject: [PATCH] init: create tpb --- .gitignore | 17 +++++++++++ LICENSE | 22 ++++++++++++++ README.md | 15 ++++++++++ build.zig | 27 +++++++++++++++++ build.zig.zon | 13 +++++++++ src/main.zig | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/main.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d3982c --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# This file is for zig-specific build artifacts. If you have OS-specific or +# editor-specific files to ignore, such as *.swp or .DS_Store, put those in +# your global ~/.gitignore and put this in your ~/.gitconfig: +# +# [core] +# excludesfile = ~/.gitignore +# +# Cheers! +# -andrewrk + +.zig-cache/ +zig-out/ +/release/ +/debug/ +/build/ +/build-*/ +/docgen_tmp/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b66f4c5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright 2026 Grace Yoder + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8eeb08 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# TPB + +> Pronounced `tee pee bee` or `tee pasteboard` + +TPB is a super simple, small program to "tee" into my clipboard using OSC 52 +codes. + +> Usage: `command | tpb` + +I was somewhat fed up with `pbcopy` eating stdout making it impossible to see +the output of what I was doing without pasting it. While, yes, this could just +be a shell script, I decided to write in a systems language because its more fun +and I like the idea of it being super low latency. Originally, I was going to +write this in Rust but decided that Zig was a better fit for something like +this. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..4e2fbe0 --- /dev/null +++ b/build.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const exe = b.addExecutable(.{ + .name = "tpb", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + + b.installArtifact(exe); + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..304943f --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,13 @@ +.{ + .name = .tpb, + .version = "0.0.1", + .fingerprint = 0x203a601c46df0265, + .minimum_zig_version = "0.16.0", + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "LICENSE", + "README.md", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..cd716ab --- /dev/null +++ b/src/main.zig @@ -0,0 +1,80 @@ +const std = @import("std"); +const File = std.Io.File; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + + // This program should not be run interactively + if (try File.stdin().isTty(io)) { + std.debug.print("This program is not meant to be run interactively. Please pipe into this program.\n", .{}); + std.process.exit(1); + } + + std.debug.print("\x1b[0;32mCopying the following to Pasteboard\x1b[0m\n", .{}); + + // We want to avoid doing as many allocations as possible so having raw + // pages with it being larger should be totally fine + const alloc = std.heap.page_allocator; + + var buf: [4096]u8 = undefined; + var copy_index: u8 = 0; + + var encoded_buffer = try std.ArrayList(u8).initCapacity(alloc, std.heap.pageSize()); + var encoder = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, null); + + while (true) { + // Read as much of stdin there is + const bytes = File.stdin().readStreaming(io, &.{buf[copy_index..]}) catch |err| { + if (err == File.ReadStreamingError.EndOfStream) { + break; + } else { + return err; + } + }; + const length = copy_index + bytes; + + // Passthrough to stdout + _ = try File.stdout().writeStreamingAll(io, buf[copy_index..length]); + + // Base64 encode + const chunkable_length = length - (length % 3); + var chunker = std.mem.window(u8, buf[0..chunkable_length], 3, 3); + while (chunker.next()) |chunk| { + var temp: [5]u8 = undefined; + const s = encoder.encode(&temp, chunk); + try encoded_buffer.appendSlice(alloc, s); + } + + // Move left over bits to the start of the array + copy_index = @intCast(length - chunkable_length); + if (copy_index != 0) { + @memmove(buf[0..copy_index], buf[chunkable_length..length]); + } + } + // Encode last bytes + var temp: [5]u8 = undefined; + const s = encoder.encode(&temp, buf[0..copy_index]); + try encoded_buffer.appendSlice(alloc, s); + + // OSC 52 Copying — written as one contiguous buffer so the terminal + // receives the complete sequence in a single write. + // https://ghostty.org/docs/vt/osc/52 + const osc_header = &[_]u8{ + 0x1b, // ESC + 0x5d, // ']' + 0x35, 0x32, // Code 52 + 0x3b, // ';' + // 0x70, // 'p' Primary Clipboard + 0x63, // 'c' Standard Clipboard + 0x3b, // ';' + }; + const osc_footer = &[_]u8{ + 0x1b, // ESC + 0x5c, // '\' + }; + + _ = try File.stdout().writeStreamingAll(io, osc_header); + _ = try File.stdout().writeStreamingAll(io, encoded_buffer.items); + _ = try File.stdout().writeStreamingAll(io, osc_footer); + std.debug.print("\x1b[0;32mCopied To Pasteboard!\x1b[0m\n", .{}); +}