init: create tpb
This commit is contained in:
commit
bcf298696b
6 changed files with 174 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -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/
|
||||||
22
LICENSE
Normal file
22
LICENSE
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
15
README.md
Normal file
15
README.md
Normal file
|
|
@ -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.
|
||||||
27
build.zig
Normal file
27
build.zig
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
build.zig.zon
Normal file
13
build.zig.zon
Normal file
|
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
80
src/main.zig
Normal file
80
src/main.zig
Normal file
|
|
@ -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", .{});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue