Files
herdr/src/terminal/runtime.rs
T

588 lines
17 KiB
Rust

use std::sync::Arc;
use crate::render_signal::RenderSignal;
use bytes::Bytes;
use ratatui::{layout::Rect, Frame};
use tokio::sync::{mpsc, Notify};
use crate::events::AppEvent;
use crate::layout::PaneId;
/// Live runtime for a server-owned terminal.
///
/// The PTY implementation still delegates to the legacy pane runtime while the
/// migration proceeds, but production code now depends on this terminal-layer
/// type instead of the pane module's implementation detail.
pub struct TerminalRuntime(crate::pane::PaneRuntime);
impl TerminalRuntime {
pub fn shutdown(self) {
self.0.shutdown();
}
#[cfg(unix)]
pub fn duplicate_handoff_fd(&self) -> std::io::Result<std::os::fd::RawFd> {
self.0.duplicate_handoff_fd()
}
#[cfg(unix)]
pub fn preserve_for_handoff(self) {
self.0.preserve_for_handoff()
}
#[cfg(unix)]
pub fn assume_handoff_ownership(&mut self) {
self.0.assume_handoff_ownership();
}
#[cfg(unix)]
pub fn set_handoff_reader_paused(&self, paused: bool) {
self.0.set_handoff_reader_paused(paused);
}
#[cfg(unix)]
pub fn pause_handoff_reader(&self, timeout: std::time::Duration) -> std::io::Result<()> {
self.0.pause_handoff_reader(timeout)
}
#[cfg(unix)]
pub fn handoff_runtime_state(
&self,
pane_id: u32,
) -> crate::handoff_runtime::HandoffRuntimeState {
self.0.handoff_runtime_state(pane_id)
}
#[cfg(unix)]
pub fn handoff_history_ansi(&self) -> Option<String> {
self.0.handoff_history_ansi()
}
#[cfg(unix)]
pub fn from_handoff_fd(
import: crate::handoff_runtime::ImportedHandoffRuntime,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::from_handoff_fd(
import,
scrollback_limit_bytes,
host_terminal_theme,
host_terminal_appearance,
events,
render_notify,
render_dirty,
)
.map(Self)
}
// Wrapper mirrors pane runtime construction arguments.
#[allow(clippy::too_many_arguments)]
pub fn spawn(
pane_id: PaneId,
rows: u16,
cols: u16,
cwd: std::path::PathBuf,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
launch_env: &crate::pane::PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::spawn(
pane_id,
rows,
cols,
cwd,
scrollback_limit_bytes,
host_terminal_theme,
host_terminal_appearance,
shell_config,
launch_env,
events,
render_notify,
render_dirty,
)
.map(Self)
}
// Wrapper mirrors pane runtime construction arguments.
#[allow(clippy::too_many_arguments)]
pub fn spawn_with_initial_history(
pane_id: PaneId,
rows: u16,
cols: u16,
cwd: std::path::PathBuf,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
shell_config: crate::pane::PaneShellConfig<'_>,
launch_env: &crate::pane::PaneLaunchEnv,
initial_history_ansi: Option<&str>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::spawn_with_initial_history(
pane_id,
rows,
cols,
cwd,
scrollback_limit_bytes,
host_terminal_theme,
host_terminal_appearance,
shell_config,
launch_env,
initial_history_ansi,
events,
render_notify,
render_dirty,
)
.map(Self)
}
// Wrapper mirrors pane runtime construction arguments.
#[allow(clippy::too_many_arguments)]
pub fn spawn_shell_command(
pane_id: PaneId,
rows: u16,
cols: u16,
cwd: std::path::PathBuf,
command: &str,
launch_env: &crate::pane::PaneLaunchEnv,
agent_detection: crate::pane::AgentDetection,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::spawn_shell_command(
pane_id,
rows,
cols,
cwd,
command,
launch_env,
agent_detection,
scrollback_limit_bytes,
host_terminal_theme,
host_terminal_appearance,
events,
render_notify,
render_dirty,
)
.map(Self)
}
// Wrapper mirrors pane runtime construction arguments, including detection policy.
#[allow(clippy::too_many_arguments)]
pub fn spawn_argv_command(
pane_id: PaneId,
rows: u16,
cols: u16,
cwd: std::path::PathBuf,
argv: &[String],
launch_env: &crate::pane::PaneLaunchEnv,
agent_detection: crate::pane::AgentDetection,
scrollback_limit_bytes: usize,
host_terminal_theme: crate::terminal_theme::TerminalTheme,
host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::spawn_argv_command(
pane_id,
rows,
cols,
cwd,
argv,
launch_env,
agent_detection,
scrollback_limit_bytes,
host_terminal_theme,
host_terminal_appearance,
events,
render_notify,
render_dirty,
)
.map(Self)
}
pub fn apply_host_terminal_theme(&self, theme: crate::terminal_theme::TerminalTheme) {
self.0.apply_host_terminal_theme(theme);
}
pub fn apply_host_terminal_appearance(
&self,
appearance: Option<crate::terminal_theme::HostAppearance>,
) {
self.0.apply_host_terminal_appearance(appearance);
}
pub fn begin_graceful_release(&self, agent: crate::detect::Agent) {
self.0.begin_graceful_release(agent);
}
pub fn reset_agent_detection(&self) {
self.0.reset_agent_detection();
}
#[cfg(test)]
pub(crate) fn agent_detection_reset_notify_for_test(
&self,
) -> std::sync::Arc<tokio::sync::Notify> {
self.0.agent_detection_reset_notify_for_test()
}
#[cfg(test)]
pub(crate) fn agent_detection_enabled_for_test(&self) -> bool {
self.0.agent_detection_enabled_for_test()
}
pub fn set_full_lifecycle_authority_active(&self, active: bool) {
self.0.set_full_lifecycle_authority_active(active);
}
pub fn resize(&self, rows: u16, cols: u16, cell_width_px: u32, cell_height_px: u32) {
self.0.resize(rows, cols, cell_width_px, cell_height_px);
}
#[cfg(unix)]
pub fn nudge_child_redraw_after_handoff(&self) {
self.0.nudge_child_redraw_after_handoff();
}
pub fn scroll_up(&self, lines: usize) {
self.0.scroll_up(lines);
}
pub fn scroll_down(&self, lines: usize) {
self.0.scroll_down(lines);
}
pub fn scroll_reset(&self) {
self.0.scroll_reset();
}
pub fn set_scroll_offset_from_bottom(&self, lines: usize) {
self.0.set_scroll_offset_from_bottom(lines);
}
pub fn scroll_metrics(&self) -> Option<crate::pane::ScrollMetrics> {
self.0.scroll_metrics()
}
pub(crate) fn search_text_matches(
&self,
query: &str,
case_sensitive: bool,
) -> Vec<crate::pane::TerminalTextMatch> {
self.0.search_text_matches(query, case_sensitive)
}
pub(crate) fn text_match_is_current(&self, text_match: crate::pane::TerminalTextMatch) -> bool {
self.0.text_match_is_current(text_match)
}
pub(crate) fn text_matches_are_current(
&self,
text_matches: &[crate::pane::TerminalTextMatch],
) -> Vec<bool> {
self.0.text_matches_are_current(text_matches)
}
pub(crate) fn word_motion_target(
&self,
row: u32,
col: u16,
motion: crate::pane::TerminalWordMotion,
) -> Option<crate::pane::TerminalTextPoint> {
self.0.word_motion_target(row, col, motion)
}
/// Collects the complete terminal input-mode snapshot.
///
/// This performs multiple terminal queries and may format keyboard state.
/// Keep it out of render/layout and pane-scaled loops; add a narrow accessor
/// when only one terminal fact is needed.
pub fn input_state(&self) -> Option<crate::pane::InputState> {
self.0.input_state()
}
/// Reads only whether the alternate screen is active.
pub fn alternate_screen_active(&self) -> bool {
self.0.alternate_screen_active()
}
pub fn cursor_state(
&self,
area: Rect,
show_cursor: bool,
) -> Option<crate::pane::TerminalCursorState> {
self.0.cursor_state(area, show_cursor)
}
pub fn synchronized_output_active(&self) -> bool {
self.0.synchronized_output_active()
}
pub fn visible_text(&self) -> String {
self.0.visible_text()
}
pub fn visible_ansi(&self) -> String {
self.0.visible_ansi()
}
pub fn detection_text(&self) -> String {
self.0.detection_text()
}
pub fn terminal_title(&self) -> Option<String> {
self.0.terminal_title()
}
pub fn agent_osc_title(&self) -> String {
self.0.agent_osc_title()
}
pub fn agent_osc_progress(&self) -> String {
self.0.agent_osc_progress()
}
pub(crate) fn recent_text_snapshot(&self, lines: usize) -> crate::pane::TerminalReadSnapshot {
self.0.recent_text_snapshot(lines)
}
pub(crate) fn recent_ansi_snapshot(&self, lines: usize) -> crate::pane::TerminalReadSnapshot {
self.0.recent_ansi_snapshot(lines)
}
#[cfg(test)]
pub fn recent_unwrapped_text(&self, lines: usize) -> String {
self.0.recent_unwrapped_text_snapshot(lines).text
}
pub(crate) fn recent_unwrapped_text_snapshot(
&self,
lines: usize,
) -> crate::pane::TerminalReadSnapshot {
self.0.recent_unwrapped_text_snapshot(lines)
}
pub(crate) fn recent_unwrapped_ansi_snapshot(
&self,
lines: usize,
) -> crate::pane::TerminalReadSnapshot {
self.0.recent_unwrapped_ansi_snapshot(lines)
}
pub fn snapshot_history(&self) -> Option<String> {
self.0.snapshot_history()
}
pub fn extract_selection(&self, selection: &crate::selection::Selection) -> Option<String> {
self.0.extract_selection(selection)
}
pub fn render(&self, frame: &mut Frame, area: Rect, show_cursor: bool) {
self.0.render(frame, area, show_cursor);
}
pub(crate) fn collect_dirty_patch(
&self,
area_width: u16,
area_height: u16,
) -> crate::pane::TerminalDirtyPatchOutcome {
self.0.collect_dirty_patch(area_width, area_height)
}
pub fn visible_hyperlinks(&self, area: Rect) -> Vec<((u16, u16), String, String)> {
self.0.visible_hyperlinks(area)
}
pub fn kitty_image_placements_with_data_filter<F>(
&self,
needs_data: F,
) -> Vec<crate::ghostty::KittyImagePlacement>
where
F: FnMut(crate::ghostty::KittyImageDescriptor) -> bool,
{
self.0.kitty_image_placements_with_data_filter(needs_data)
}
pub fn keyboard_protocol(&self) -> crate::input::KeyboardProtocol {
self.0.keyboard_protocol()
}
pub fn encode_terminal_key(&self, key: crate::input::TerminalKey) -> Vec<u8> {
self.0.encode_terminal_key(key)
}
pub async fn send_bytes(&self, bytes: Bytes) -> Result<(), mpsc::error::SendError<Bytes>> {
self.0.send_bytes(bytes).await
}
pub fn try_send_bytes(&self, bytes: Bytes) -> Result<(), mpsc::error::TrySendError<Bytes>> {
self.0.try_send_bytes(bytes)
}
pub fn send_bytes_after(&self, bytes: Bytes, delay: std::time::Duration) {
self.0.send_bytes_after(bytes, delay);
}
pub async fn send_paste(&self, text: String) -> Result<(), mpsc::error::SendError<Bytes>> {
self.0.send_paste(text).await
}
pub fn try_send_paste(&self, text: String) -> Result<(), mpsc::error::TrySendError<Bytes>> {
self.0.try_send_paste(text)
}
pub fn try_send_focus_event(&self, event: crate::ghostty::FocusEvent) -> bool {
self.0.try_send_focus_event(event)
}
pub fn wheel_routing(&self) -> Option<crate::pane::WheelRouting> {
self.0.wheel_routing()
}
pub(crate) fn screen_text_snapshot(
&self,
) -> Option<(
crate::ghostty::ActiveScreen,
crate::terminal::ScreenSnapshot,
)> {
let (screen, cols, rows) = self.0.screen_text_snapshot()?;
Some((screen, crate::terminal::ScreenSnapshot { cols, rows }))
}
pub fn encode_mouse_button(
&self,
kind: crossterm::event::MouseEventKind,
position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
self.0.encode_mouse_button(kind, position, modifiers)
}
pub(crate) fn encode_mouse_motion(
&self,
kind: crossterm::event::MouseEventKind,
position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
self.0.encode_mouse_motion(kind, position, modifiers)
}
pub(crate) fn encode_mouse_wheel(
&self,
kind: crossterm::event::MouseEventKind,
position: crate::input::mouse::Position,
modifiers: crossterm::event::KeyModifiers,
) -> Option<Vec<u8>> {
self.0.encode_mouse_wheel(kind, position, modifiers)
}
pub(crate) fn pixel_size(&self) -> Option<(u32, u32)> {
self.0.pixel_size()
}
pub fn encode_alternate_scroll(
&self,
kind: crossterm::event::MouseEventKind,
) -> Option<Vec<u8>> {
self.0.encode_alternate_scroll(kind)
}
pub fn cwd(&self) -> Option<std::path::PathBuf> {
self.0.cwd()
}
pub fn follow_cwd(&self) -> Option<std::path::PathBuf> {
self.0.follow_cwd()
}
pub fn foreground_cwd(&self) -> Option<std::path::PathBuf> {
self.0.foreground_cwd()
}
pub fn child_pid(&self) -> Option<u32> {
self.0.child_pid()
}
pub(crate) fn current_size(&self) -> (u16, u16) {
self.0.current_size()
}
}
#[cfg(test)]
impl TerminalRuntime {
pub(crate) fn test_with_channel(cols: u16, rows: u16) -> (Self, mpsc::Receiver<Bytes>) {
let (runtime, rx) = crate::pane::PaneRuntime::test_with_channel(cols, rows);
(Self(runtime), rx)
}
pub(crate) fn test_with_channel_capacity(
cols: u16,
rows: u16,
capacity: usize,
) -> (Self, mpsc::Receiver<Bytes>) {
let (runtime, rx) =
crate::pane::PaneRuntime::test_with_channel_capacity(cols, rows, capacity);
(Self(runtime), rx)
}
pub(crate) fn test_with_screen_bytes(cols: u16, rows: u16, bytes: &[u8]) -> Self {
Self(crate::pane::PaneRuntime::test_with_screen_bytes(
cols, rows, bytes,
))
}
pub(crate) fn test_process_pty_bytes(&self, bytes: &[u8]) {
self.0.test_process_pty_bytes(bytes);
}
pub(crate) fn test_with_scrollback_bytes(
cols: u16,
rows: u16,
scrollback_limit_bytes: usize,
bytes: &[u8],
) -> Self {
Self(crate::pane::PaneRuntime::test_with_scrollback_bytes(
cols,
rows,
scrollback_limit_bytes,
bytes,
))
}
pub(crate) fn test_with_channel_and_scrollback_bytes(
cols: u16,
rows: u16,
scrollback_limit_bytes: usize,
bytes: &[u8],
channel_capacity: usize,
) -> (Self, mpsc::Receiver<Bytes>) {
let (runtime, rx) = crate::pane::PaneRuntime::test_with_channel_and_scrollback_bytes(
cols,
rows,
scrollback_limit_bytes,
bytes,
channel_capacity,
);
(Self(runtime), rx)
}
}