diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9332c5479..8a0e0c7f2 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1522,6 +1522,7 @@ pub fn linefeed(self: *Terminal) !void { defer tracy.end(); try self.index(); + if (self.modes.get(.linefeed)) self.carriageReturn(); } /// 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); } +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" { var t = try init(testing.allocator, 5, 80); defer t.deinit(testing.allocator); diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 0e70dd53c..b7d2e567e 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -174,6 +174,7 @@ const entries: []const ModeEntry = &.{ .{ .name = "disable_keyboard", .value = 2, .ansi = true }, // KAM .{ .name = "insert", .value = 4, .ansi = true }, .{ .name = "send_receive_mode", .value = 12, .ansi = true, .default = true }, // SRM + .{ .name = "linefeed", .value = 20, .ansi = true }, // DEC .{ .name = "cursor_keys", .value = 1 }, // DECCKM diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index bdf9bffad..c25b70031 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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. assert(self.terminal.cursorIsAtPrompt()); - try self.queueWrite(&[_]u8{0x0C}); + try self.queueWrite(&[_]u8{0x0C}, false); } /// 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.?; // 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) { const req = try ev.write_req_pool.getGrow(self.alloc); const buf = try ev.write_buf_pool.getGrow(self.alloc); - const end = @min(data.len, i + buf.len); - fastmem.copy(u8, buf, data[i..end]); + const slice = slice: { + // 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.loop, &ev.write_queue, req, - .{ .slice = buf[0..(end - i)] }, + .{ .slice = slice }, EventData, ev, ttyWrite, ); - - i = end; } } @@ -1507,6 +1537,10 @@ const StreamHandler = struct { try self.queueRender(); }, + .linefeed => { + self.messageWriter(.{ .linefeed_mode = enabled }); + }, + .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_button => self.terminal.flags.mouse_event = if (enabled) .button else .none, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 0b345ac1f..459cec97c 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -62,6 +62,10 @@ sync_reset_cancel_c: xev.Completion = .{}, /// The underlying IO implementation. 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 /// this is a blocking queue so if it is full you will get errors (or block). mailbox: *Mailbox, @@ -175,11 +179,12 @@ fn drainMailbox(self: *Thread) !void { .scroll_viewport => |v| try self.impl.scrollViewport(v), .jump_to_prompt => |v| try self.impl.jumpToPrompt(v), .start_synchronized_output => self.startSynchronizedOutput(), - .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), - .write_stable => |v| try self.impl.queueWrite(v), + .linefeed_mode => |v| self.linefeed_mode = 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| { defer v.alloc.free(v.data); - try self.impl.queueWrite(v.data); + try self.impl.queueWrite(v.data, self.linefeed_mode); }, } } diff --git a/src/termio/message.zig b/src/termio/message.zig index 06e8aae85..c7fe19976 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -56,6 +56,9 @@ pub const Message = union(enum) { /// period of time so that a bad actor can't hang the terminal. start_synchronized_output: void, + /// Enable or disable linefeed mode (mode 20). + linefeed_mode: bool, + /// Write where the data fits in the union. write_small: WriteReq.Small, diff --git a/website/app/vt/modes/linefeed/page.mdx b/website/app/vt/modes/linefeed/page.mdx new file mode 100644 index 000000000..27fc5b1d8 --- /dev/null +++ b/website/app/vt/modes/linefeed/page.mdx @@ -0,0 +1,30 @@ +import VTMode from "@/components/VTMode"; + +# Linefeed + + + +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________| +```