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>
231 lines
7.4 KiB
Zig
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;
|
|
}
|
|
}
|