Add iOS QWERTZ layout facts
This commit is contained in:
+365
@@ -0,0 +1,365 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const input = @import("input.zig");
|
||||||
|
|
||||||
|
// Source-patched keyboard layout facts, intentionally separate from commands.
|
||||||
|
// req: input/004, input/005, governance/001, testing/001, testing/002, testing/003, testing/004
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = ios_qwertz;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ProfileId = enum {
|
||||||
|
ios_qwertz,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Kind = enum {
|
||||||
|
letter,
|
||||||
|
whitespace,
|
||||||
|
control,
|
||||||
|
punctuation,
|
||||||
|
unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Fact = struct {
|
||||||
|
raw: []const u8,
|
||||||
|
kind: Kind,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Profile = struct {
|
||||||
|
id: ProfileId,
|
||||||
|
name: []const u8,
|
||||||
|
facts: []const Fact,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Classified = struct {
|
||||||
|
event: input.Event,
|
||||||
|
kind: Kind,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TraceResult = union(enum) {
|
||||||
|
ok,
|
||||||
|
diverged: Divergence,
|
||||||
|
|
||||||
|
pub fn deinit(self: TraceResult, allocator: std.mem.Allocator) void {
|
||||||
|
switch (self) {
|
||||||
|
.ok => {},
|
||||||
|
.diverged => |diverged| allocator.free(diverged.message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Divergence = struct {
|
||||||
|
step: usize,
|
||||||
|
message: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const LayoutTraceRecorder = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
profile: *const Profile,
|
||||||
|
lines: std.ArrayList(u8) = .empty,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, profile: *const Profile) LayoutTraceRecorder {
|
||||||
|
return .{ .allocator = allocator, .profile = profile };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *LayoutTraceRecorder) void {
|
||||||
|
self.lines.deinit(self.allocator);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capture(self: *LayoutTraceRecorder, raw: []const u8) !void {
|
||||||
|
const label = try classificationLabelAlloc(self.allocator, classifyRaw(self.profile, raw));
|
||||||
|
defer self.allocator.free(label);
|
||||||
|
try appendHex(self.allocator, &self.lines, raw);
|
||||||
|
try self.lines.appendSlice(self.allocator, " => ");
|
||||||
|
try self.lines.appendSlice(self.allocator, @tagName(self.profile.id));
|
||||||
|
try self.lines.append(self.allocator, ':');
|
||||||
|
try self.lines.appendSlice(self.allocator, label);
|
||||||
|
try self.lines.append(self.allocator, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(self: *const LayoutTraceRecorder) []const u8 {
|
||||||
|
return self.lines.items;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ios_qwertz = Profile{
|
||||||
|
.id = .ios_qwertz,
|
||||||
|
.name = "iOS QWERTZ",
|
||||||
|
.facts = &ios_qwertz_facts,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ios_qwertz_facts = [_]Fact{
|
||||||
|
.{ .raw = "a", .kind = .letter },
|
||||||
|
.{ .raw = "b", .kind = .letter },
|
||||||
|
.{ .raw = "c", .kind = .letter },
|
||||||
|
.{ .raw = "d", .kind = .letter },
|
||||||
|
.{ .raw = "e", .kind = .letter },
|
||||||
|
.{ .raw = "f", .kind = .letter },
|
||||||
|
.{ .raw = "g", .kind = .letter },
|
||||||
|
.{ .raw = "h", .kind = .letter },
|
||||||
|
.{ .raw = "i", .kind = .letter },
|
||||||
|
.{ .raw = "j", .kind = .letter },
|
||||||
|
.{ .raw = "k", .kind = .letter },
|
||||||
|
.{ .raw = "l", .kind = .letter },
|
||||||
|
.{ .raw = "m", .kind = .letter },
|
||||||
|
.{ .raw = "n", .kind = .letter },
|
||||||
|
.{ .raw = "o", .kind = .letter },
|
||||||
|
.{ .raw = "p", .kind = .letter },
|
||||||
|
.{ .raw = "q", .kind = .letter },
|
||||||
|
.{ .raw = "r", .kind = .letter },
|
||||||
|
.{ .raw = "s", .kind = .letter },
|
||||||
|
.{ .raw = "t", .kind = .letter },
|
||||||
|
.{ .raw = "u", .kind = .letter },
|
||||||
|
.{ .raw = "v", .kind = .letter },
|
||||||
|
.{ .raw = "w", .kind = .letter },
|
||||||
|
.{ .raw = "x", .kind = .letter },
|
||||||
|
.{ .raw = "y", .kind = .letter },
|
||||||
|
.{ .raw = "z", .kind = .letter },
|
||||||
|
.{ .raw = "ä", .kind = .letter },
|
||||||
|
.{ .raw = "ö", .kind = .letter },
|
||||||
|
.{ .raw = "ü", .kind = .letter },
|
||||||
|
.{ .raw = "ß", .kind = .letter },
|
||||||
|
|
||||||
|
.{ .raw = ".", .kind = .punctuation },
|
||||||
|
.{ .raw = ",", .kind = .punctuation },
|
||||||
|
.{ .raw = ":", .kind = .punctuation },
|
||||||
|
.{ .raw = ";", .kind = .punctuation },
|
||||||
|
.{ .raw = "-", .kind = .punctuation },
|
||||||
|
.{ .raw = "_", .kind = .punctuation },
|
||||||
|
.{ .raw = "/", .kind = .punctuation },
|
||||||
|
.{ .raw = "\\", .kind = .punctuation },
|
||||||
|
.{ .raw = "'", .kind = .punctuation },
|
||||||
|
.{ .raw = "\"", .kind = .punctuation },
|
||||||
|
.{ .raw = "`", .kind = .punctuation },
|
||||||
|
.{ .raw = "~", .kind = .punctuation },
|
||||||
|
.{ .raw = "(", .kind = .punctuation },
|
||||||
|
.{ .raw = ")", .kind = .punctuation },
|
||||||
|
.{ .raw = "[", .kind = .punctuation },
|
||||||
|
.{ .raw = "]", .kind = .punctuation },
|
||||||
|
.{ .raw = "{", .kind = .punctuation },
|
||||||
|
.{ .raw = "}", .kind = .punctuation },
|
||||||
|
.{ .raw = "<", .kind = .punctuation },
|
||||||
|
.{ .raw = ">", .kind = .punctuation },
|
||||||
|
.{ .raw = "=", .kind = .punctuation },
|
||||||
|
.{ .raw = "+", .kind = .punctuation },
|
||||||
|
.{ .raw = "*", .kind = .punctuation },
|
||||||
|
.{ .raw = "!", .kind = .punctuation },
|
||||||
|
.{ .raw = "?", .kind = .punctuation },
|
||||||
|
.{ .raw = "@", .kind = .punctuation },
|
||||||
|
.{ .raw = "#", .kind = .punctuation },
|
||||||
|
.{ .raw = "$", .kind = .punctuation },
|
||||||
|
.{ .raw = "%", .kind = .punctuation },
|
||||||
|
.{ .raw = "&", .kind = .punctuation },
|
||||||
|
.{ .raw = "|", .kind = .punctuation },
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn classifyRaw(profile: *const Profile, raw: []const u8) Classified {
|
||||||
|
return classifyEvent(profile, input.normalize(raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn classifyEvent(profile: *const Profile, event: input.Event) Classified {
|
||||||
|
const kind: Kind = switch (event) {
|
||||||
|
.key => |key| switch (key) {
|
||||||
|
.space, .tab => .whitespace,
|
||||||
|
.enter, .backspace, .escape, .arrow_left, .arrow_right, .arrow_up, .arrow_down => .control,
|
||||||
|
},
|
||||||
|
.text => |text| lookupFact(profile, text) orelse .unknown,
|
||||||
|
.unknown => .unknown,
|
||||||
|
};
|
||||||
|
return .{ .event = event, .kind = kind };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replayLayoutTrace(allocator: std.mem.Allocator, profile: *const Profile, trace: []const u8) !TraceResult {
|
||||||
|
var step: usize = 0;
|
||||||
|
var lines = std.mem.splitScalar(u8, trace, '\n');
|
||||||
|
while (lines.next()) |line| {
|
||||||
|
if (line.len == 0) continue;
|
||||||
|
step += 1;
|
||||||
|
const separator = std.mem.indexOf(u8, line, " => ") orelse return divergence(allocator, step, "layout trace line is missing separator");
|
||||||
|
const raw_hex = line[0..separator];
|
||||||
|
const expected = line[separator + 4 ..];
|
||||||
|
var raw = std.ArrayList(u8).empty;
|
||||||
|
defer raw.deinit(allocator);
|
||||||
|
parseHexInto(allocator, &raw, raw_hex) catch |err| switch (err) {
|
||||||
|
error.InvalidHex => return divergence(allocator, step, "layout trace line has invalid hex bytes"),
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
const actual = try classificationLabelWithProfileAlloc(allocator, profile, classifyRaw(profile, raw.items));
|
||||||
|
defer allocator.free(actual);
|
||||||
|
if (!std.mem.eql(u8, expected, actual)) {
|
||||||
|
return divergenceFmt(allocator, step, "expected layout event '{s}' but got '{s}'", .{ expected, actual });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookupFact(profile: *const Profile, text: []const u8) ?Kind {
|
||||||
|
for (profile.facts) |fact| {
|
||||||
|
if (std.mem.eql(u8, text, fact.raw)) return fact.kind;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classificationLabelWithProfileAlloc(allocator: std.mem.Allocator, profile: *const Profile, classified: Classified) ![]u8 {
|
||||||
|
const label = try classificationLabelAlloc(allocator, classified);
|
||||||
|
defer allocator.free(label);
|
||||||
|
return std.fmt.allocPrint(allocator, "{s}:{s}", .{ @tagName(profile.id), label });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classificationLabelAlloc(allocator: std.mem.Allocator, classified: Classified) ![]u8 {
|
||||||
|
return switch (classified.event) {
|
||||||
|
.key => |key| std.fmt.allocPrint(allocator, "{s}:key:{s}", .{ @tagName(classified.kind), @tagName(key) }),
|
||||||
|
.text => |text| std.fmt.allocPrint(allocator, "{s}:text:{s}", .{ @tagName(classified.kind), text }),
|
||||||
|
.unknown => |raw| unknownLabelAlloc(allocator, raw),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unknownLabelAlloc(allocator: std.mem.Allocator, raw: []const u8) ![]u8 {
|
||||||
|
var out = std.ArrayList(u8).empty;
|
||||||
|
errdefer out.deinit(allocator);
|
||||||
|
try out.appendSlice(allocator, "unknown:raw:");
|
||||||
|
try appendHex(allocator, &out, raw);
|
||||||
|
return out.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendHex(allocator: std.mem.Allocator, out: *std.ArrayList(u8), bytes: []const u8) !void {
|
||||||
|
const alphabet = "0123456789abcdef";
|
||||||
|
for (bytes) |byte| {
|
||||||
|
try out.append(allocator, alphabet[byte >> 4]);
|
||||||
|
try out.append(allocator, alphabet[byte & 0x0f]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseHexInto(allocator: std.mem.Allocator, out: *std.ArrayList(u8), hex: []const u8) !void {
|
||||||
|
if (hex.len % 2 != 0) return error.InvalidHex;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < hex.len) : (i += 2) {
|
||||||
|
const high = hexNibble(hex[i]) orelse return error.InvalidHex;
|
||||||
|
const low = hexNibble(hex[i + 1]) orelse return error.InvalidHex;
|
||||||
|
try out.append(allocator, (high << 4) | low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hexNibble(byte: u8) ?u8 {
|
||||||
|
return switch (byte) {
|
||||||
|
'0'...'9' => byte - '0',
|
||||||
|
'a'...'f' => byte - 'a' + 10,
|
||||||
|
'A'...'F' => byte - 'A' + 10,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn divergence(allocator: std.mem.Allocator, step: usize, message: []const u8) !TraceResult {
|
||||||
|
return .{ .diverged = .{ .step = step, .message = try allocator.dupe(u8, message) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn divergenceFmt(allocator: std.mem.Allocator, step: usize, comptime fmt: []const u8, args: anytype) !TraceResult {
|
||||||
|
return .{ .diverged = .{ .step = step, .message = try std.fmt.allocPrint(allocator, fmt, args) } };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectKind(raw: []const u8, expected: Kind) !void {
|
||||||
|
try std.testing.expectEqual(expected, classifyRaw(&ios_qwertz, raw).kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectReplayOk(result: TraceResult) !void {
|
||||||
|
switch (result) {
|
||||||
|
.ok => {},
|
||||||
|
.diverged => return error.ExpectedTraceOk,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: iOS QWERTZ classifies letters including QWERTZ y and z" {
|
||||||
|
try expectKind("a", .letter);
|
||||||
|
try expectKind("m", .letter);
|
||||||
|
try expectKind("z", .letter);
|
||||||
|
try expectKind("y", .letter);
|
||||||
|
try expectKind("ä", .letter);
|
||||||
|
try expectKind("ö", .letter);
|
||||||
|
try expectKind("ü", .letter);
|
||||||
|
try expectKind("ß", .letter);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: iOS QWERTZ classifies whitespace and editing controls without commands" {
|
||||||
|
try expectKind(" ", .whitespace);
|
||||||
|
try expectKind("\t", .whitespace);
|
||||||
|
try expectKind("\r", .control);
|
||||||
|
try expectKind("\x7f", .control);
|
||||||
|
try expectKind("\x1b[D", .control);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: iOS QWERTZ classifies key coding punctuation" {
|
||||||
|
inline for (.{ ".", ",", ":", ";", "-", "_", "/", "\\", "'", "\"", "`", "~", "(", ")", "[", "]", "{", "}", "<", ">", "=", "+", "*", "!", "?", "@", "#", "$", "%", "&", "|" }) |raw| {
|
||||||
|
try expectKind(raw, .punctuation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: iOS QWERTZ layout trace records raw bytes and replays" {
|
||||||
|
var recorder = LayoutTraceRecorder.init(std.testing.allocator, &ios_qwertz);
|
||||||
|
defer recorder.deinit();
|
||||||
|
|
||||||
|
try recorder.capture("z");
|
||||||
|
try recorder.capture("y");
|
||||||
|
try recorder.capture(" ");
|
||||||
|
try recorder.capture("\r");
|
||||||
|
try recorder.capture("\x7f");
|
||||||
|
try recorder.capture("{");
|
||||||
|
try recorder.capture("|");
|
||||||
|
|
||||||
|
const result = try replayLayoutTrace(std.testing.allocator, &ios_qwertz, recorder.text());
|
||||||
|
defer result.deinit(std.testing.allocator);
|
||||||
|
try expectReplayOk(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: profile labels are facts, not editor command intents" {
|
||||||
|
var recorder = LayoutTraceRecorder.init(std.testing.allocator, &ios_qwertz);
|
||||||
|
defer recorder.deinit();
|
||||||
|
try recorder.capture(" ");
|
||||||
|
|
||||||
|
try std.testing.expectEqualStrings("20 => ios_qwertz:whitespace:key:space\n", recorder.text());
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, recorder.text(), "leader") == null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, recorder.text(), "save") == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: unknown printable text and unknown escapes stay unknown" {
|
||||||
|
try expectKind("🔥", .unknown);
|
||||||
|
try expectKind("\x1b[999~", .unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: layout trace reports malformed lines and invalid hex" {
|
||||||
|
{
|
||||||
|
const result = try replayLayoutTrace(std.testing.allocator, &ios_qwertz, "7a ios_qwertz:letter:text:z\n");
|
||||||
|
defer result.deinit(std.testing.allocator);
|
||||||
|
switch (result) {
|
||||||
|
.diverged => |diverged| {
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), diverged.step);
|
||||||
|
try std.testing.expectEqualStrings("layout trace line is missing separator", diverged.message);
|
||||||
|
},
|
||||||
|
.ok => return error.ExpectedTraceDivergence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try replayLayoutTrace(std.testing.allocator, &ios_qwertz, "zz => ios_qwertz:letter:text:z\n");
|
||||||
|
defer result.deinit(std.testing.allocator);
|
||||||
|
switch (result) {
|
||||||
|
.diverged => |diverged| {
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), diverged.step);
|
||||||
|
try std.testing.expectEqualStrings("layout trace line has invalid hex bytes", diverged.message);
|
||||||
|
},
|
||||||
|
.ok => return error.ExpectedTraceDivergence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: layout trace names first classification mismatch" {
|
||||||
|
const result = try replayLayoutTrace(std.testing.allocator, &ios_qwertz, "7a => ios_qwertz:punctuation:text:z\n");
|
||||||
|
defer result.deinit(std.testing.allocator);
|
||||||
|
switch (result) {
|
||||||
|
.diverged => |diverged| {
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), diverged.step);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, diverged.message, "expected layout event") != null);
|
||||||
|
},
|
||||||
|
.ok => return error.ExpectedTraceDivergence,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
|
const layout = @import("layout.zig");
|
||||||
const protocol = @import("protocol.zig");
|
const protocol = @import("protocol.zig");
|
||||||
const replay = @import("replay.zig");
|
const replay = @import("replay.zig");
|
||||||
const session = @import("session.zig");
|
const session = @import("session.zig");
|
||||||
@@ -118,6 +119,7 @@ fn collectRemainingArgs(allocator: std.mem.Allocator, args: *std.process.Args.It
|
|||||||
|
|
||||||
test {
|
test {
|
||||||
_ = input;
|
_ = input;
|
||||||
|
_ = layout;
|
||||||
_ = protocol;
|
_ = protocol;
|
||||||
_ = replay;
|
_ = replay;
|
||||||
_ = session;
|
_ = session;
|
||||||
|
|||||||
Reference in New Issue
Block a user