diff --git a/include/ghostty.h b/include/ghostty.h index 702a88ecc..cd716e38f 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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, diff --git a/src/Surface.zig b/src/Surface.zig index 8cd8d253b..951ef14ef 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 94965d38c..83e2f5011 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -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, diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 47c2972ac..bbf408e02 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -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), diff --git a/src/input/Binding.zig b/src/input/Binding.zig index e1c636ab7..d368c48b2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -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, diff --git a/src/input/command.zig b/src/input/command.zig index 639fc6e39..ce218718f 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -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",