//! Countdown timer. Ticks down by `interval_ns` until reaching zero. View //! renders remaining time. `timed_out` flips true on the tick that reaches //! zero; the chain stops there. const std = @import("std"); const matcha = @import("matcha"); const Allocator = std.mem.Allocator; const Writer = std.Io.Writer; const Cmd = matcha.Cmd; const Msg = matcha.Msg; const Timer = @This(); pub const default_interval_ns: u64 = std.time.ns_per_s; var tick_marker: u8 = 0; fn tickTypeId() usize { return @intFromPtr(&tick_marker); } model: matcha.Model = .{ .vtable = &vtable }, /// Time remaining until the timer expires. timeout_ns: u64, interval_ns: u64 = default_interval_ns, running: bool = false, const vtable: matcha.Model.VTable = .{ .init = vtableInit, .update = vtableUpdate, .view = vtableView, }; fn vtableInit(m: *matcha.Model) ?Cmd { const self: *Timer = @fieldParentPtr("model", m); return self.start(m.gpa); } fn vtableUpdate(m: *matcha.Model, msg: Msg) ?Cmd { const self: *Timer = @fieldParentPtr("model", m); return self.update(m.gpa, msg); } fn vtableView(m: *matcha.Model, w: *Writer) Writer.Error!matcha.View { const self: *Timer = @fieldParentPtr("model", m); try self.view(w); return .{}; } pub fn timedOut(self: *const Timer) bool { return self.timeout_ns == 0; } pub fn isRunning(self: *const Timer) bool { return self.running and !self.timedOut(); } pub fn start(self: *Timer, allocator: Allocator) ?Cmd { if (self.timedOut() or self.running) return null; self.running = true; return self.tick(allocator); } pub fn stop(self: *Timer) void { self.running = false; } pub fn toggle(self: *Timer, allocator: Allocator) ?Cmd { if (self.running) { self.stop(); return null; } return self.start(allocator); } pub fn update(self: *Timer, allocator: Allocator, msg: Msg) ?Cmd { switch (msg) { .custom => |c| { if (c.type_id != tickTypeId()) return null; if (!self.isRunning()) return null; self.timeout_ns -|= self.interval_ns; if (self.timedOut()) return null; return self.tick(allocator); }, else => return null, } } pub fn view(self: *const Timer, w: *Writer) Writer.Error!void { try formatDuration(w, self.timeout_ns); } fn tick(self: *Timer, allocator: Allocator) ?Cmd { return Cmd.tick(allocator, self.interval_ns, .{ .custom = .{ .type_id = tickTypeId() }, }) catch { self.running = false; return null; }; } fn formatDuration(w: *Writer, ns: u64) Writer.Error!void { if (ns == 0) { try w.writeAll("0s"); return; } var rem = ns; const h = rem / std.time.ns_per_hour; rem -= h * std.time.ns_per_hour; const m = rem / std.time.ns_per_min; rem -= m * std.time.ns_per_min; const s = rem / std.time.ns_per_s; rem -= s * std.time.ns_per_s; const ms = rem / std.time.ns_per_ms; if (h > 0) try w.print("{d}h", .{h}); if (h > 0 or m > 0) try w.print("{d}m", .{m}); if (ms == 0) { try w.print("{d}s", .{s}); } else { try w.print("{d}.{d:0>3}s", .{ s, ms }); } } test "timed out timer" { const t: Timer = .{ .timeout_ns = 0 }; try std.testing.expect(t.timedOut()); try std.testing.expect(!t.isRunning()); } test "view renders timeout" { var buf: [32]u8 = undefined; var w: Writer = .fixed(&buf); const t: Timer = .{ .timeout_ns = 10 * std.time.ns_per_s }; try t.view(&w); try std.testing.expectEqualStrings("10s", w.buffered()); } test "stop pauses" { var t: Timer = .{ .timeout_ns = 5 * std.time.ns_per_s }; t.stop(); try std.testing.expect(!t.running); try std.testing.expect(!t.isRunning()); }