zcatui/examples/clipboard_demo.zig
reugenio 3ec75f6264 Add clipboard support via OSC 52 escape sequences
New features:
- Copy text to system clipboard (Ctrl+V paste)
- Copy to X11 primary selection (middle-click paste)
- Copy to both selections simultaneously
- Clear clipboard contents
- Query clipboard (limited terminal support)
- Parse OSC 52 responses

API:
- copy(allocator, writer, text) - allocating version
- copySmall(writer, text) - stack-based for text <= 1024 bytes
- copyTo/copySmallTo - select target (clipboard/primary/both)
- clear(writer) - clear clipboard
- parseResponse(allocator, response) - decode clipboard responses

Example: clipboard_demo.zig with interactive testing

Tests: 8 clipboard tests, all pass

Terminal support: xterm, iTerm2, kitty, alacritty, WezTerm, foot
Note: tmux requires 'set -g set-clipboard on'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 13:43:36 +01:00

192 lines
6 KiB
Zig

//! Clipboard demo for zcatui.
//!
//! Demonstrates OSC 52 clipboard support:
//! - Copy text to system clipboard
//! - Copy to primary selection (X11)
//! - Clear clipboard
//!
//! Run with: zig build clipboard-demo
//!
//! Note: OSC 52 support varies by terminal. If clipboard operations
//! don't work, check your terminal's documentation.
const std = @import("std");
const zcatui = @import("zcatui");
const Terminal = zcatui.Terminal;
const Buffer = zcatui.Buffer;
const Rect = zcatui.Rect;
const Style = zcatui.Style;
const Color = zcatui.Color;
const Event = zcatui.Event;
const KeyCode = zcatui.KeyCode;
const Layout = zcatui.Layout;
const Constraint = zcatui.Constraint;
const Block = zcatui.widgets.Block;
const Borders = zcatui.widgets.Borders;
const Paragraph = zcatui.widgets.Paragraph;
const Clipboard = zcatui.clipboard;
const AppState = struct {
running: bool = true,
last_action: []const u8 = "Press a key to test clipboard",
copy_count: u32 = 0,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var term = try Terminal.init(allocator);
defer term.deinit();
var state = AppState{};
while (state.running) {
try term.drawWithContext(&state, render);
if (try term.pollEvent(100)) |event| {
try handleEvent(&state, &term, allocator, event);
}
}
}
fn handleEvent(state: *AppState, term: *Terminal, allocator: std.mem.Allocator, event: Event) !void {
const writer = term.backend.stdout.deprecatedWriter();
switch (event) {
.key => |key| {
switch (key.code) {
.esc => state.running = false,
.char => |c| {
switch (c) {
'q', 'Q' => state.running = false,
'1' => {
state.copy_count += 1;
var buf: [64]u8 = undefined;
const text = std.fmt.bufPrint(&buf, "Hello from zcatui #{d}!", .{state.copy_count}) catch "Hello!";
try Clipboard.copy(allocator, writer, text);
state.last_action = "Copied text to clipboard (Ctrl+V to paste)";
},
'2' => {
try Clipboard.copySmallTo(writer, "Primary selection text", .primary);
state.last_action = "Copied to primary (middle-click to paste)";
},
'3' => {
try Clipboard.copySmallTo(writer, "Both clipboard and primary!", .both);
state.last_action = "Copied to both clipboard and primary";
},
'c', 'C' => {
try Clipboard.clear(writer);
state.last_action = "Cleared clipboard";
},
else => {},
}
},
else => {},
}
},
else => {},
}
}
fn render(state: *AppState, area: Rect, buf: *Buffer) void {
// Main layout
const chunks = Layout.vertical(&.{
Constraint.length(3), // Title
Constraint.length(8), // Actions
Constraint.length(5), // Status
Constraint.min(0), // Help
}).split(area);
// Title
renderTitle(chunks.get(0), buf);
// Actions
renderActions(chunks.get(1), buf);
// Status
renderStatus(state, chunks.get(2), buf);
// Help
renderHelp(chunks.get(3), buf);
}
fn renderTitle(area: Rect, buf: *Buffer) void {
const block = Block.init()
.title(" Clipboard Demo (OSC 52) ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.cyan));
block.render(area, buf);
const inner = block.inner(area);
_ = buf.setString(inner.left(), inner.top(), "Test clipboard operations in your terminal", Style.default);
}
fn renderActions(area: Rect, buf: *Buffer) void {
const block = Block.init()
.title(" Available Actions ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.yellow));
block.render(area, buf);
const inner = block.inner(area);
var y = inner.top();
const actions = [_][]const u8{
"1 - Copy text to clipboard",
"2 - Copy to primary selection (X11)",
"3 - Copy to both clipboard and primary",
"c - Clear clipboard",
"q - Quit",
};
for (actions) |action| {
if (y < inner.bottom()) {
_ = buf.setString(inner.left(), y, action, Style.default);
y += 1;
}
}
}
fn renderStatus(state: *AppState, area: Rect, buf: *Buffer) void {
const block = Block.init()
.title(" Last Action ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.green));
block.render(area, buf);
const inner = block.inner(area);
_ = buf.setString(inner.left(), inner.top(), state.last_action, Style.default.fg(Color.white).bold());
if (state.copy_count > 0) {
var count_buf: [32]u8 = undefined;
const count_text = std.fmt.bufPrint(&count_buf, "Total copies: {d}", .{state.copy_count}) catch "?";
_ = buf.setString(inner.left(), inner.top() + 1, count_text, Style.default.dim());
}
}
fn renderHelp(area: Rect, buf: *Buffer) void {
const block = Block.init()
.title(" Notes ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.blue));
block.render(area, buf);
const inner = block.inner(area);
var y = inner.top();
const notes = [_][]const u8{
"OSC 52 clipboard support varies by terminal.",
"Supported: xterm, iTerm2, kitty, alacritty, WezTerm, foot",
"tmux: enable with 'set -g set-clipboard on'",
"Some terminals disable OSC 52 by default for security.",
};
for (notes) |note| {
if (y < inner.bottom()) {
_ = buf.setString(inner.left(), y, note, Style.default.dim());
y += 1;
}
}
}