terminal: linefeed mode

This commit is contained in:
Mitchell Hashimoto
2023-10-12 20:46:26 -07:00
parent 8c61f8d890
commit 5ce50d08a1
6 changed files with 100 additions and 10 deletions

View File

@ -1522,6 +1522,7 @@ pub fn linefeed(self: *Terminal) !void {
defer tracy.end(); defer tracy.end();
try self.index(); try self.index();
if (self.modes.get(.linefeed)) self.carriageReturn();
} }
/// Inserts spaces at current cursor position moving existing cell contents /// Inserts spaces at current cursor position moving existing cell contents
@ -2319,6 +2320,22 @@ test "Terminal: linefeed unsets pending wrap" {
try testing.expect(t.screen.cursor.pending_wrap == false); try testing.expect(t.screen.cursor.pending_wrap == false);
} }
test "Terminal: linefeed mode automatic carriage return" {
var t = try init(testing.allocator, 10, 10);
defer t.deinit(testing.allocator);
// Basic grid writing
t.modes.set(.linefeed, true);
try t.printString("123456");
try t.linefeed();
try t.print('X');
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("123456\nX", str);
}
}
test "Terminal: carriage return unsets pending wrap" { test "Terminal: carriage return unsets pending wrap" {
var t = try init(testing.allocator, 5, 80); var t = try init(testing.allocator, 5, 80);
defer t.deinit(testing.allocator); defer t.deinit(testing.allocator);

View File

@ -174,6 +174,7 @@ const entries: []const ModeEntry = &.{
.{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM .{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM
.{ .name = "insert", .value = 4, .ansi = true }, .{ .name = "insert", .value = 4, .ansi = true },
.{ .name = "send_receive_mode", .value = 12, .ansi = true, .default = true }, // SRM .{ .name = "send_receive_mode", .value = 12, .ansi = true, .default = true }, // SRM
.{ .name = "linefeed", .value = 20, .ansi = true },
// DEC // DEC
.{ .name = "cursor_keys", .value = 1 }, // DECCKM .{ .name = "cursor_keys", .value = 1 }, // DECCKM

View File

@ -393,7 +393,7 @@ pub fn clearScreen(self: *Exec, history: bool) !void {
// If we reached here it means we're at a prompt, so we send a form-feed. // If we reached here it means we're at a prompt, so we send a form-feed.
assert(self.terminal.cursorIsAtPrompt()); assert(self.terminal.cursorIsAtPrompt());
try self.queueWrite(&[_]u8{0x0C}); try self.queueWrite(&[_]u8{0x0C}, false);
} }
/// Scroll the viewport /// Scroll the viewport
@ -418,7 +418,7 @@ pub fn jumpToPrompt(self: *Exec, delta: isize) !void {
} }
} }
pub inline fn queueWrite(self: *Exec, data: []const u8) !void { pub inline fn queueWrite(self: *Exec, data: []const u8, linefeed: bool) !void {
const ev = self.data.?; const ev = self.data.?;
// We go through and chunk the data if necessary to fit into // We go through and chunk the data if necessary to fit into
@ -427,19 +427,49 @@ pub inline fn queueWrite(self: *Exec, data: []const u8) !void {
while (i < data.len) { while (i < data.len) {
const req = try ev.write_req_pool.getGrow(self.alloc); const req = try ev.write_req_pool.getGrow(self.alloc);
const buf = try ev.write_buf_pool.getGrow(self.alloc); const buf = try ev.write_buf_pool.getGrow(self.alloc);
const end = @min(data.len, i + buf.len); const slice = slice: {
fastmem.copy(u8, buf, data[i..end]); // The maximum end index is either the end of our data or
// the end of our buffer, whichever is smaller.
const max = @min(data.len, i + buf.len);
// Fast
if (!linefeed) {
fastmem.copy(u8, buf, data[i..max]);
const len = max - i;
i = max;
break :slice buf[0..len];
}
// Slow, have to replace \r with \r\n
var buf_i: usize = 0;
while (i < data.len and buf_i < buf.len - 1) {
const ch = data[i];
i += 1;
if (ch != '\r') {
buf[buf_i] = ch;
buf_i += 1;
continue;
}
// CRLF
buf[buf_i] = '\r';
buf[buf_i + 1] = '\n';
buf_i += 2;
}
break :slice buf[0..buf_i];
};
ev.data_stream.queueWrite( ev.data_stream.queueWrite(
ev.loop, ev.loop,
&ev.write_queue, &ev.write_queue,
req, req,
.{ .slice = buf[0..(end - i)] }, .{ .slice = slice },
EventData, EventData,
ev, ev,
ttyWrite, ttyWrite,
); );
i = end;
} }
} }
@ -1507,6 +1537,10 @@ const StreamHandler = struct {
try self.queueRender(); try self.queueRender();
}, },
.linefeed => {
self.messageWriter(.{ .linefeed_mode = enabled });
},
.mouse_event_x10 => self.terminal.flags.mouse_event = if (enabled) .x10 else .none, .mouse_event_x10 => self.terminal.flags.mouse_event = if (enabled) .x10 else .none,
.mouse_event_normal => self.terminal.flags.mouse_event = if (enabled) .normal else .none, .mouse_event_normal => self.terminal.flags.mouse_event = if (enabled) .normal else .none,
.mouse_event_button => self.terminal.flags.mouse_event = if (enabled) .button else .none, .mouse_event_button => self.terminal.flags.mouse_event = if (enabled) .button else .none,

View File

@ -62,6 +62,10 @@ sync_reset_cancel_c: xev.Completion = .{},
/// The underlying IO implementation. /// The underlying IO implementation.
impl: *termio.Impl, impl: *termio.Impl,
/// True if linefeed mode is enabled. This is duplicated here so that the
/// write thread doesn't need to grab a lock to check this on every write.
linefeed_mode: bool = false,
/// The mailbox that can be used to send this thread messages. Note /// The mailbox that can be used to send this thread messages. Note
/// this is a blocking queue so if it is full you will get errors (or block). /// this is a blocking queue so if it is full you will get errors (or block).
mailbox: *Mailbox, mailbox: *Mailbox,
@ -175,11 +179,12 @@ fn drainMailbox(self: *Thread) !void {
.scroll_viewport => |v| try self.impl.scrollViewport(v), .scroll_viewport => |v| try self.impl.scrollViewport(v),
.jump_to_prompt => |v| try self.impl.jumpToPrompt(v), .jump_to_prompt => |v| try self.impl.jumpToPrompt(v),
.start_synchronized_output => self.startSynchronizedOutput(), .start_synchronized_output => self.startSynchronizedOutput(),
.write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), .linefeed_mode => |v| self.linefeed_mode = v,
.write_stable => |v| try self.impl.queueWrite(v), .write_small => |v| try self.impl.queueWrite(v.data[0..v.len], self.linefeed_mode),
.write_stable => |v| try self.impl.queueWrite(v, self.linefeed_mode),
.write_alloc => |v| { .write_alloc => |v| {
defer v.alloc.free(v.data); defer v.alloc.free(v.data);
try self.impl.queueWrite(v.data); try self.impl.queueWrite(v.data, self.linefeed_mode);
}, },
} }
} }

View File

@ -56,6 +56,9 @@ pub const Message = union(enum) {
/// period of time so that a bad actor can't hang the terminal. /// period of time so that a bad actor can't hang the terminal.
start_synchronized_output: void, start_synchronized_output: void,
/// Enable or disable linefeed mode (mode 20).
linefeed_mode: bool,
/// Write where the data fits in the union. /// Write where the data fits in the union.
write_small: WriteReq.Small, write_small: WriteReq.Small,

View File

@ -0,0 +1,30 @@
import VTMode from "@/components/VTMode";
# Linefeed
<VTMode value={20} ansi={true} />
When enabled, [LF](/vt/lf), [VF](/vt/vf), [FF](/vt/ff) all add an
automatic [carriage return](/vt/cr) after the linefeed. Additionally,
all `\r` sent from the terminal to the application are replaced by
`\r\n`.
This mode is typically disabled on terminal startup.
## Validation
### LINEFEED V-1: Simple Usage
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "123456"
printf "\033[20h"
printf "\n"
printf "X"
```
```
|123456____|
|Xc________|
```