const 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")); }