Fix terminal input mode and cursor rendering

This commit is contained in:
slhx agent
2026-06-21 18:08:59 +02:00
parent 5d7f239d34
commit 9aafe0e01c
7 changed files with 276 additions and 90 deletions
+38 -15
View File
@@ -236,18 +236,12 @@ fn runLocalEditor(
return;
}
client.enterInsertMode();
var raw_terminal = try RawTerminal.enable(stdin_file.handle);
defer raw_terminal.restore();
var stdin_buffer: [4096]u8 = undefined;
var stdin_reader = stdin_file.readerStreaming(io, &stdin_buffer);
const stdin = &stdin_reader.interface;
while (true) {
try renderLocalFrame(allocator, io, stdout, client, path, is_dir, true);
const raw = try readEditorInputAlloc(allocator, stdin);
const raw = try readEditorInputFdAlloc(allocator, stdin_file.handle);
defer if (raw) |bytes| allocator.free(bytes);
if (raw == null) return;
@@ -276,9 +270,9 @@ fn runLocalEditor(
} else |_| {}
if (client.requestedQuit()) {
if (dirty) {
if (dirty and !client.requestedDiscardQuit()) {
client.clearQuit();
client.setStatusMessage("dirty buffer: Space w saves, quit blocked");
client.setStatusMessage("dirty buffer: Space w saves, Space Q discards");
continue;
}
return;
@@ -333,7 +327,31 @@ const RawTerminal = struct {
}
};
fn readEditorInputAlloc(allocator: std.mem.Allocator, stdin: *std.Io.Reader) !?[]u8 {
fn readEditorInputFdAlloc(allocator: std.mem.Allocator, fd: std.posix.fd_t) !?[]u8 {
var one: [1]u8 = undefined;
const n = try std.posix.read(fd, &one);
if (n == 0) return null;
var bytes = std.ArrayList(u8).empty;
errdefer bytes.deinit(allocator);
try bytes.append(allocator, one[0]);
if (one[0] != 0x1b) return try bytes.toOwnedSlice(allocator);
var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }};
const ready = std.posix.poll(&fds, 30) catch 0;
if (ready == 0 or (fds[0].revents & std.posix.POLL.IN) == 0) return try bytes.toOwnedSlice(allocator);
const second_n = try std.posix.read(fd, &one);
if (second_n == 0) return try bytes.toOwnedSlice(allocator);
try bytes.append(allocator, one[0]);
if (one[0] == '[') {
const third_ready = std.posix.poll(&fds, 30) catch 0;
if (third_ready == 0 or (fds[0].revents & std.posix.POLL.IN) == 0) return try bytes.toOwnedSlice(allocator);
const third_n = try std.posix.read(fd, &one);
if (third_n != 0) try bytes.append(allocator, one[0]);
}
return try bytes.toOwnedSlice(allocator);
}
fn readEditorInputAlloc(allocator: std.mem.Allocator, stdin: *std.Io.Reader, fd: std.posix.fd_t) !?[]u8 {
const first = stdin.takeByte() catch |err| switch (err) {
error.EndOfStream => return null,
else => return err,
@@ -343,9 +361,14 @@ fn readEditorInputAlloc(allocator: std.mem.Allocator, stdin: *std.Io.Reader) !?[
try bytes.append(allocator, first);
if (first == 0x1b) {
var fds = [_]std.posix.pollfd{.{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }};
const ready: usize = if (fd < 0) 1 else std.posix.poll(&fds, 30) catch 0;
if (ready == 0 or (fd >= 0 and (fds[0].revents & std.posix.POLL.IN) == 0)) return try bytes.toOwnedSlice(allocator);
const second = stdin.takeByte() catch return try bytes.toOwnedSlice(allocator);
try bytes.append(allocator, second);
if (second == '[') {
const third_ready: usize = if (fd < 0) 1 else std.posix.poll(&fds, 30) catch 0;
if (third_ready == 0 or (fd >= 0 and (fds[0].revents & std.posix.POLL.IN) == 0)) return try bytes.toOwnedSlice(allocator);
const third = stdin.takeByte() catch return try bytes.toOwnedSlice(allocator);
try bytes.append(allocator, third);
}
@@ -373,7 +396,7 @@ fn renderLocalFrame(allocator: std.mem.Allocator, io: std.Io, stdout: std.Io.Fil
try writeTerminalText(allocator, io, stdout, if (is_dir)
"\nDirectory browser. Space opens commands; o/Enter opens panel items where available; q quits when clean.\n"
else
"\nEditor starts in insert mode. Esc or Space n enters normal mode; Space commands work in normal mode; Space w saves; Space q quits when clean.\n");
"\nEditor starts in normal mode. Press i to insert; Space commands work in normal mode; Space w saves; Space q quits when clean; Space Q discards dirty changes.\n");
} else {
try stdout.writeStreamingAll(io, frame);
try stdout.writeStreamingAll(io, "\n\n");
@@ -382,7 +405,7 @@ fn renderLocalFrame(allocator: std.mem.Allocator, io: std.Io, stdout: std.Io.Fil
try stdout.writeStreamingAll(io, if (is_dir)
"\nDirectory browser. Space opens commands; o/Enter opens panel items where available; q quits when clean.\n"
else
"\nEditor starts in insert mode. Esc or Space n enters normal mode; Space commands work in normal mode; Space w saves; Space q quits when clean.\n");
"\nEditor starts in normal mode. Press i to insert; Space commands work in normal mode; Space w saves; Space q quits when clean; Space Q discards dirty changes.\n");
}
}
@@ -545,17 +568,17 @@ test "regular: positional path opens file directory or new buffer" {
test "regular: local editor input reader preserves text utf8 and arrows" {
var ascii = std.Io.Reader.fixed("a");
const ascii_event = (try readEditorInputAlloc(std.testing.allocator, &ascii)).?;
const ascii_event = (try readEditorInputAlloc(std.testing.allocator, &ascii, -1)).?;
defer std.testing.allocator.free(ascii_event);
try std.testing.expectEqualStrings("a", ascii_event);
var utf8 = std.Io.Reader.fixed("é");
const utf8_event = (try readEditorInputAlloc(std.testing.allocator, &utf8)).?;
const utf8_event = (try readEditorInputAlloc(std.testing.allocator, &utf8, -1)).?;
defer std.testing.allocator.free(utf8_event);
try std.testing.expectEqualStrings("é", utf8_event);
var arrow = std.Io.Reader.fixed("\x1b[D");
const arrow_event = (try readEditorInputAlloc(std.testing.allocator, &arrow)).?;
const arrow_event = (try readEditorInputAlloc(std.testing.allocator, &arrow, -1)).?;
defer std.testing.allocator.free(arrow_event);
try std.testing.expectEqualStrings("\x1b[D", arrow_event);
}