diff --git a/src/Parser.zig b/src/Parser.zig index 64fab62..a6a3751 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -281,19 +281,18 @@ pub fn parse(self: *Parser, input: []const u8) !Result { 'u' => blk: { if (seq.private_indicator) |priv| { // response to our kitty query - // TODO: kitty query handling if (priv == '?') { return .{ .event = .cap_kitty_keyboard, .n = i + 1, }; + } else { + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); + return .{ + .event = null, + .n = i + 1, + }; } - - log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); - return .{ - .event = null, - .n = i + 1, - }; } if (seq.param_idx == 0) { log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); @@ -313,6 +312,51 @@ pub fn parse(self: *Parser, input: []const u8) !Result { 'O' => { // focus out return .{ .event = .focus_out, .n = i + 1 }; }, + 'y' => { // DECRQM response + const priv = seq.private_indicator orelse { + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); + return .{ .event = null, .n = i + 1 }; + }; + if (priv != '?') { + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); + return .{ .event = null, .n = i + 1 }; + } + const intm = seq.intermediate orelse { + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); + return .{ .event = null, .n = i + 1 }; + }; + if (intm != '$') { + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); + return .{ .event = null, .n = i + 1 }; + } + if (seq.param_idx != 2) { + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); + return .{ .event = null, .n = i + 1 }; + } + // We'll get two fields, the first is the mode + // we requested, the second is the status of the + // mode + // 0: not recognize + // 1: set + // 2: reset + // 3: permanently set + // 4: permanently reset + switch (seq.params[0]) { + 2027 => { + switch (seq.params[1]) { + 0, 4 => return .{ .event = null, .n = i + 1 }, + else => return .{ .event = .cap_unicode, .n = i + 1 }, + } + }, + 2031 => {}, + else => { + log.warn("unhandled DECRPM: CSI {s}", .{input[start + 1 .. i + 1]}); + return .{ .event = null, .n = i + 1 }; + }, + } + log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); + return .{ .event = null, .n = i + 1 }; + }, else => { log.warn("unhandled csi: CSI {s}", .{input[start + 1 .. i + 1]}); return .{ diff --git a/src/Tty.zig b/src/Tty.zig index 0be97e1..0e2baa6 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -187,6 +187,11 @@ pub fn run( vx.postEvent(.cap_rgb); } }, + .cap_unicode => { + if (@hasField(EventType, "cap_unicode")) { + vx.postEvent(.cap_unicode); + } + }, } } } diff --git a/src/event.zig b/src/event.zig index 441d1a0..7604fe1 100644 --- a/src/event.zig +++ b/src/event.zig @@ -12,4 +12,5 @@ pub const Event = union(enum) { // these are delivered as discovered terminal capabilities cap_kitty_keyboard, cap_rgb, + cap_unicode, }; diff --git a/src/gwidth.zig b/src/gwidth.zig new file mode 100644 index 0000000..3d6d35f --- /dev/null +++ b/src/gwidth.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const unicode = std.unicode; +const testing = std.testing; +const ziglyph = @import("ziglyph"); + +/// the method to use when calculating the width of a grapheme +pub const Method = enum { + unicode, + wcwidth, +}; + +/// returns the width of the provided string, as measured by the method chosen +pub fn gwidth(str: []const u8, method: Method) !usize { + switch (method) { + .unicode => { + return try ziglyph.display_width.strWidth(str, .half); + }, + .wcwidth => { + var total: usize = 0; + const utf8 = try unicode.Utf8View.init(str); + var iter = utf8.iterator(); + + while (iter.nextCodepoint()) |cp| { + const w = ziglyph.display_width.codePointWidth(cp, .half); + if (w < 0) continue; + total += @intCast(w); + } + return total; + }, + } +} + +test "gwidth: a" { + try testing.expectEqual(1, try gwidth("a", .unicode)); + try testing.expectEqual(1, try gwidth("a", .wcwidth)); +} + +test "gwidth: emoji with ZWJ" { + try testing.expectEqual(2, try gwidth("👩‍🚀", .unicode)); + try testing.expectEqual(4, try gwidth("👩‍🚀", .wcwidth)); +} + +test "gwidth: emoji with skin tone selector" { + try testing.expectEqual(2, try gwidth("👋🏿", .unicode)); + try testing.expectEqual(4, try gwidth("👋🏿", .wcwidth)); +} + +test "gwidth: invalid string" { + try testing.expectError(error.InvalidUtf8, gwidth("\xc3\x28", .unicode)); + try testing.expectError(error.InvalidUtf8, gwidth("\xc3\x28", .wcwidth)); +} diff --git a/src/main.zig b/src/main.zig index e9b140a..0fcbb76 100644 --- a/src/main.zig +++ b/src/main.zig @@ -27,6 +27,7 @@ test { _ = @import("cell.zig"); _ = @import("ctlseqs.zig"); _ = @import("event.zig"); + _ = @import("gwidth.zig"); _ = @import("queue.zig"); _ = @import("vaxis.zig"); }