mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
terminal: linefeed mode
This commit is contained in:
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
|
||||||
|
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