Add Space leader command rail

This commit is contained in:
slhx agent
2026-06-21 02:16:23 +02:00
parent db3028ce0e
commit cfbacb3cd2
3 changed files with 392 additions and 5 deletions
+125 -5
View File
@@ -1,4 +1,6 @@
const std = @import("std");
const input = @import("input.zig");
const leader_mod = @import("leader.zig");
const protocol = @import("protocol.zig");
const replay = @import("replay.zig");
const session_mod = @import("session.zig");
@@ -59,6 +61,7 @@ pub const TraceResult = struct {
pub const Client = struct {
allocator: std.mem.Allocator,
session: session_mod.Session,
leader: leader_mod.Leader,
viewport: Viewport,
saved_bytes: ?[]u8 = null,
quit: bool = false,
@@ -68,12 +71,14 @@ pub const Client = struct {
return .{
.allocator = allocator,
.session = session_mod.Session.init(allocator),
.leader = leader_mod.Leader.init(allocator),
.viewport = viewport,
};
}
pub fn deinit(self: *Client) void {
if (self.saved_bytes) |bytes| self.allocator.free(bytes);
self.leader.deinit();
self.session.deinit();
self.* = undefined;
}
@@ -85,6 +90,8 @@ pub const Client = struct {
}
if (self.quit) return Error.ClientQuit;
if (std.mem.startsWith(u8, line, "key ")) return self.handleTraceKey(line[4..]);
if (std.mem.startsWith(u8, line, "type ")) return self.handleInput(line[5..]);
if (std.mem.startsWith(u8, line, "open ")) return self.applyProtocol(line);
if (std.mem.startsWith(u8, line, "insert ")) return self.applyProtocolCommand(line);
if (std.mem.eql(u8, line, "left")) return self.applyProtocol("command move_left");
@@ -125,20 +132,58 @@ pub const Client = struct {
try out.append(allocator, '\n');
}
const status = try std.fmt.allocPrint(
allocator,
"mim row={d} col={d} bytes={d}{s}",
.{ cursor_line + 1, cursor_col + 1, snap.bytes.len, if (self.quit) " quit" else "" },
);
const leader_status = self.leader.status();
const status = if (leader_status.len != 0)
try std.fmt.allocPrint(
allocator,
"{s}{s}{s}",
.{ leader_status, if (self.leader.promptText().len > 0) ": " else "", self.leader.promptText() },
)
else
try std.fmt.allocPrint(
allocator,
"mim row={d} col={d} bytes={d}{s}",
.{ cursor_line + 1, cursor_col + 1, snap.bytes.len, if (self.quit) " quit" else "" },
);
defer allocator.free(status);
try appendVisibleCells(allocator, &out, status, self.viewport.width);
return out.toOwnedSlice(allocator);
}
pub fn handleInput(self: *Client, raw: []const u8) !void {
if (self.quit) return Error.ClientQuit;
const action = try self.leader.handleEvent(input.normalize(raw));
defer action.deinit(self.allocator);
try self.applyLeaderAction(action);
}
pub fn saved(self: *const Client) ![]const u8 {
return self.saved_bytes orelse Error.NothingSaved;
}
fn handleTraceKey(self: *Client, key_name: []const u8) !void {
if (std.mem.eql(u8, key_name, "space")) return self.handleInput(" ");
if (std.mem.eql(u8, key_name, "enter")) return self.handleInput("\r");
if (std.mem.eql(u8, key_name, "backspace")) return self.handleInput("\x7f");
if (std.mem.eql(u8, key_name, "escape")) return self.handleInput("\x1b");
if (key_name.len == 1) return self.handleInput(key_name);
return Error.UnknownTraceEvent;
}
fn applyLeaderAction(self: *Client, action: leader_mod.Action) !void {
switch (action) {
.none => {},
.save => try self.save(),
.quit => self.quit = true,
.open => |path| {
const line = try std.fmt.allocPrint(self.allocator, "open {s}", .{path});
defer self.allocator.free(line);
try self.applyProtocol(line);
},
.not_built => {},
}
}
fn applyProtocolCommand(self: *Client, line: []const u8) !void {
const command = try std.fmt.allocPrint(self.allocator, "command {s}", .{line});
defer self.allocator.free(command);
@@ -294,3 +339,78 @@ test "adversarial: quit prevents further edits" {
try std.testing.expectError(Error.ClientQuit, client.handleTraceLine("insert x"));
}
test "regular: leader rail is visible in terminal render and dispatches save" {
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("key space");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "leader: s save") != null);
try client.handleTraceLine("key s");
try std.testing.expectEqualStrings("abc", try client.saved());
}
test "regular: leader rail dispatches quit" {
var client = try Client.init(std.testing.allocator, .{ .width = 40, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("key space");
try client.handleTraceLine("key q");
try std.testing.expect(client.quit);
try std.testing.expectError(Error.ClientQuit, client.handleTraceLine("insert x"));
}
test "regular: leader open prompt opens typed UTF-8 payload" {
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open old");
try client.handleTraceLine("key space");
try client.handleTraceLine("key o");
try client.handleTraceLine("type café.zig");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "café.zig") != null);
try client.handleTraceLine("key enter");
try client.handleTraceLine("key space");
try client.handleTraceLine("key s");
try std.testing.expectEqualStrings("café.zig", try client.saved());
}
test "regular: leader search and panel-close entries recover as not-built messages" {
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("key space");
try client.handleTraceLine("key /");
const search_frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(search_frame);
try std.testing.expect(std.mem.indexOf(u8, search_frame, "search is not built") != null);
try client.handleTraceLine("key space");
try client.handleTraceLine("key x");
const close_frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(close_frame);
try std.testing.expect(std.mem.indexOf(u8, close_frame, "panel close is not built") != null);
}
test "adversarial: unknown leader input recovers without saving or quitting" {
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 4 });
defer client.deinit();
try client.handleTraceLine("open abc");
try client.handleTraceLine("key space");
try client.handleTraceLine("key ?");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "unknown leader key") != null);
try std.testing.expect(!client.quit);
try std.testing.expectError(Error.NothingSaved, client.saved());
}