paginator
Which page of many: dots or numbers, and keys to turn the page
termancy tui add paginator
No recording here. zig build site-gifs records one from docs/tapes/paginator.tape when there is one; termancy ui demo paginator runs it, if it has a demo.
Which page of many you are on, as dots (●○○) or numbers (2/7), and the keys to
turn the page. Pair it with a list of items: bounds gives the slice on the page.
var pages: Paginator = .{ .per_page = 10 };
pages.setTotal(items.len);
_ = pages.update(msg); // left/right, h/l, pgup/pgdown
const range = pages.bounds(items.len);
for (items[range.start..range.end]) |item| ...;
try pages.view(w);
paginator.zig
//! Which page of many you are on, as dots (`●○○`) or numbers (`2/7`), and the keys to
//! turn the page. Pair it with a list of items: `bounds` gives the slice on the page.
//!
//! ```zig
//! var pages: Paginator = .{ .per_page = 10 };
//! pages.setTotal(items.len);
//! _ = pages.update(msg); // left/right, h/l, pgup/pgdown
//! const range = pages.bounds(items.len);
//! for (items[range.start..range.end]) |item| ...;
//! try pages.view(w);
//! ```
const std = @import("std");
const tui = @import("tui");
const Writer = std.Io.Writer;
const Paginator = @This();
kind: Kind = .dots,
/// The page shown, from 0.
page: usize = 0,
per_page: usize = 10,
/// How many pages there are (at least one).
pages: usize = 1,
active_dot: []const u8 = "●",
inactive_dot: []const u8 = "○",
/// Turn pages with the keys in `update`.
keys: bool = true,
styles: Styles = .{},
pub const Kind = enum { dots, arabic };
pub const Styles = struct {
active: tui.Style = .{},
inactive: tui.Style = .{ .dim = true },
arabic: tui.Style = .{},
};
/// Sets `pages` for `items` items, keeping the page on the last one at most.
pub fn setTotal(p: *Paginator, items: usize) void {
const per = @max(p.per_page, 1);
p.pages = @max(1, (items + per - 1) / per);
p.page = @min(p.page, p.pages - 1);
}
/// The range of item indices on the page, of `items` in all.
pub fn bounds(p: *const Paginator, items: usize) struct { start: usize, end: usize } {
const start = @min(p.page * p.per_page, items);
return .{ .start = start, .end = @min(start + p.per_page, items) };
}
pub fn onFirstPage(p: *const Paginator) bool {
return p.page == 0;
}
pub fn onLastPage(p: *const Paginator) bool {
return p.page + 1 >= p.pages;
}
pub fn nextPage(p: *Paginator) void {
if (!p.onLastPage()) p.page += 1;
}
pub fn previousPage(p: *Paginator) void {
if (!p.onFirstPage()) p.page -= 1;
}
pub fn update(p: *Paginator, msg: tui.Msg) tui.Cmd {
if (!p.keys or msg != .key) return .none;
const key = msg.key;
if (key.matches("right") or key.matches("l") or key.matches("pgdown")) p.nextPage();
if (key.matches("left") or key.matches("h") or key.matches("pgup")) p.previousPage();
return .none;
}
pub fn view(p: *const Paginator, w: *Writer) Writer.Error!void {
switch (p.kind) {
.arabic => {
var buffer: [48]u8 = undefined;
const pages = .{ p.page + 1, p.pages };
const text = std.fmt.bufPrint(&buffer, "{d}/{d}", pages) catch "";
try p.styles.arabic.paint(w, text);
},
.dots => for (0..p.pages) |i| {
if (i == p.page) {
try p.styles.active.paint(w, p.active_dot);
} else {
try p.styles.inactive.paint(w, p.inactive_dot);
}
},
}
}
const testing = std.testing;
fn viewOf(p: *const Paginator) ![]u8 {
var out: Writer.Allocating = .init(testing.allocator);
errdefer out.deinit();
try p.view(&out.writer);
return out.toOwnedSlice();
}
test "pages of items, turned with keys" {
var p: Paginator = .{ .per_page = 3, .styles = .{ .inactive = .{} } };
p.setTotal(7);
try testing.expectEqual(3, p.pages);
const first = try viewOf(&p);
defer testing.allocator.free(first);
try testing.expectEqualStrings("●○○", first);
_ = p.update(.{ .key = try .parse("right") });
_ = p.update(.{ .key = try .parse("l") });
_ = p.update(.{ .key = try .parse("l") });
try testing.expectEqual(2, p.page);
const range = p.bounds(7);
try testing.expectEqual(6, range.start);
try testing.expectEqual(7, range.end);
p.kind = .arabic;
const numbers = try viewOf(&p);
defer testing.allocator.free(numbers);
try testing.expectEqualStrings("3/3", numbers);
p.setTotal(2);
try testing.expectEqual(0, p.page);
}