Show recoverable panel and editor feedback
This commit is contained in:
+79
-7
@@ -65,6 +65,7 @@ pub const Client = struct {
|
||||
leader: leader_mod.Leader,
|
||||
viewport: Viewport,
|
||||
saved_bytes: ?[]u8 = null,
|
||||
message: ?[]const u8 = null,
|
||||
quit: bool = false,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, viewport: Viewport) !Client {
|
||||
@@ -96,6 +97,10 @@ pub const Client = struct {
|
||||
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "message ")) {
|
||||
self.message = line[8..];
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, line, "list_down")) return self.applyProtocol("command list_down");
|
||||
if (std.mem.eql(u8, line, "list_up")) return self.applyProtocol("command list_up");
|
||||
if (std.mem.eql(u8, line, "list_select")) return self.applyProtocol("command list_select");
|
||||
@@ -149,7 +154,9 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
const leader_status = self.leader.status();
|
||||
const status = if (leader_status.len != 0)
|
||||
const status = if (self.message) |message|
|
||||
try std.fmt.allocPrint(allocator, "{s}", .{message})
|
||||
else if (leader_status.len != 0)
|
||||
try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"{s}{s}{s}",
|
||||
@@ -206,11 +213,14 @@ pub const Client = struct {
|
||||
try out.append(allocator, '\n');
|
||||
}
|
||||
|
||||
const status = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"panel {d}/{d} x close",
|
||||
.{ snap.active_panel_index.? + 1, snap.panel_depth },
|
||||
);
|
||||
const status = if (self.message) |message|
|
||||
try std.fmt.allocPrint(allocator, "{s}", .{message})
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"panel {d}/{d} x close",
|
||||
.{ snap.active_panel_index.? + 1, snap.panel_depth },
|
||||
);
|
||||
defer allocator.free(status);
|
||||
try appendVisibleCells(allocator, &out, status, self.viewport.width);
|
||||
return out.toOwnedSlice(allocator);
|
||||
@@ -293,7 +303,19 @@ pub const Client = struct {
|
||||
fn applyProtocol(self: *Client, line: []const u8) !void {
|
||||
const response = try protocol.handleLine(self.allocator, &self.session, line);
|
||||
defer self.allocator.free(response);
|
||||
if (std.mem.startsWith(u8, response, "err ")) return Error.ProtocolRejected;
|
||||
if (std.mem.startsWith(u8, response, "err ")) {
|
||||
self.message = protocolErrorMessage(response);
|
||||
return Error.ProtocolRejected;
|
||||
}
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn protocolErrorMessage(response: []const u8) []const u8 {
|
||||
if (std.mem.indexOf(u8, response, "invalid panel title") != null) return "error: invalid panel title";
|
||||
if (std.mem.indexOf(u8, response, "no panel open") != null) return "error: no panel open";
|
||||
if (std.mem.indexOf(u8, response, "invalid list") != null) return "error: invalid list";
|
||||
if (std.mem.indexOf(u8, response, "active panel is not list") != null) return "error: active panel is not list";
|
||||
return "error: command failed";
|
||||
}
|
||||
|
||||
fn resize(self: *Client, payload: []const u8) !void {
|
||||
@@ -310,6 +332,7 @@ pub const Client = struct {
|
||||
const copy = try self.allocator.dupe(u8, snap.bytes);
|
||||
if (self.saved_bytes) |old| self.allocator.free(old);
|
||||
self.saved_bytes = copy;
|
||||
self.message = "saved";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -665,3 +688,52 @@ test "adversarial: list no-match state and invalid actions recover" {
|
||||
try client.handleTraceLine("list_cancel");
|
||||
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("list_down"));
|
||||
}
|
||||
|
||||
test "regular: save status appears near editor status line" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 20, .height = 4 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open abc");
|
||||
try client.handleTraceLine("key space");
|
||||
try client.handleTraceLine("key s");
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "saved") != null);
|
||||
try assertLinesFit(frame, 20);
|
||||
}
|
||||
|
||||
test "regular: empty generic panel shows local empty state" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 22, .height = 4 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("panel_open help");
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "help: no content yet") != null);
|
||||
try assertLinesFit(frame, 22);
|
||||
}
|
||||
|
||||
test "adversarial: protocol errors render recoverable local message" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 24, .height = 4 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open abc");
|
||||
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("panel_open bad title"));
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "error: invalid panel") != null);
|
||||
try assertLinesFit(frame, 24);
|
||||
}
|
||||
|
||||
test "adversarial: long feedback messages are clipped to viewport width" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 16, .height = 4 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open abc");
|
||||
try client.handleTraceLine("message this-message-is-way-too-long-for-the-phone-width");
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try assertLinesFit(frame, 16);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "this-message-is-") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "phone-width") == null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user