Something went wrong. Try again.
A charm-like tui library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156//! Blinking text cursor for text-input style widgets. Renders a single//! character that toggles between styled and reverse-video styled.//!//! Typical use: embed a Cursor in a parent struct, set `char` to whatever's//! under the cursor, route relevant updates from the parent's `update`, and//! call `cursor.view(w)` from the parent's `view`.const std = @import("std");const tea = @import("blacktea");const matte = @import("matte");const util = @import("util.zig");
const t = std.testing;const Allocator = std.mem.Allocator;const Writer = std.Io.Writer;
const Cursor = @This();
const Mode = enum { blink, static, hide };
/// Marker address used to identify our tick messages in `Msg.custom`.var tick_marker: u8 = 0;
fn tickTypeId() usize { return @intFromPtr(&tick_marker);}
/// Style applied to the cursor cell when it is visible.style: matte.Style = .none,/// Style applied to the cursor cell when blinked off (i.e. plain text).text_style: matte.Style = .none,/// How long each blink half-cycle lasts.blink_speed_ns: u64 = 530_000_000,/// When true, the cursor is currently in its hidden half-cycle.is_blinked: bool = false,mode: Mode = .blink,focused: bool = false,/// The character under the cursor. Must be a single grapheme; rendered as-is.char: []const u8 = " ",/// Set true while a chain of tick Cmds is in flight.running: bool = false,model: tea.Model = .{ .vtable = &.{ .init = vtableInit, .update = vtableUpdate, .view = vtableView, },},
fn vtableInit(model: *tea.Model) ?tea.Cmd { const self: *Cursor = @fieldParentPtr("model", model); return self.focus(model.gpa);}
fn vtableUpdate(model: *tea.Model, msg: tea.Msg) ?tea.Cmd { const self: *Cursor = @fieldParentPtr("model", model); return self.update(model.gpa, msg);}
fn vtableView(model: *tea.Model, writer: *Writer) Writer.Error!tea.View { const self: *Cursor = @fieldParentPtr("model", model); try self.view(writer); return .{};}
pub fn setMode(self: *Cursor, allocator: Allocator, mode: Mode) ?tea.Cmd { self.mode = mode; self.is_blinked = mode == .hide or !self.focused; if (mode == .blink and self.focused and !self.running) return self.startBlink(allocator); if (mode != .blink) self.running = false; return null;}
pub fn focus(self: *Cursor, allocator: Allocator) ?tea.Cmd { self.focused = true; self.is_blinked = self.mode == .hide; if (self.mode == .blink and !self.running) return self.startBlink(allocator); return null;}
pub fn blur(self: *Cursor) void { self.focused = false; self.is_blinked = true; self.running = false;}
pub fn setChar(self: *Cursor, char: []const u8) void { self.char = char;}
pub fn update(self: *Cursor, allocator: Allocator, msg: tea.Msg) ?tea.Cmd { switch (msg) { .focus => return self.focus(allocator), .blur => { self.blur(); return null; }, .custom => |c| { if (c.type_id != tickTypeId()) return null; if (!self.running or self.mode != .blink or !self.focused) { self.running = false; return null; } self.is_blinked = !self.is_blinked; return self.scheduleTick(allocator); }, else => return null, }}
pub fn view(self: *const Cursor, writer: *Writer) Writer.Error!void { if (self.is_blinked) { try util.writeInline(writer, self.text_style, self.char); return; } var s: matte.Style = self.style; s.reverse = true; try util.writeInline(writer, s, self.char);}
fn startBlink(self: *Cursor, allocator: Allocator) ?tea.Cmd { self.running = true; return self.scheduleTick(allocator);}
fn scheduleTick(self: *Cursor, allocator: Allocator) ?tea.Cmd { return tea.Cmd.tick(allocator, self.blink_speed_ns, .{ .custom = .{ .type_id = tickTypeId() }, }) catch { self.running = false; return null; };}
test "blur hides cursor" { var c: Cursor = .{}; c.blur(); try t.expect(c.is_blinked); try t.expect(!c.focused);}
test "view renders char when visible" { var buf: [16]u8 = undefined; var w: Writer = .fixed(&buf); const c: Cursor = .{ .char = "x", .is_blinked = false }; try c.view(&w); try t.expectEqualStrings("\x1b[7mx\x1b[0m", w.buffered());}
test "view renders char when blinked" { var buf: [16]u8 = undefined; var w: Writer = .fixed(&buf); const c: Cursor = .{ .char = "x", .is_blinked = true }; try c.view(&w); try t.expectEqualStrings("x", w.buffered());}