Something went wrong. Try again.
A charm-like tui library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124const std = @import("std");const util = @import("util.zig");const cw = @import("width.zig");
/// Returns the terminal cell width of `string`, ignoring ANSI escape/// sequences. Wide code points (CJK, fullwidth, emoji) count as 2 cells and/// combining marks as 0; see [width.zig] for coverage caveats.pub fn cellWidth(string: []const u8) u16 { var width: u16 = 0; var i: usize = 0; while (i < string.len) { const b = string[i]; if (b == 0x1b) { i = skipEscape(string, i); continue; } const len = std.unicode.utf8ByteSequenceLength(b) catch { // Invalid lead byte: count one cell, advance one byte. width = util.satAdd(width, 1); i += 1; continue; }; if (i + len > string.len) { width = util.satAdd(width, 1); i += 1; continue; } const cp = std.unicode.utf8Decode(string[i .. i + len]) catch { width = util.satAdd(width, 1); i += 1; continue; }; width = util.satAdd(width, cw.codepointWidth(cp)); i += len; } return width;}
pub fn maxLineWidth(string: []const u8) u16 { var max: u16 = 0; var start: usize = 0; var i: usize = 0; while (i < string.len) : (i += 1) { if (string[i] == '\n') { const w = cellWidth(string[start..i]); if (w > max) max = w; start = i + 1; } } if (start < string.len) { const w = cellWidth(string[start..]); if (w > max) max = w; } return max;}
/// Returns the number of lines in `string`, mirroring lipgloss `Height`:/// `count('\n') + 1`. Empty input is one line; a trailing `\n` adds a line.pub fn lineCount(string: []const u8) u16 { const nls = std.mem.count(u8, string, "\n"); return @intCast(@min(nls + 1, util.max_dim));}
fn skipEscape(string: []const u8, index: usize) usize { if (index + 1 >= string.len) return string.len; switch (string[index + 1]) { '[' => { var j = index + 2; while (j < string.len and (string[j] < 0x40 or string[j] > 0x7e)) : (j += 1) {} return if (j < string.len) j + 1 else string.len; }, ']' => { var j = index + 2; while (j < string.len) : (j += 1) { if (string[j] == 0x07) return j + 1; if (string[j] == 0x1b and j + 1 < string.len and string[j + 1] == '\\') return j + 2; } return string.len; }, else => return index + 2, }}
test "cellWidth plain ASCII" { try std.testing.expectEqual(@as(u16, 5), cellWidth("hello"));}
test "cellWidth strips CSI" { try std.testing.expectEqual(@as(u16, 5), cellWidth("\x1b[31mhello\x1b[0m"));}
test "cellWidth control chars are zero" { try std.testing.expectEqual(@as(u16, 0), cellWidth("\x07"));}
test "cellWidth wide CJK" { try std.testing.expectEqual(@as(u16, 4), cellWidth("世界"));}
test "cellWidth emoji" { try std.testing.expectEqual(@as(u16, 2), cellWidth("🎉"));}
test "cellWidth combining mark is zero width" { // "e" + U+0301 combining acute accent renders in a single cell. try std.testing.expectEqual(@as(u16, 1), cellWidth("e\u{0301}"));}
test "cellWidth mixed ascii and wide" { try std.testing.expectEqual(@as(u16, 7), cellWidth("ab世界c"));}
test "maxLineWidth multi-line" { try std.testing.expectEqual(@as(u16, 5), maxLineWidth("hi\nworld\na"));}
test "lineCount" { try std.testing.expectEqual(@as(u16, 1), lineCount("")); try std.testing.expectEqual(@as(u16, 1), lineCount("hi")); try std.testing.expectEqual(@as(u16, 2), lineCount("hi\nthere")); try std.testing.expectEqual(@as(u16, 3), lineCount("hi\nthere\n")); try std.testing.expectEqual(@as(u16, 3), lineCount("a\n\nb"));}