handle upper case in input buffers

This commit is contained in:
Emil Lerch 2026-06-28 07:27:34 -07:00
parent 8986537d93
commit 8b85dcb9ea
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -80,12 +80,9 @@ pub fn handleKey(buf: []u8, len: *usize, key: vaxis.Key) Result {
len.* = 0;
return .edited;
}
// Accept printable ASCII (letters, digits, common punctuation).
if (key.codepoint < std.math.maxInt(u7) and std.ascii.isPrint(@intCast(key.codepoint)) and len.* < buf.len) {
buf[len.*] = @intCast(key.codepoint);
len.* += 1;
return .edited;
}
// Printable input. Prefer the terminal-resolved text so Shift /
// Caps Lock produce the right case (see `appendPrintable`).
if (appendPrintable(buf, len, key)) return .edited;
return .ignored;
}
@ -155,12 +152,45 @@ pub fn handleKeyMulti(buf: []u8, len: *usize, key: vaxis.Key) MultiResult {
len.* = 0;
return .edited;
}
if (appendPrintable(buf, len, key)) return .edited;
return .ignored;
}
/// Append the character a key produced to `buf`, returning true on
/// success. Callers handle the special keys (Esc, Enter, Backspace,
/// Ctrl+*) before this runs, so anything reaching here is ordinary
/// text input.
///
/// We prefer `key.text` - the terminal's resolved text for the
/// event - over the raw codepoint. Under the Kitty keyboard protocol
/// (which vaxis turns on), a Shift+a press reports `codepoint = 'a'`
/// (the base-layout key) with `text = "A"`; keying off the codepoint
/// alone silently downcases everything and turns shifted symbols
/// (`!@#`) back into their digits. Only single printable-ASCII text
/// bytes are taken: these buffers are ASCII (ticker symbols, ack-note
/// reasoning), so a stray multibyte grapheme or control sequence
/// shouldn't land in them. When no text is reported (legacy terminal,
/// no Kitty protocol) we fall back to the codepoint, which already
/// carries the shifted value in that mode.
fn appendPrintable(buf: []u8, len: *usize, key: vaxis.Key) bool {
if (key.text) |text| {
if (text.len == 1 and std.ascii.isPrint(text[0]) and len.* < buf.len) {
buf[len.*] = text[0];
len.* += 1;
return true;
}
// Text was reported but isn't a single printable ASCII byte
// (multibyte grapheme, control char, or the buffer is full).
// Do NOT fall through to the codepoint path: that would
// re-append a downcased/duplicate byte.
return false;
}
if (key.codepoint < std.math.maxInt(u7) and std.ascii.isPrint(@intCast(key.codepoint)) and len.* < buf.len) {
buf[len.*] = @intCast(key.codepoint);
len.* += 1;
return .edited;
return true;
}
return .ignored;
return false;
}
// Tests
@ -194,6 +224,49 @@ test "handleKey: printable ASCII appends and increments len" {
try testing.expectEqual(@as(u8, 'x'), buf[0]);
}
test "handleKey: Shift+letter appends the uppercase text, not the base codepoint" {
// Kitty keyboard protocol reports the base-layout key in
// `codepoint` and the resolved character in `text`. Shift+a must
// land 'A', not 'a'.
var buf: [16]u8 = undefined;
var len: usize = 0;
const result = handleKey(&buf, &len, .{ .codepoint = 'a', .text = "A", .mods = .{ .shift = true } });
try testing.expectEqual(Result.edited, result);
try testing.expectEqual(@as(usize, 1), len);
try testing.expectEqual(@as(u8, 'A'), buf[0]);
}
test "handleKey: shifted symbol appends the symbol, not the digit" {
// Shift+1 => '!' : codepoint stays '1', text is "!".
var buf: [16]u8 = undefined;
var len: usize = 0;
const result = handleKey(&buf, &len, .{ .codepoint = '1', .text = "!", .mods = .{ .shift = true } });
try testing.expectEqual(Result.edited, result);
try testing.expectEqual(@as(u8, '!'), buf[0]);
}
test "handleKey: legacy terminal (no text) falls back to the codepoint" {
// Without the Kitty protocol, vaxis reports the shifted value
// directly in `codepoint` and leaves `text` null.
var buf: [16]u8 = undefined;
var len: usize = 0;
const result = handleKey(&buf, &len, .{ .codepoint = 'A' });
try testing.expectEqual(Result.edited, result);
try testing.expectEqual(@as(u8, 'A'), buf[0]);
}
test "handleKey: multibyte/control text is not appended" {
// A non-ASCII grapheme or control sequence in `text` must not
// corrupt the ASCII buffer, and must not fall through to the
// codepoint path.
var buf: [16]u8 = undefined;
var len: usize = 0;
try testing.expectEqual(Result.ignored, handleKey(&buf, &len, .{ .codepoint = 0x2014, .text = "\u{2014}" }));
try testing.expectEqual(@as(usize, 0), len);
try testing.expectEqual(Result.ignored, handleKey(&buf, &len, .{ .codepoint = vaxis.Key.tab, .text = "\t" }));
try testing.expectEqual(@as(usize, 0), len);
}
test "handleKey: backspace decrements len" {
var buf: [16]u8 = undefined;
var len: usize = 3;
@ -296,6 +369,22 @@ test "handleKeyMulti: printable ASCII appends" {
try testing.expectEqual(@as(u8, 'x'), buf[0]);
}
test "handleKeyMulti: Shift+letter appends uppercase text (the ack-note bug)" {
var buf: [64]u8 = undefined;
var len: usize = 0;
const result = handleKeyMulti(&buf, &len, .{ .codepoint = 'a', .text = "A", .mods = .{ .shift = true } });
try testing.expectEqual(MultiResult.edited, result);
try testing.expectEqual(@as(u8, 'A'), buf[0]);
}
test "handleKeyMulti: legacy terminal (no text) falls back to the codepoint" {
var buf: [64]u8 = undefined;
var len: usize = 0;
const result = handleKeyMulti(&buf, &len, .{ .codepoint = 'Z' });
try testing.expectEqual(MultiResult.edited, result);
try testing.expectEqual(@as(u8, 'Z'), buf[0]);
}
test "handleKeyMulti: backspace decrements len" {
var buf: [64]u8 = undefined;
var len: usize = 3;