mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
terminal: linefeed mode
This commit is contained in:
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
||||
|
30
website/app/vt/modes/linefeed/page.mdx
Normal file
30
website/app/vt/modes/linefeed/page.mdx
Normal 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________|
|
||||
```
|
Reference in New Issue
Block a user