mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #349 from mitchellh/sync
Synchronized Output Sequence
This commit is contained in:
@ -469,6 +469,12 @@ pub fn render(
|
||||
state.mutex.lock();
|
||||
defer state.mutex.unlock();
|
||||
|
||||
// If we're in a synchronized output state, we pause all rendering.
|
||||
if (state.terminal.modes.get(.synchronized_output)) {
|
||||
log.debug("synchronized output started, skipping render", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
self.cursor_visible = visible: {
|
||||
// If the cursor is explicitly not visible in the state,
|
||||
// then it is not visible.
|
||||
|
@ -711,6 +711,12 @@ pub fn render(
|
||||
state.mutex.lock();
|
||||
defer state.mutex.unlock();
|
||||
|
||||
// If we're in a synchronized output state, we pause all rendering.
|
||||
if (state.terminal.modes.get(.synchronized_output)) {
|
||||
log.debug("synchronized output started, skipping render", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
self.cursor_visible = visible: {
|
||||
// If the cursor is explicitly not visible in the state,
|
||||
// then it is not visible.
|
||||
|
@ -694,6 +694,33 @@ test "csi: colon for non-m final" {
|
||||
try testing.expect(p.state == .ground);
|
||||
}
|
||||
|
||||
test "csi: request mode decrqm" {
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
for ("[?2026$") |c| {
|
||||
const a = p.next(c);
|
||||
try testing.expect(a[0] == null);
|
||||
try testing.expect(a[1] == null);
|
||||
try testing.expect(a[2] == null);
|
||||
}
|
||||
|
||||
{
|
||||
const a = p.next('p');
|
||||
try testing.expect(p.state == .ground);
|
||||
try testing.expect(a[0] == null);
|
||||
try testing.expect(a[1].? == .csi_dispatch);
|
||||
try testing.expect(a[2] == null);
|
||||
|
||||
const d = a[1].?.csi_dispatch;
|
||||
try testing.expect(d.final == 'p');
|
||||
try testing.expectEqual(@as(usize, 2), d.intermediates.len);
|
||||
try testing.expectEqual(@as(usize, 1), d.params.len);
|
||||
try testing.expectEqual(@as(u16, '?'), d.intermediates[0]);
|
||||
try testing.expectEqual(@as(u16, '$'), d.intermediates[1]);
|
||||
try testing.expectEqual(@as(u16, 2026), d.params[0]);
|
||||
}
|
||||
}
|
||||
|
||||
test "osc: change window title" {
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
|
@ -170,6 +170,7 @@ const entries: []const ModeEntry = &.{
|
||||
.{ .name = "alt_sends_escape", .value = 1039 },
|
||||
.{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049 },
|
||||
.{ .name = "bracketed_paste", .value = 2004 },
|
||||
.{ .name = "synchronized_output", .value = 2026 },
|
||||
};
|
||||
|
||||
test {
|
||||
|
@ -569,6 +569,35 @@ pub fn Stream(comptime Handler: type) type {
|
||||
),
|
||||
},
|
||||
|
||||
// DECRQM - Request Mode
|
||||
'p' => switch (action.intermediates.len) {
|
||||
2 => decrqm: {
|
||||
if (action.intermediates[0] != '?' and
|
||||
action.intermediates[1] != '$')
|
||||
{
|
||||
log.warn(
|
||||
"ignoring unimplemented CSI p with intermediates: {s}",
|
||||
.{action.intermediates},
|
||||
);
|
||||
break :decrqm;
|
||||
}
|
||||
|
||||
if (action.params.len != 1) {
|
||||
log.warn("invalid DECRQM command: {}", .{action});
|
||||
break :decrqm;
|
||||
}
|
||||
|
||||
if (@hasDecl(T, "requestMode")) {
|
||||
try self.handler.requestMode(action.params[0]);
|
||||
} else log.warn("unimplemented DECRQM callback: {}", .{action});
|
||||
},
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI p with intermediates: {s}",
|
||||
.{action.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// DECSCUSR - Select Cursor Style
|
||||
// TODO: test
|
||||
'q' => if (@hasDecl(T, "setCursorStyle")) try self.handler.setCursorStyle(
|
||||
|
@ -293,9 +293,25 @@ pub fn resize(
|
||||
// Update our pixel sizes
|
||||
self.terminal.width_px = padded_size.width;
|
||||
self.terminal.height_px = padded_size.height;
|
||||
|
||||
// Disable synchronized output mode so that we show changes
|
||||
// immediately for a resize. This is allowed by the spec.
|
||||
self.terminal.modes.set(.synchronized_output, false);
|
||||
|
||||
// Wake up our renderer so any changes will be shown asap
|
||||
self.renderer_wakeup.notify() catch {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the synchronized output mode. This is usually called by timer
|
||||
/// expiration from the termio thread.
|
||||
pub fn resetSynchronizedOutput(self: *Exec) void {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
self.terminal.modes.set(.synchronized_output, false);
|
||||
self.renderer_wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
/// Clear the screen.
|
||||
pub fn clearScreen(self: *Exec, history: bool) !void {
|
||||
{
|
||||
@ -1272,6 +1288,27 @@ const StreamHandler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn requestMode(self: *StreamHandler, mode_raw: u16) !void {
|
||||
// Get the mode value and respond.
|
||||
const code: u8 = code: {
|
||||
if (!terminal.modes.hasSupport(mode_raw)) break :code 0;
|
||||
if (self.terminal.modes.get(@enumFromInt(mode_raw))) break :code 1;
|
||||
break :code 2;
|
||||
};
|
||||
|
||||
var msg: termio.Message = .{ .write_small = .{} };
|
||||
const resp = try std.fmt.bufPrint(
|
||||
&msg.write_small.data,
|
||||
"\x1B[?{};{}$y",
|
||||
.{
|
||||
mode_raw,
|
||||
code,
|
||||
},
|
||||
);
|
||||
msg.write_small.len = @intCast(resp.len);
|
||||
self.messageWriter(msg);
|
||||
}
|
||||
|
||||
pub fn saveMode(self: *StreamHandler, mode: terminal.Mode) !void {
|
||||
// log.debug("save mode={}", .{mode});
|
||||
self.terminal.modes.save(mode);
|
||||
@ -1334,6 +1371,13 @@ const StreamHandler = struct {
|
||||
if (enabled) .@"132_cols" else .@"80_cols",
|
||||
),
|
||||
|
||||
// We need to start a timer to prevent the emulator being hung
|
||||
// forever.
|
||||
.synchronized_output => {
|
||||
if (enabled) self.messageWriter(.{ .start_synchronized_output = {} });
|
||||
try self.queueRender();
|
||||
},
|
||||
|
||||
.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,
|
||||
|
@ -27,6 +27,10 @@ const Coalesce = struct {
|
||||
resize: ?termio.Message.Resize = null,
|
||||
};
|
||||
|
||||
/// The number of milliseconds before we reset the synchronized output flag
|
||||
/// if the running program hasn't already.
|
||||
const sync_reset_ms = 1000;
|
||||
|
||||
/// Allocator used for some state
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
@ -49,6 +53,12 @@ coalesce_c: xev.Completion = .{},
|
||||
coalesce_cancel_c: xev.Completion = .{},
|
||||
coalesce_data: Coalesce = .{},
|
||||
|
||||
/// This timer is used to reset synchronized output modes so that
|
||||
/// the terminal doesn't freeze with a bad actor.
|
||||
sync_reset: xev.Timer,
|
||||
sync_reset_c: xev.Completion = .{},
|
||||
sync_reset_cancel_c: xev.Completion = .{},
|
||||
|
||||
/// The underlying IO implementation.
|
||||
impl: *termio.Impl,
|
||||
|
||||
@ -79,6 +89,10 @@ pub fn init(
|
||||
var coalesce_h = try xev.Timer.init();
|
||||
errdefer coalesce_h.deinit();
|
||||
|
||||
// This timer is used to reset synchronized output modes.
|
||||
var sync_reset_h = try xev.Timer.init();
|
||||
errdefer sync_reset_h.deinit();
|
||||
|
||||
// The mailbox for messaging this thread
|
||||
var mailbox = try Mailbox.create(alloc);
|
||||
errdefer mailbox.destroy(alloc);
|
||||
@ -89,6 +103,7 @@ pub fn init(
|
||||
.wakeup = wakeup_h,
|
||||
.stop = stop_h,
|
||||
.coalesce = coalesce_h,
|
||||
.sync_reset = sync_reset_h,
|
||||
.impl = impl,
|
||||
.mailbox = mailbox,
|
||||
};
|
||||
@ -98,6 +113,7 @@ pub fn init(
|
||||
/// completes executing; the caller must join prior to this.
|
||||
pub fn deinit(self: *Thread) void {
|
||||
self.coalesce.deinit();
|
||||
self.sync_reset.deinit();
|
||||
self.stop.deinit();
|
||||
self.wakeup.deinit();
|
||||
self.loop.deinit();
|
||||
@ -158,6 +174,7 @@ fn drainMailbox(self: *Thread) !void {
|
||||
.clear_screen => |v| try self.impl.clearScreen(v.history),
|
||||
.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),
|
||||
.write_alloc => |v| {
|
||||
@ -174,6 +191,18 @@ fn drainMailbox(self: *Thread) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn startSynchronizedOutput(self: *Thread) void {
|
||||
self.sync_reset.reset(
|
||||
&self.loop,
|
||||
&self.sync_reset_c,
|
||||
&self.sync_reset_cancel_c,
|
||||
sync_reset_ms,
|
||||
Thread,
|
||||
self,
|
||||
syncResetCallback,
|
||||
);
|
||||
}
|
||||
|
||||
fn handleResize(self: *Thread, resize: termio.Message.Resize) void {
|
||||
self.coalesce_data.resize = resize;
|
||||
|
||||
@ -193,6 +222,25 @@ fn handleResize(self: *Thread, resize: termio.Message.Resize) void {
|
||||
);
|
||||
}
|
||||
|
||||
fn syncResetCallback(
|
||||
self_: ?*Thread,
|
||||
_: *xev.Loop,
|
||||
_: *xev.Completion,
|
||||
r: xev.Timer.RunError!void,
|
||||
) xev.CallbackAction {
|
||||
_ = r catch |err| switch (err) {
|
||||
error.Canceled => {},
|
||||
else => {
|
||||
log.warn("error during sync reset callback err={}", .{err});
|
||||
return .disarm;
|
||||
},
|
||||
};
|
||||
|
||||
const self = self_ orelse return .disarm;
|
||||
self.impl.resetSynchronizedOutput();
|
||||
return .disarm;
|
||||
}
|
||||
|
||||
fn coalesceCallback(
|
||||
self_: ?*Thread,
|
||||
_: *xev.Loop,
|
||||
|
@ -51,6 +51,11 @@ pub const Message = union(enum) {
|
||||
/// Jump forward/backward n prompts.
|
||||
jump_to_prompt: isize,
|
||||
|
||||
/// Send this when a synchronized output mode is started. This will
|
||||
/// start the timer so that the output mode is disabled after a
|
||||
/// period of time so that a bad actor can't hang the terminal.
|
||||
start_synchronized_output: void,
|
||||
|
||||
/// Write where the data fits in the union.
|
||||
write_small: WriteReq.Small,
|
||||
|
||||
|
Reference in New Issue
Block a user