Files
galaxy/app/src/terminal/mod.rs
T
rkw6086 7c106eecd5 feat: expand Galaxy agent and remote tooling
Add Wormhole remote helpers, provider and agent improvements, filesystem diagnostics, model metadata support, and schema-aware settings IntelliSense.
2026-08-23 13:55:47 -05:00

499 lines
15 KiB
Rust

use std::cmp::max;
pub use block_list_element::GridType;
use model::alt_screen::AltScreen;
use model::blocks::BlockList;
use model::index::Side;
pub use model::terminal_model::TerminalModel;
use ordered_float::Float;
use pathfinder_geometry::vector::vec2f;
use serde::{Deserialize, Serialize};
mod package_installers;
pub use galaxy_terminal::shell::{self, ShellLaunchData};
use galaxyui::geometry::vector::Vector2F;
use galaxyui::units::{IntoPixels, Lines, Pixels};
use galaxyui::{AppContext, WindowId};
pub(crate) use history::UpArrowHistoryConfig;
pub use history::{History, HistoryEntry, HistoryEvent, ShellHost};
pub use view::{Event, TerminalView};
mod block_list_settings;
mod alias;
mod alt_screen;
pub mod alt_screen_reporting;
mod audible_bell;
pub use audible_bell::AudibleBell;
pub mod available_shells;
mod block_filter;
pub mod block_list_element;
pub mod block_list_viewport;
pub mod blockgrid_element;
mod blockgrid_renderer;
mod bootstrap;
mod buy_credits_banner;
pub mod color;
mod command_corrections_denylist;
pub mod dynamic_enum_suggestions;
pub mod enable_auto_reload_modal;
pub mod event;
pub mod event_listener;
pub mod find;
pub(crate) mod focus_env;
pub mod general_settings;
pub mod grid_renderer;
pub mod grid_size_util;
pub mod history;
pub mod input;
pub mod keys;
pub mod keys_settings;
pub mod ligature_settings;
mod line_editor_status;
pub mod links;
#[cfg(all(not(target_family = "wasm"), feature = "local_tty"))]
pub mod local_shell;
#[cfg(feature = "local_tty")]
pub mod local_tty;
mod meta_shortcuts;
pub mod mock_terminal_manager;
pub mod model;
pub mod model_events;
pub mod platform;
pub mod profile_model_selector;
pub mod prompt;
pub mod prompt_render_helper;
pub mod recorder;
pub mod remote_tty;
pub mod resizable_data;
pub mod rich_history;
pub mod safe_mode_settings;
mod secret_regex_updater;
pub mod session_settings;
pub mod settings;
mod share_block_modal;
pub mod shared_session;
mod shell_launch_state;
pub mod universal_developer_input;
pub mod ssh;
pub mod terminal_manager;
mod terminal_size_element;
pub mod view;
mod waterfall_gap_element;
pub mod wormhole;
mod writeable_pty;
#[cfg(feature = "tui")]
pub use writeable_pty::{PtyIntent, PtyIntentEvent, TerminalSurface};
#[cfg(windows)]
pub mod wsl;
pub mod cli_agent;
pub use cli_agent::CLIAgent;
pub(crate) mod cli_agent_sessions;
pub use block_list_settings::*;
pub use mock_terminal_manager::MockTerminalManager;
use model_events::{ModelEvent, ModelEventDispatcher};
pub use secret_regex_updater::CustomSecretRegexUpdater;
pub use share_block_modal::{ShareBlockModal, ShareBlockModalEvent, ShareBlockType};
pub use shell_launch_state::ShellLaunchState;
pub use terminal_manager::TerminalManager;
pub use view::{
CANCEL_COMMAND_KEYBINDING, TOGGLE_AUTOEXECUTE_MODE_KEYBINDING,
TOGGLE_HIDE_CLI_RESPONSES_KEYBINDING, TOGGLE_QUEUE_NEXT_PROMPT_KEYBINDING,
};
/// Minimum number of visible lines.
const MIN_ROWS: usize = 1;
/// Minimum number of columns.
///
/// A minimum of 2 is necessary to hold fullwidth unicode characters.
const MIN_COLUMNS: usize = 2;
/// The broadcast channel capacity for PTY reads.
/// This constant was picked arbitrarily. We really shouldn't
/// fall more than this many reads behind the PTY itself anyways.
/// We also don't want to make this too large because we
/// have to pay the cost of pre-allocating memory for the channel
/// (and the larger this is, the more memory we eagerly allocate).
/// TODO: investigate if we can reduce the number of PTY reads we need to buffer
/// per event loop run.
pub const PTY_READS_BROADCAST_CHANNEL_SIZE: usize = 1024;
pub fn init(app: &mut AppContext) {
share_block_modal::init(app);
view::init(app);
}
/// Treat rounding errors for heights within this amount as equal.
pub const HEIGHT_FUDGE_FACTOR_LINES: Lines = Lines::new(0.01);
/// Returns whether two heights in lines are approximately equal.
/// This is an annoying cludge to handle the fact that we're using floating point
/// throughout our block heights code and have to deal with the consequences of accumulated
/// rounding errors.
pub fn heights_approx_eq(a: Lines, b: Lines) -> bool {
(a - b).abs() < HEIGHT_FUDGE_FACTOR_LINES
}
/// Returns whether height a is greater than or equal to height b, allowing
/// for a bit of fudging to account for accumulated rounding errors.
pub fn heights_approx_gte(a: Lines, b: Lines) -> bool {
a > b || heights_approx_eq(a, b)
}
/// Returns whether height a is greater than height b, allowing for a bit of fudging to account
/// for accumulated rounding errors.
pub fn heights_approx_gt(a: Lines, b: Lines) -> bool {
a > b && !heights_approx_eq(a, b)
}
/// Returns whether height a is less than or equal to height b, allowing
/// for a bit of fudging to account for accumulated rounding errors.
pub fn heights_approx_lte(a: Lines, b: Lines) -> bool
where
{
a < b || heights_approx_eq(a, b)
}
/// Returns whether height a is less than height b, allowing for a bit of fudging to account
/// for accumulated rounding errors.
pub fn heights_approx_lt(a: Lines, b: Lines) -> bool {
a < b && !heights_approx_eq(a, b)
}
/// Returns whether the given height is between the start and end heights,
/// allowing for a bit of fudging to account for accumulated rounding errors.
pub fn height_in_range_approx(height: Lines, start: Lines, end: Lines) -> bool {
heights_approx_gte(height, start) && heights_approx_lte(height, end)
}
/// Returns the size of the `SavePosition`-ed element with the given ID from the last layout cycle.
///
/// If this is the first app layout, if if there was no laid-out element with the given ID, returns
/// `None`.
pub(crate) fn element_size_at_last_frame(
element_position_id: &str,
window_id: WindowId,
app: &AppContext,
) -> Option<Vector2F> {
app.element_position_by_id_at_last_frame(window_id, element_position_id)
.map(|position| position.size())
}
/// The reason that the terminal size is being updated
#[derive(Debug, Copy, Clone)]
pub enum SizeUpdateReason {
/// Updated because of some general refresh (e.g. a font-size change)
Refresh,
/// Updated after the temrinal has been laid out, so some of the element
/// sizes that drive terminal size may have changed.
AfterLayout,
/// The shared session sharer's size changed.
/// This is only applicable for shared session viewers.
///
/// The resultant [`SizeUpdate`] will use the larger of the
/// sharer's and viewer's size.
SharerSizeChanged { num_rows: usize, num_cols: usize },
/// A viewer reported its terminal size to the sharer.
/// This is only applicable for shared session sharers.
///
/// The resultant [`SizeUpdate`] will use the viewer's reported
/// size directly (floored at 1 row and 1 column).
ViewerSizeReported { num_rows: usize, num_cols: usize },
}
/// Encapsulates info for updating the size of the terminal.
#[derive(Debug, Copy, Clone)]
pub struct SizeUpdate {
/// The reason for the update.
update_reason: SizeUpdateReason,
/// The last size info.
last_size: SizeInfo,
/// The new size info.
new_size: SizeInfo,
/// The new gap height, if there is one.
new_gap_height: Option<Lines>,
/// The pane-computed rows before any shared session size adjustments.
natural_rows: usize,
/// The pane-computed columns before any shared session size adjustments.
natural_cols: usize,
}
impl SizeUpdate {
/// Whether the reason for the update is a refresh.
pub fn is_refresh(&self) -> bool {
matches!(self.update_reason, SizeUpdateReason::Refresh)
}
/// Returns whether there was any change with this update.
pub fn anything_changed(&self) -> bool {
self.pane_size_changed() || self.gap_height_changed() || self.rows_or_columns_changed()
}
pub fn rows_or_columns_changed(&self) -> bool {
self.last_size.columns() != self.new_size.columns()
|| self.last_size.rows() != self.new_size.rows()
}
/// Returns whether the pane size changed with this update
pub fn pane_size_changed(&self) -> bool {
// It's fine for this to be a near-exact comparison because pane size
// is not determined by summing floats like content element size is.
(self.last_size.pane_size_px().x() - self.new_size.pane_size_px().x()).abs() > f32::EPSILON
|| (self.last_size.pane_size_px().y() - self.new_size.pane_size_px().y()).abs()
> f32::EPSILON
}
/// Returns any new gap height to set with this update
pub fn new_gap_height(&self) -> Option<Lines> {
self.new_gap_height
}
/// Returns whether the gap height changed with this update
pub fn gap_height_changed(&self) -> bool {
self.new_gap_height.is_some()
}
/// The pane-computed natural rows before shared session adjustments.
pub fn natural_rows(&self) -> usize {
self.natural_rows
}
/// The pane-computed natural columns before shared session adjustments.
pub fn natural_cols(&self) -> usize {
self.natural_cols
}
/// Returns true if this resize was caused by a sharer size change.
pub fn is_sharer_size_change(&self) -> bool {
matches!(
self.update_reason,
SizeUpdateReason::SharerSizeChanged { .. }
)
}
}
/// Terminal size info.
///
/// Note that this implements Serialize/Deserialize for ref tests.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct SizeInfo {
/// The width of the TerminalView pane.
///
/// This is basically the `x` of the the incoming max size constraint on the TerminalView. This
/// is represented as a raw float rather than a Vector2F to satisfy the
/// `Serialize`/`Deserialize` trait requirements.
pane_width_px: f32,
/// The height of the TerminalView pane.
///
/// This is basically the `y` of the the incoming max size constraint on the TerminalView. This
/// is represented as a raw float rather than a Vector2F to satisfy the
/// `Serialize`/`Deserialize` trait requirements.
pane_height_px: f32,
/// This height in rows that the pty and grid model thinks the terminal is.
///
/// Note that *rows* is always determined as a function of pane size, not
/// the content element size, which is somewhat counterintuitive. The reason
/// is that the content element size changes frequenetly as the input size
/// changes or the input dissapears for long running commands, but many
/// programs do not handle size changes while they are running very well. To
/// get around this we make them think that rows always comes from the pane size.
rows: usize,
/// This is width in columns that the pty and grid model thinks the terminal is.
columns: usize,
/// Width of an individual cell.
cell_width_px: Pixels,
/// Height of an individual cell.
cell_height_px: Pixels,
/// Horizontal window padding.
padding_x_px: Pixels,
/// Vertical window padding.
padding_y_px: Pixels,
}
/// Helper struct containing the cell size and window padding info.
pub struct CellSizeAndWindowPadding {
/// Width of an individual cell.
pub cell_width_px: Pixels,
/// Height of an individual cell.
pub cell_height_px: Pixels,
/// Horizontal window padding.
pub padding_x_px: Pixels,
/// Vertical window padding.
pub padding_y_px: Pixels,
}
impl SizeInfo {
pub fn new(
pane_size_px: Vector2F,
cell_width_px: Pixels,
cell_height_px: Pixels,
padding_x_px: Pixels,
padding_y_px: Pixels,
) -> SizeInfo {
let rows = (pane_size_px.y() - 2. * padding_y_px.as_f32()) / cell_height_px.as_f32();
let columns = (pane_size_px.x() - 2. * padding_x_px.as_f32()) / cell_width_px.as_f32();
SizeInfo {
pane_width_px: pane_size_px.x(),
pane_height_px: pane_size_px.y(),
columns: max(columns as usize, MIN_COLUMNS),
rows: max(rows as usize, MIN_ROWS),
cell_width_px,
cell_height_px,
padding_x_px: padding_x_px.floor(),
padding_y_px: padding_y_px.floor(),
}
}
/// Create SizeInfo for a [`TerminalModel`] instance that doesn't have font metrics,
/// which comes from either a headless Warp instance or tests.
pub fn new_without_font_metrics(rows: usize, cols: usize) -> Self {
let width = cols as f32;
let height = rows as f32;
SizeInfo::new(
vec2f(width, height),
1.0.into_pixels(), /* cell_width */
1.0.into_pixels(), /* cell_height */
Pixels::zero(), /* padding_x */
Pixels::zero(), /* padding_y */
)
}
pub fn with_rows_and_columns(mut self, rows: usize, cols: usize) -> SizeInfo {
self.columns = cols;
self.rows = rows;
self
}
/// Returns the side of the cell where the mouse is located.
pub fn get_mouse_side(&self, position: Vector2F) -> Side {
let x = position.x() as usize;
let cell_x = x.saturating_sub(self.padding_x_px().as_f32() as usize)
% self.cell_width_px().as_f32() as usize;
let half_cell_width = (self.cell_width_px().as_f32() / 2.0) as usize;
let additional_padding = (self.pane_width_px().as_f32()
- self.padding_x_px().as_f32() * 2.)
% self.cell_width_px().as_f32();
let end_of_grid =
self.pane_width_px() - self.padding_x_px() - additional_padding.into_pixels();
if cell_x > half_cell_width
// Edge case when mouse leaves the window.
|| x as f32 >= end_of_grid.as_f32()
{
Side::Right
} else {
Side::Left
}
}
#[inline]
pub fn pane_size_px(&self) -> Vector2F {
vec2f(self.pane_width_px, self.pane_height_px)
}
#[inline]
pub fn pane_width_px(&self) -> Pixels {
self.pane_width_px.into_pixels()
}
#[inline]
pub fn pane_height_px(&self) -> Pixels {
self.pane_height_px.into_pixels()
}
#[inline]
pub fn rows(&self) -> usize {
self.rows
}
#[inline]
pub fn columns(&self) -> usize {
self.columns
}
#[inline]
pub fn cell_width_px(&self) -> Pixels {
self.cell_width_px
}
#[inline]
pub fn cell_height_px(&self) -> Pixels {
self.cell_height_px
}
#[inline]
pub fn padding_x_px(&self) -> Pixels {
self.padding_x_px
}
#[inline]
pub fn padding_y_px(&self) -> Pixels {
self.padding_y_px
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum ClipboardType {
Clipboard,
Selection,
}
/// The padding around each block, represented in fractional lines.
///
/// TODO(vorporeal): Change this to hold `Lines` instead of `f32`.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct BlockPadding {
/// Padding from top of the block to the prompt.
pub padding_top: f32,
/// Padding from bottom of prompt to top of the command.
pub command_padding_top: f32,
/// Padding from bottom of command to top of output.
pub middle: f32,
/// Padding from bottom of output to bottom of block.
pub bottom: f32,
}
impl BlockPadding {
pub fn new(
padding_top: f32,
command_padding_top: f32,
padding_middle: f32,
padding_bottom: f32,
) -> Self {
BlockPadding {
padding_top,
command_padding_top,
middle: padding_middle,
bottom: padding_bottom,
}
}
}
#[cfg(test)]
mod ref_tests;