feat: add readonly surface mode

pull/9130/head
Matthew Hrehirchuk 2025-10-10 12:30:55 -06:00 committed by Mitchell Hashimoto
parent dd06d8a13b
commit 12bb2f3f47
6 changed files with 58 additions and 1 deletions

View File

@ -797,6 +797,7 @@ typedef enum {
GHOSTTY_ACTION_RESIZE_SPLIT,
GHOSTTY_ACTION_EQUALIZE_SPLITS,
GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
GHOSTTY_ACTION_TOGGLE_READONLY,
GHOSTTY_ACTION_PRESENT_TERMINAL,
GHOSTTY_ACTION_SIZE_LIMIT,
GHOSTTY_ACTION_RESET_WINDOW_SIZE,

View File

@ -145,6 +145,12 @@ focused: bool = true,
/// Used to determine whether to continuously scroll.
selection_scroll_active: bool = false,
/// True if the surface is in read-only mode. When read-only, no input
/// is sent to the PTY but terminal-level operations like selections,
/// scrolling, and copy/paste keybinds still work. Warn before quit is
/// always enabled in this state.
readonly: bool = false,
/// Used to send notifications that long running commands have finished.
/// Requires that shell integration be active. Should represent a nanosecond
/// precision timestamp. It does not necessarily need to correspond to the
@ -871,6 +877,9 @@ pub fn deactivateInspector(self: *Surface) void {
/// True if the surface requires confirmation to quit. This should be called
/// by apprt to determine if the surface should confirm before quitting.
pub fn needsConfirmQuit(self: *Surface) bool {
// If the surface is in read-only mode, always require confirmation
if (self.readonly) return true;
// If the child has exited, then our process is certainly not alive.
// We check this first to avoid the locking overhead below.
if (self.child_exited) return false;
@ -2559,6 +2568,12 @@ pub fn keyCallback(
if (insp_ev) |*ev| ev else null,
)) |v| return v;
// If the surface is in read-only mode, we consume the key event here
// without sending it to the PTY.
if (self.readonly) {
return .consumed;
}
// If we allow KAM and KAM is enabled then we do nothing.
if (self.config.vt_kam_allowed) {
self.renderer_state.mutex.lock();
@ -3267,7 +3282,9 @@ pub fn scrollCallback(
// we convert to cursor keys. This only happens if we're:
// (1) alt screen (2) no explicit mouse reporting and (3) alt
// scroll mode enabled.
if (self.io.terminal.screens.active_key == .alternate and
// Additionally, we don't send cursor keys if the surface is in read-only mode.
if (!self.readonly and
self.io.terminal.screens.active_key == .alternate and
self.io.terminal.flags.mouse_event == .none and
self.io.terminal.modes.get(.mouse_alternate_scroll))
{
@ -3393,6 +3410,9 @@ fn mouseReport(
assert(self.config.mouse_reporting);
assert(self.io.terminal.flags.mouse_event != .none);
// If the surface is in read-only mode, do not send mouse reports to the PTY
if (self.readonly) return;
// Depending on the event, we may do nothing at all.
switch (self.io.terminal.flags.mouse_event) {
.none => unreachable, // checked by assert above
@ -5383,6 +5403,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
{},
),
.toggle_readonly => {
self.readonly = !self.readonly;
return try self.rt_app.performAction(
.{ .surface = self },
.toggle_readonly,
{},
);
},
.reset_window_size => return try self.rt_app.performAction(
.{ .surface = self },
.reset_window_size,

View File

@ -139,6 +139,11 @@ pub const Action = union(Key) {
/// to take up the entire window.
toggle_split_zoom,
/// Toggle whether the surface is in read-only mode. When read-only,
/// no input is sent to the PTY but terminal-level operations like
/// selections, scrolling, and copy/paste keybinds still work.
toggle_readonly,
/// Present the target terminal whether its a tab, split, or window.
present_terminal,
@ -335,6 +340,7 @@ pub const Action = union(Key) {
resize_split,
equalize_splits,
toggle_split_zoom,
toggle_readonly,
present_terminal,
size_limit,
reset_window_size,

View File

@ -724,6 +724,10 @@ pub const Application = extern struct {
.toggle_window_decorations => return Action.toggleWindowDecorations(target),
.toggle_command_palette => return Action.toggleCommandPalette(target),
.toggle_split_zoom => return Action.toggleSplitZoom(target),
.toggle_readonly => {
// The readonly state is managed in Surface.zig.
return true;
},
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
.command_finished => return Action.commandFinished(target, value),

View File

@ -552,6 +552,16 @@ pub const Action = union(enum) {
/// reflect this by displaying an icon indicating the zoomed state.
toggle_split_zoom,
/// Toggle read-only mode for the current surface.
///
/// When a surface is in read-only mode:
/// - No input is sent to the PTY (mouse events, key encoding)
/// - Input can still be used at the terminal level to make selections,
/// copy/paste (keybinds), scroll, etc.
/// - Warn before quit is always enabled in this state even if an active
/// process is not running
toggle_readonly,
/// Resize the current split in the specified direction and amount in
/// pixels. The two arguments should be joined with a comma (`,`),
/// like in `resize_split:up,10`.
@ -1241,6 +1251,7 @@ pub const Action = union(enum) {
.new_split,
.goto_split,
.toggle_split_zoom,
.toggle_readonly,
.resize_split,
.equalize_splits,
.inspector,

View File

@ -485,6 +485,12 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Toggle the zoom state of the current split.",
}},
.toggle_readonly => comptime &.{.{
.action = .toggle_readonly,
.title = "Toggle Read-Only Mode",
.description = "Toggle read-only mode for the current surface.",
}},
.equalize_splits => comptime &.{.{
.action = .equalize_splits,
.title = "Equalize Splits",