zcatui/examples/input_demo.zig
reugenio 4d27a7b13d Add readline-style Input widget with history support
New widget: src/widgets/input.zig
- Basic editing (insert, delete, backspace)
- Cursor movement (home, end, left, right)
- Word navigation (Ctrl+Left/Right, Ctrl+B/F)
- Kill/yank operations (Ctrl+K, Ctrl+U, Ctrl+Y)
- Delete word (Ctrl+W)
- History navigation (Up/Down arrows)
- Unicode support (UTF-8 encoding)
- Password masking support

New example: examples/input_demo.zig
- Interactive demo with normal and password inputs
- Tab to switch focus between inputs
- Shows submitted values in message log

Run with: zig build input-demo

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

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

231 lines
7.4 KiB
Zig

//! Interactive input demo for zcatui.
//!
//! Demonstrates the readline-style Input widget with:
//! - Basic text editing (insert, delete, backspace)
//! - Cursor movement (home, end, left, right)
//! - Word navigation (Ctrl+Left/Right)
//! - Kill/yank (Ctrl+K, Ctrl+Y)
//! - Clear line (Ctrl+U)
//! - History navigation (Up/Down arrows)
//!
//! Run with: zig build input-demo
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 Input = zcatui.widgets.Input;
const InputState = zcatui.widgets.InputState;
const AppState = struct {
input_state: InputState,
password_state: InputState,
focus: Focus = .normal_input,
messages: std.ArrayListUnmanaged([]u8) = .{},
running: bool = true,
allocator: std.mem.Allocator,
const Focus = enum { normal_input, password_input };
fn init(allocator: std.mem.Allocator) AppState {
return .{
.input_state = InputState.init(allocator),
.password_state = InputState.init(allocator),
.allocator = allocator,
};
}
fn deinit(self: *AppState) void {
self.input_state.deinit();
self.password_state.deinit();
for (self.messages.items) |msg| {
self.allocator.free(msg);
}
self.messages.deinit(self.allocator);
}
fn addMessage(self: *AppState, msg: []const u8) !void {
const copy = try self.allocator.dupe(u8, msg);
try self.messages.append(self.allocator, copy);
// Keep only last 10 messages
while (self.messages.items.len > 10) {
self.allocator.free(self.messages.orderedRemove(0));
}
}
fn toggleFocus(self: *AppState) void {
self.focus = if (self.focus == .normal_input) .password_input else .normal_input;
}
};
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.init(allocator);
defer state.deinit();
// Add initial help message
try state.addMessage("Type text and press Enter to submit.");
try state.addMessage("Use Tab to switch between inputs.");
try state.addMessage("Press Ctrl+C or Esc to quit.");
while (state.running) {
try term.drawWithContext(&state, render);
if (try term.pollEvent(100)) |event| {
try handleEvent(&state, event);
}
}
}
fn handleEvent(state: *AppState, event: Event) !void {
switch (event) {
.key => |key| {
switch (key.code) {
.esc => state.running = false,
.char => |c| {
if (key.modifiers.ctrl and (c == 'c' or c == 'C')) {
state.running = false;
return;
}
// Let input handle other chars
const input_state = switch (state.focus) {
.normal_input => &state.input_state,
.password_input => &state.password_state,
};
_ = try input_state.handleKey(key);
},
.tab => state.toggleFocus(),
.enter => {
const input_state = switch (state.focus) {
.normal_input => &state.input_state,
.password_input => &state.password_state,
};
const submitted = try input_state.submit();
if (submitted.len > 0) {
var msg_buf: [128]u8 = undefined;
const prefix = if (state.focus == .normal_input) "Input" else "Password";
const msg = std.fmt.bufPrint(&msg_buf, "{s}: {s}", .{ prefix, submitted }) catch "???";
try state.addMessage(msg);
state.allocator.free(submitted);
}
},
else => {
const input_state = switch (state.focus) {
.normal_input => &state.input_state,
.password_input => &state.password_state,
};
_ = try input_state.handleKey(key);
},
}
},
else => {},
}
}
fn render(state: *AppState, area: Rect, buf: *Buffer) void {
// Layout: help at top, inputs in middle, messages at bottom
const chunks = Layout.vertical(&.{
Constraint.length(6), // Help
Constraint.length(3), // Normal input
Constraint.length(3), // Password input
Constraint.min(0), // Messages
}).split(area);
// Help section
renderHelp(chunks.get(0), buf);
// Normal input
const normal_style = if (state.focus == .normal_input)
Style.default.fg(Color.cyan)
else
Style.default.fg(Color.white);
const normal_input = Input.init()
.setBlock(Block.init()
.title(" Input (Tab to switch) ")
.setBorders(Borders.all)
.style(normal_style))
.setPlaceholder("Type here...")
.showCursor(state.focus == .normal_input);
normal_input.render(chunks.get(1), buf, &state.input_state);
// Password input
const password_style = if (state.focus == .password_input)
Style.default.fg(Color.cyan)
else
Style.default.fg(Color.white);
const password_input = Input.init()
.setBlock(Block.init()
.title(" Password ")
.setBorders(Borders.all)
.style(password_style))
.setPlaceholder("Enter password...")
.setMask('*')
.showCursor(state.focus == .password_input);
password_input.render(chunks.get(2), buf, &state.password_state);
// Messages
renderMessages(state, chunks.get(3), buf);
}
fn renderHelp(area: Rect, buf: *Buffer) void {
const help_block = Block.init()
.title(" Keyboard Shortcuts ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.yellow));
help_block.render(area, buf);
const inner = help_block.inner(area);
var y = inner.top();
const lines = [_][]const u8{
"Ctrl+A/E: Home/End | Ctrl+B/F: Left/Right | Ctrl+W: Delete word",
"Ctrl+K: Kill to end | Ctrl+U: Kill to start | Ctrl+Y: Yank",
"Up/Down: History | Tab: Switch input | Enter: Submit | Esc: Quit",
};
for (lines) |line| {
if (y < inner.bottom()) {
_ = buf.setString(inner.left(), y, line, Style.default);
y += 1;
}
}
}
fn renderMessages(state: *AppState, area: Rect, buf: *Buffer) void {
const msg_block = Block.init()
.title(" Messages ")
.setBorders(Borders.all)
.style(Style.default.fg(Color.green));
msg_block.render(area, buf);
const inner = msg_block.inner(area);
var y = inner.top();
// Show messages in reverse order (newest at top)
var i: usize = state.messages.items.len;
while (i > 0 and y < inner.bottom()) {
i -= 1;
const msg = state.messages.items[i];
_ = buf.setString(inner.left(), y, msg, Style.default);
y += 1;
}
}