//! 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; } }