home
tpb

repository View

init: create tpb

change ID
kvsvtrnmqzltyssmpoqvtqvnxxuksntq
git commit
bcf298696b1a6b691cf15fc8023ac9c4a15bfe85
author
Grace Yoder <grace_yoder@outlook.com> - 2026-06-17T09:47:42-06:00
committer
Grace Yoder <grace_yoder@outlook.com> - 2026-06-25T16:42:38-06:00
parents
refs
signature
Present (not validated)
init: create tpb

tree

type name size
file .gitignore 354 bytes
file LICENSE 1073 bytes
file README.md 564 bytes
file build.zig 680 bytes
file build.zig.zon 247 bytes
file src/main.zig 2846 bytes

diff

.gitignore

added (absent → file)

--- a/.gitignore
+++ 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/

LICENSE

added (absent → file)

--- a/LICENSE
+++ 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.
+

README.md

added (absent → file)

--- a/README.md
+++ 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.

build.zig

added (absent → file)

--- a/build.zig
+++ 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);
+    }
+}

build.zig.zon

added (absent → file)

--- a/build.zig.zon
+++ 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",
+    },
+}

src/main.zig

added (absent → file)

--- a/src/main.zig
+++ 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", .{});
+}