···11+const std = @import("std");
22+const testing = std.testing;
33+44+const GraphemeCache = @This();
55+66+/// the underlying storage for graphemes
77+buf: [1024 * 4]u8 = undefined,
88+99+// the start index of the next grapheme
1010+idx: usize = 0,
1111+1212+/// the cache of graphemes. This allows up to 1024 graphemes with 4 codepoints
1313+/// each
1414+grapheme_buf: [1024]Grapheme = undefined,
1515+1616+// index of our next grapheme
1717+g_idx: u21 = 0,
1818+1919+pub const UNICODE_MAX = 1_114_112;
2020+2121+const Grapheme = struct {
2222+ // codepoint is an index into the internal storage
2323+ codepoint: u21,
2424+ start: usize,
2525+ end: usize,
2626+};
2727+2828+/// put a slice of bytes in the cache as a grapheme
2929+pub fn put(self: *GraphemeCache, bytes: []const u8) !u21 {
3030+ // See if we already have these bytes. It's a likely case that if we get one
3131+ // grapheme, we'll get it again. So this will save a lot of storage and is
3232+ // most likely worth the cost as it's pretty rare
3333+ for (self.grapheme_buf) |grapheme| {
3434+ const g_bytes = self.buf[grapheme.start..grapheme.end];
3535+ if (std.mem.eql(u8, g_bytes, bytes)) {
3636+ return grapheme.codepoint;
3737+ }
3838+ }
3939+ if (self.idx + bytes.len > self.buf.len) return error.OutOfGraphemeBufferMemory;
4040+ if (self.g_idx + 1 > self.grapheme_buf.len) return error.OutOfGraphemeMemory;
4141+4242+ // copy the grapheme to our storage
4343+ @memcpy(self.buf[self.idx .. self.idx + bytes.len], bytes);
4444+4545+ const g = Grapheme{
4646+ // assign a codepoint that is always outside of valid unicode
4747+ .codepoint = self.g_idx + UNICODE_MAX + 1,
4848+ .start = self.idx,
4949+ .end = self.idx + bytes.len,
5050+ };
5151+ self.grapheme_buf[self.g_idx] = g;
5252+ self.g_idx += 1;
5353+ self.idx += bytes.len;
5454+5555+ return g.codepoint;
5656+}
5757+5858+/// get the slice of bytes for a given grapheme
5959+pub fn get(self: *GraphemeCache, cp: u21) ![]const u8 {
6060+ if (cp < (UNICODE_MAX + 1)) return error.InvalidGraphemeIndex;
6161+ const idx: usize = cp - UNICODE_MAX - 1;
6262+ if (idx > self.g_idx) return error.InvalidGraphemeIndex;
6363+ const g = self.grapheme_buf[idx];
6464+ return self.buf[g.start..g.end];
6565+}
6666+6767+test "GraphemeCache: roundtrip" {
6868+ var cache: GraphemeCache = .{};
6969+ const cp = try cache.put("abc");
7070+ const bytes = try cache.get(cp);
7171+ try testing.expectEqualStrings("abc", bytes);
7272+7373+ const cp_2 = try cache.put("abc");
7474+ try testing.expectEqual(cp, cp_2);
7575+7676+ const cp_3 = try cache.put("def");
7777+ try testing.expectEqual(cp + 1, cp_3);
7878+}
+1-2
src/Key.zig
···1111 num_lock: bool = false,
1212};
13131414-/// the unicode codepoint of the key event. This can be greater than the maximum
1515-/// allowable unicode codepoint for special keys
1414+/// the unicode codepoint of the key event.
1615codepoint: u21,
17161817/// the text generated from the key event, if any
+6-1
src/Tty.zig
···143143 switch (event) {
144144 .key_press => |key| {
145145 if (@hasField(EventType, "key_press")) {
146146- vx.postEvent(.{ .key_press = key });
146146+ // HACK: yuck. there has to be a better way
147147+ var mut_key = key;
148148+ if (key.text) |text| {
149149+ mut_key.codepoint = try vx.g_cache.put(text);
150150+ }
151151+ vx.postEvent(.{ .key_press = mut_key });
147152 }
148153 },
149154 .focus_in => {