Files
galaxy/app/src/terminal/prompt_render_helper.rs
T

794 lines
32 KiB
Rust

use std::fmt;
use std::num::NonZeroUsize;
use galaxy_core::semantic_selection::SemanticSelection;
use galaxyui::elements::{
Container, DispatchEventResult, Element, EventHandler, SavePosition, SelectableArea,
SelectionHandle, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::presenter::ChildView;
use galaxyui::units::Pixels;
use galaxyui::{AppContext, EntityId, ModelAsRef, ModelHandle, SingletonEntity, ViewHandle};
use settings::Setting as _;
use super::input::InputRenderStateModel;
use super::model::block::Block;
use super::model::blocks::CachedPromptData;
use super::safe_mode_settings::get_secret_obfuscation_mode;
use super::session_settings::SessionSettings;
use super::settings::TerminalSettings;
use super::shell::ShellType;
use super::{prompt, SizeInfo, TerminalModel};
use crate::ai::blocklist::BlocklistAIInputModel;
use crate::appearance::Appearance;
use crate::context_chips::display::PromptDisplay;
use crate::context_chips::spacing;
use crate::features::FeatureFlag;
use crate::settings::{FontSettings, InputSettings};
use crate::terminal::blockgrid_element::BlockGridElement;
use crate::terminal::grid_size_util::grid_compute_baseline_position_fn;
use crate::terminal::input::get_input_box_top_border_width;
use crate::terminal::ligature_settings::should_use_ligature_rendering;
use crate::terminal::model::blockgrid::BlockGrid;
use crate::terminal::model::session::Sessions;
use crate::terminal::model::ObfuscateSecrets;
use crate::terminal::view::{TerminalAction, PADDING_LEFT as TERMINAL_VIEW_PADDING_LEFT};
use crate::themes::theme::PromptColors;
/// How long we're willing to wait after precmd for a marker-based prompt to appear before we
/// display an empty prompt grid in the input.
///
/// During this grace period, we will display the same prompt that was displayed in the input when
/// the user submitted the newly-completed command. We use a higher duration for PowerShell and
/// MSYS2 b/c it's significantly slower than bash/fish/zsh on Unix.
fn prompt_marker_grace_period(shell_type: Option<ShellType>, is_msys2: bool) -> chrono::Duration {
static POSIX_SHELL_PROMPT_MARKER_GRACE_PERIOD: chrono::Duration =
chrono::Duration::milliseconds(100);
static POWERSHELL_PROMPT_MARKER_GRACE_PERIOD: chrono::Duration = chrono::Duration::seconds(1);
static MSYS2_PROMPT_MARKER_GRACE_PERIOD: chrono::Duration = chrono::Duration::seconds(1);
match (shell_type, is_msys2) {
(Some(ShellType::PowerShell), _) => POWERSHELL_PROMPT_MARKER_GRACE_PERIOD,
(_, true) => MSYS2_PROMPT_MARKER_GRACE_PERIOD,
_ => POSIX_SHELL_PROMPT_MARKER_GRACE_PERIOD,
}
}
pub const LPROMPT_RIGHT_PADDING_SAME_LINE_PROMPT: f32 = 4.;
pub fn should_render_ps1_prompt(terminal_model: &TerminalModel, app: &AppContext) -> bool {
let is_classic_input_enabled = InputSettings::as_ref(app).is_classic_input_enabled(app);
let session_settings = SessionSettings::as_ref(app);
// In the context of session sharing, these values may differ from the local settings i.e.
// if the sharer is using PS1 and the viewer is not (using Warp prompt in non-SLP mode).
// In this case, we still want to render the prompt on the same line (PS1 should ALWAYS be
// rendered on the same line).
// Note that the product behavior for session sharing is normally to respect the local settings
// for prompt cosmetics, but this is an exception!
let active_block = terminal_model.block_list().active_block();
let active_block_honor_ps1 = active_block.honor_ps1();
is_classic_input_enabled && (*session_settings.honor_ps1.value() || active_block_honor_ps1)
}
/// Returns whether the prompt should be rendered on the same line as the input editor's contents.
pub fn should_render_prompt_on_same_line(
is_universal_developer_input: bool,
terminal_model: &TerminalModel,
app: &AppContext,
) -> bool {
// We render the prompt on the same line, in the input editor, if:
// 1. The user is using a custom prompt (PS1)
// 2. The user has the same line prompt setting enabled for their Warp prompt.
// If universal developer input is enabled, ignore PS1 rendering logic
if is_universal_developer_input {
return false;
}
let should_render_ps1 = should_render_ps1_prompt(terminal_model, app);
if FeatureFlag::AgentView.is_enabled() {
should_render_ps1
} else {
let session_settings = SessionSettings::as_ref(app);
should_render_ps1
|| session_settings
.saved_prompt
.value()
.same_line_prompt_enabled()
}
}
/// Returns `true` if the shell or AI prompt should be rendered using the editors
/// `EditorDecoratorElements` API.
///
/// The AI prompt is unconditionally rendered above the input.
pub fn should_render_prompt_using_editor_decorator_elements(
is_universal_developer_input: bool,
ai_input_model: &ModelHandle<BlocklistAIInputModel>,
model: &TerminalModel,
app: &AppContext,
) -> bool {
should_render_prompt_on_same_line(is_universal_developer_input, model, app)
&& (!ai_input_model.as_ref(app).is_ai_input_enabled()
|| FeatureFlag::AgentView.is_enabled())
}
pub(in crate::terminal) struct PromptAndPadding {
pub element: PromptAndPaddingElement,
pub padding_left: f32,
pub padding_right: f32,
}
pub(in crate::terminal) enum PromptAndPaddingElement {
// This is boxed because `Text` is large, and without boxing, it
// bloats the size of the `PromptAndPaddingElement` enum.
Text(Box<Text>),
// This is boxed because `BlockGridElement` is large, and without boxing, it
// bloats the size of the `PromptAndPaddingElement` enum.
BlockGrid(Box<BlockGridElement>),
ContextChips(ViewHandle<PromptDisplay>),
}
impl PromptAndPaddingElement {
pub(in crate::terminal) fn text(&self, ctx: &AppContext) -> String {
match self {
Self::Text(text_element) => text_element.text().to_owned(),
Self::BlockGrid(block_grid_element) => block_grid_element.text(),
Self::ContextChips(view) => view.as_ref(ctx).text(ctx),
}
}
pub(in crate::terminal) fn render(self) -> Box<dyn Element> {
match self {
Self::Text(text_element) => text_element.finish(),
Self::BlockGrid(block_grid_element) => block_grid_element.finish(),
Self::ContextChips(view) => ChildView::new(&view).finish(),
}
}
}
/// Struct used for storing prompt elements in the default, non same-line
/// prompt case.
pub(super) struct PromptElements {
pub(super) lprompt: Option<Box<dyn Element>>,
pub(super) rprompt: Option<Box<dyn Element>>,
}
/// Struct used for storing prompt elements when same-line prompt is toggled on.
pub(super) struct SameLinePromptElements {
// Top n-1 lines of lprompt.
pub(super) lprompt_top: Option<Box<dyn Element>>,
// Bottom (nth) line of lprompt.
pub(super) lprompt_bottom: Option<Box<dyn Element>>,
pub(super) rprompt: Option<Box<dyn Element>>,
}
#[derive(Clone)]
pub struct PromptRenderHelper {
sessions: ModelHandle<Sessions>,
prompt_parent_view_id: EntityId,
prompt_view: ViewHandle<PromptDisplay>,
prompt_selection_state_handle: SelectionHandle,
input_render_state_model_handle: ModelHandle<InputRenderStateModel>,
ai_input_model: ModelHandle<BlocklistAIInputModel>,
}
#[derive(Clone, Copy)]
enum PromptSide {
Left,
Right,
}
impl fmt::Display for PromptSide {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PromptSide::Left => f.write_str("prompt_area"),
PromptSide::Right => f.write_str("rprompt_area"),
}
}
}
impl PromptRenderHelper {
pub(in crate::terminal) fn new(
sessions: ModelHandle<Sessions>,
prompt_view_handle: ViewHandle<PromptDisplay>,
prompt_selection_state_handle: SelectionHandle,
parent_view_id: EntityId,
input_render_state_model_handle: ModelHandle<InputRenderStateModel>,
ai_input_model: ModelHandle<BlocklistAIInputModel>,
) -> Self {
Self {
sessions,
prompt_view: prompt_view_handle,
prompt_selection_state_handle,
prompt_parent_view_id: parent_view_id,
input_render_state_model_handle,
ai_input_model,
}
}
pub fn prompt_view(&self) -> &ViewHandle<PromptDisplay> {
&self.prompt_view
}
/// Returns the block from which we should be retrieving prompt-related data.
pub(in crate::terminal) fn prompt_block<'a>(
&self,
model: &'a TerminalModel,
) -> Option<&'a Block> {
model.prompt_block()
}
pub fn prompt_working_dir(&self, model: &TerminalModel, sessions: &Sessions) -> String {
let block = self.prompt_block(model);
let home_dir = block.and_then(|block| prompt::home_dir_for_block(block, sessions));
if model.block_list().is_bootstrapped() {
prompt::display_path_string(block.and_then(|b| b.pwd()), home_dir.as_deref())
// If the block list is not bootstrapped and there are SSH sessions in the current
// terminal session, we should display the "Starting shell..." message when
// fetching the login_shell information.
} else {
self.bootstrapping_shell_message(model, sessions)
}
}
pub fn has_open_chip_menu(&self, app: &AppContext) -> bool {
self.prompt_view.as_ref(app).has_open_chip_menu(app)
}
fn bootstrapping_shell_message(&self, model: &TerminalModel, sessions: &Sessions) -> String {
use crate::terminal::event::RemoteServerSetupState;
// If a remote server setup is in progress for the pending session,
// show a stage-specific message instead of the generic "Starting shell...".
if let Some(pending_session_id) = model.pending_session_id() {
if let Some(state) = sessions.remote_server_setup_state(pending_session_id) {
return match state {
RemoteServerSetupState::Checking => "Starting shell...".to_string(),
RemoteServerSetupState::Installing {
progress_percent: Some(p),
} => format!("Installing Warp SSH Extension... ({p}%)"),
RemoteServerSetupState::Installing {
progress_percent: None,
} => "Installing Warp SSH Extension...".to_string(),
RemoteServerSetupState::Updating => {
"Updating Warp SSH Extension...".to_string()
}
RemoteServerSetupState::Initializing => "Initializing...".to_string(),
RemoteServerSetupState::Ready => "Starting shell...".to_string(),
// Failed and Unsupported both fall back to the wrapper-only SSH
// flow, so we render the same generic prompt as a normal
// SSH session that doesn't have the remote-server extension.
RemoteServerSetupState::Failed { .. }
| RemoteServerSetupState::Unsupported { .. } => "Starting shell...".to_string(),
};
}
}
if !sessions.is_empty() {
"Starting shell...".to_string()
} else {
format!("Starting {}...", model.shell_launch_state().display_name())
}
}
/// The bootstrapping shell message, wrapped into a [`Text`] element.
fn bootstrapping_shell_text(
&self,
model: &TerminalModel,
appearance: &Appearance,
app: &AppContext,
) -> Text {
let prompt_colors: PromptColors = appearance.theme().clone().into();
let prompt_message = self.bootstrapping_shell_message(model, self.sessions.as_ref(app));
Text::new_inline(
prompt_message,
appearance.monospace_font_family(),
appearance.monospace_font_size(),
)
.with_color(prompt_colors.input_prompt_pwd)
.with_style(Properties::default().weight(Weight::Bold))
.with_line_height_ratio(appearance.line_height_ratio())
.with_compute_baseline_position_fn(grid_compute_baseline_position_fn(
appearance.monospace_font_family(),
))
}
#[allow(clippy::too_many_arguments)]
fn prompt_block_grid_to_prompt_and_padding(
block_grid: BlockGrid,
padding_left: f32,
padding_right: f32,
appearance: &Appearance,
obfuscate_secrets: ObfuscateSecrets,
size_info: SizeInfo,
app: &AppContext,
) -> PromptAndPadding {
let enforce_minimum_contrast = *FontSettings::as_ref(app).enforce_minimum_contrast;
let mut block_grid_element = BlockGridElement::new(
&block_grid,
appearance,
enforce_minimum_contrast,
obfuscate_secrets,
size_info,
);
if should_use_ligature_rendering(app) {
block_grid_element = block_grid_element.with_ligature_rendering();
}
PromptAndPadding {
element: PromptAndPaddingElement::BlockGrid(Box::new(block_grid_element)),
padding_left,
padding_right,
}
}
fn split_prompt_grids(prompt_grid: &BlockGrid) -> (Option<BlockGrid>, Option<BlockGrid>) {
// We check the ABSOLUTE rows to cursor here, to discern whether it is a single line prompt or multiple lines.
let rows_to_cursor = prompt_grid.grid_handler().rows_to_cursor();
if let Some(rows_to_cursor) = NonZeroUsize::new(rows_to_cursor) {
let (prompt_grid, mut bottom_prompt_grid) = prompt_grid.split(rows_to_cursor);
if let Some(bottom_prompt_grid) = &mut bottom_prompt_grid {
bottom_prompt_grid
.grid_handler_mut()
.truncate_to_cursor_rows();
bottom_prompt_grid
.grid_handler_mut()
.truncate_to_cursor_cols();
}
(Some(prompt_grid), bottom_prompt_grid)
} else {
let mut bottom_prompt_grid = prompt_grid.clone();
bottom_prompt_grid
.grid_handler_mut()
.truncate_to_cursor_rows();
bottom_prompt_grid
.grid_handler_mut()
.truncate_to_cursor_cols();
(None, Some(bottom_prompt_grid))
}
}
/// Computes the correct prompt layout, based on whether same line prompt is enabled or not,
/// along with the number of lines in the prompt.
/// Returns: (lprompt_top, lprompt_bottom, rprompt)
fn compute_prompt_layout(
same_line_prompt_enabled: bool,
prompt_grid: &BlockGrid,
rprompt_grid: &BlockGrid,
) -> (Option<BlockGrid>, Option<BlockGrid>, BlockGrid) {
if same_line_prompt_enabled {
let (lprompt_top, lprompt_bottom) = Self::split_prompt_grids(prompt_grid);
let mut truncated_rprompt_grid = rprompt_grid.clone();
truncated_rprompt_grid
.grid_handler_mut()
.truncate_to_cursor_rows();
truncated_rprompt_grid
.grid_handler_mut()
.truncate_to_cursor_cols();
(lprompt_top, lprompt_bottom, truncated_rprompt_grid)
} else {
(Some(prompt_grid.clone()), None, rprompt_grid.clone())
}
}
/// Core logic for rendering the prompt.
///
/// This method reads the prompt data from the TerminalModel and converts it to
/// the appropriate renderable PromptAndPadding structs.
///
/// # Returns
///
/// In the case of default (non same-line prompt):
/// Returns lprompt, None and rprompt
/// In the case of same-line prompt with 1 line prompt:
/// Returns None, lprompt and rprompt
/// In the case of same-line prompt with multiple-line prompt:
/// Returns lprompt top (n-1 lines), lprompt bottom (nth line) and rprompt
///
/// Note that rprompt's value is returned as None in all above cases,
/// if should_display_rprompt is false. There are also a variety of bootstrapping/loading
/// cases which are simply 1 line prompt cases (without an rprompt).
///
pub(in crate::terminal) fn render_prompt(
&self,
model: &TerminalModel,
appearance: &Appearance,
app: &AppContext,
) -> (
Option<PromptAndPadding>,
Option<PromptAndPadding>,
Option<PromptAndPadding>,
) {
let active_block = model.block_list().active_block();
let is_universal_input =
InputSettings::as_ref(app).is_universal_developer_input_enabled(app);
let render_prompt_on_same_line =
should_render_prompt_on_same_line(is_universal_input, model, app);
let padding_right = if should_render_prompt_using_editor_decorator_elements(
is_universal_input,
&self.ai_input_model,
model,
app,
) {
LPROMPT_RIGHT_PADDING_SAME_LINE_PROMPT
} else {
*TERMINAL_VIEW_PADDING_LEFT
};
// If the active block hasn't received the precmd message, we're waiting for the next
// prompt. However, we don't want the UI to flicker so we show the previous prompt
// until the user changes the editor.
if !active_block.has_received_precmd()
&& app
.model(&self.input_render_state_model_handle)
.editor_modified_since_block_finished()
{
let prompt = PromptAndPadding {
element: PromptAndPaddingElement::Text(Box::new(
Text::new_inline(
"Loading prompt...",
appearance.monospace_font_family(),
appearance.monospace_font_size(),
)
.with_color(appearance.theme().disabled_ui_text_color().into())
.with_line_height_ratio(appearance.line_height_ratio())
.with_compute_baseline_position_fn(
grid_compute_baseline_position_fn(appearance.monospace_font_family()),
),
)),
padding_left: 0.,
padding_right,
};
if render_prompt_on_same_line {
(None, Some(prompt), None)
} else {
(Some(prompt), None, None)
}
} else if active_block.honor_ps1()
&& model.block_list().is_bootstrapped()
&& !is_universal_input
{
// Only render PS1 directly if the shell is bootstrapped and universal developer input is disabled.
let prompt_block = self.prompt_block(model).unwrap_or(active_block);
let shell_type = active_block.shell_host().map(|shell| shell.shell_type);
let is_msys2 = active_block
.session_id()
.and_then(|session_id| self.sessions.as_ref(app).get(session_id))
.map(|session| session.is_msys2())
.unwrap_or_default();
let (lprompt_grid_top, lprompt_grid_bottom, rprompt_grid) =
match &model.block_list().cached_prompt_data_from_last_user_block() {
// If we've cached the prompt from the active block, use our
// cached copy instead of the block's current prompt. This
// avoids flicker when a prompt is changed in preexec (e.g.: the
// transient prompt feature of p10k).
// Or, if the prompt is empty and the block is less than
// 50ms old, also use the cached prompt from the previous block (to
// avoid flicker in the brief window between receiving precmd
// metadata and receiving prompt bytes, when using marker-based
// prompts).
Some(CachedPromptData {
prompt_grid,
rprompt_grid,
block_creation_time,
}) if block_creation_time == prompt_block.creation_ts()
|| (prompt_block.is_prompt_empty()
&& chrono::Local::now() - *prompt_block.creation_ts()
< prompt_marker_grace_period(shell_type, is_msys2)) =>
{
Self::compute_prompt_layout(
render_prompt_on_same_line,
prompt_grid,
rprompt_grid,
)
}
// If neither of those conditions apply, simply use the prompt
// grid as-is.
_ => Self::compute_prompt_layout(
render_prompt_on_same_line,
prompt_block.prompt_grid(),
prompt_block.rprompt_grid(),
),
};
// Ignore the default horizontal padding used for grids, as this is
// already applied by the Input.
let mut size_info = app.model(&self.input_render_state_model_handle).size_info();
size_info.padding_x_px = Pixels::zero();
let obfuscate_secrets: ObfuscateSecrets = get_secret_obfuscation_mode(app);
let lprompt_top = lprompt_grid_top.map(|grid| {
Self::prompt_block_grid_to_prompt_and_padding(
grid,
0.,
0.,
appearance,
obfuscate_secrets,
size_info,
app,
)
});
let lprompt_bottom = lprompt_grid_bottom.map(|grid| {
Self::prompt_block_grid_to_prompt_and_padding(
grid,
0.,
0.,
appearance,
obfuscate_secrets,
size_info,
app,
)
});
let rprompt = Self::prompt_block_grid_to_prompt_and_padding(
rprompt_grid,
0.,
0.,
appearance,
obfuscate_secrets,
size_info,
app,
);
let rprompt_val = prompt_block
.should_display_rprompt(&size_info)
.then_some(rprompt);
let lprompt_bottom_val = render_prompt_on_same_line
.then_some(lprompt_bottom)
.flatten();
(lprompt_top, lprompt_bottom_val, rprompt_val)
// If not render the default starting shell message.
} else if model.block_list().active_block().honor_ps1() && !is_universal_input {
let prompt = PromptAndPadding {
element: PromptAndPaddingElement::Text(Box::new(
self.bootstrapping_shell_text(model, appearance, app),
)),
padding_left: 0.,
padding_right,
};
if render_prompt_on_same_line {
(None, Some(prompt), None)
} else {
(Some(prompt), None, None)
}
} else {
let element = {
if model.block_list().is_bootstrapped() {
PromptAndPaddingElement::ContextChips(self.prompt_view.clone())
} else {
PromptAndPaddingElement::Text(Box::new(
self.bootstrapping_shell_text(model, appearance, app),
))
}
};
let prompt = PromptAndPadding {
element,
padding_left: 0.,
padding_right,
};
if render_prompt_on_same_line {
(None, Some(prompt), None)
} else {
(Some(prompt), None, None)
}
}
}
fn render_prompt_area_helper(
&self,
terminal_model: &TerminalModel,
prompt_and_padding: PromptAndPadding,
appearance: &Appearance,
prompt_side: PromptSide,
app: &AppContext,
) -> Option<Box<dyn Element>> {
let is_universal_input =
InputSettings::as_ref(app).is_universal_developer_input_enabled(app);
let should_render_prompt_using_editor_decorator_elements =
should_render_prompt_using_editor_decorator_elements(
is_universal_input,
&self.ai_input_model,
terminal_model,
app,
);
let view_id = self.prompt_parent_view_id;
let position_id = format!("{prompt_side}_{view_id}");
let size_info = app.model(&self.input_render_state_model_handle).size_info();
let terminal_spacing = TerminalSettings::as_ref(app)
.terminal_input_spacing(appearance.line_height_ratio(), app);
let prompt_with_padding_container = Container::new(prompt_and_padding.element.render())
.with_padding_top({
if should_render_prompt_using_editor_decorator_elements {
0.0
} else {
terminal_spacing.block_padding.padding_top * size_info.cell_height_px().as_f32()
- get_input_box_top_border_width()
}
})
.with_padding_left(prompt_and_padding.padding_left)
.with_padding_right(prompt_and_padding.padding_right)
.with_padding_bottom({
if should_render_prompt_using_editor_decorator_elements {
0.0
} else {
terminal_spacing.prompt_to_editor_padding
}
})
.finish();
let semantic_selection = SemanticSelection::as_ref(app);
Some(
SavePosition::new(
EventHandler::new(if FeatureFlag::SelectablePrompt.is_enabled() {
// TODO(Simon): Distinguish right-clicks performed directly within selected text bounds.
// The "Copy" option should only appear when selected text is right-clicked directly.
// Right-clicks performed directly within selected text can be handled separately via
// the `SelectableArea`'s `on_selection_right_click` function. It may be helpful to
// follow the paradigm used for `TerminalAction::BlockListContextMenu`.
SelectableArea::new(
self.prompt_selection_state_handle.clone(),
|_selection_args, _ctx, _| {},
prompt_with_padding_container,
)
.with_word_boundaries_policy(semantic_selection.word_boundary_policy())
.with_smart_select_fn(semantic_selection.smart_select_fn())
.finish()
} else {
prompt_with_padding_container
})
.on_right_mouse_down(move |ctx, _, position| {
let position_id = format!("prompt_area_{view_id}");
let Some(prompt_rect) = ctx.element_position_by_id(position_id) else {
return DispatchEventResult::PropagateToParent;
};
let offset_position = position - prompt_rect.origin();
ctx.dispatch_typed_action(TerminalAction::PromptContextMenu {
position_offset_from_prompt: offset_position,
});
DispatchEventResult::StopPropagation
})
.finish(),
&position_id,
)
.finish(),
)
}
pub(in crate::terminal) fn render_universal_developer_input_prompt(
&self,
model: &TerminalModel,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let element = {
if model.block_list().is_bootstrapped() {
PromptAndPaddingElement::ContextChips(self.prompt_view.clone())
} else {
PromptAndPaddingElement::Text(Box::new(
self.bootstrapping_shell_text(model, appearance, app),
))
}
};
let view_id = self.prompt_parent_view_id;
let position_id = format!("{}_{}", PromptSide::Left, view_id);
let size_info = app.model(&self.input_render_state_model_handle).size_info();
let terminal_spacing = TerminalSettings::as_ref(app)
.terminal_input_spacing(appearance.line_height_ratio(), app);
let prompt_with_padding_container = Container::new(element.render())
.with_padding_top({
(terminal_spacing.block_padding.padding_top * size_info.cell_height_px().as_f32()
- get_input_box_top_border_width())
* spacing::UDI_PROMPT_TOP_PADDING_FACTOR
})
.finish();
SavePosition::new(
EventHandler::new(prompt_with_padding_container)
.on_right_mouse_down(move |ctx, _, position| {
let position_id = format!("prompt_area_{view_id}");
let Some(prompt_rect) = ctx.element_position_by_id(position_id) else {
return DispatchEventResult::PropagateToParent;
};
let offset_position = position - prompt_rect.origin();
ctx.dispatch_typed_action(TerminalAction::PromptContextMenu {
position_offset_from_prompt: offset_position,
});
DispatchEventResult::StopPropagation
})
.finish(),
&position_id,
)
.finish()
}
pub(in crate::terminal) fn render_prompt_areas(
&self,
model: &TerminalModel,
appearance: &Appearance,
app: &AppContext,
) -> PromptElements {
let (lprompt_and_padding_option, _, rprompt_and_padding_option) =
self.render_prompt(model, appearance, app);
let lprompt = lprompt_and_padding_option.and_then(|lprompt_top_and_padding| {
self.render_prompt_area_helper(
model,
lprompt_top_and_padding,
appearance,
PromptSide::Left,
app,
)
});
let rprompt = rprompt_and_padding_option.and_then(|rprompt_and_padding| {
self.render_prompt_area_helper(
model,
rprompt_and_padding,
appearance,
PromptSide::Right,
app,
)
});
PromptElements { lprompt, rprompt }
}
pub(in crate::terminal) fn render_same_line_prompt_areas(
&self,
model: &TerminalModel,
appearance: &Appearance,
app: &AppContext,
) -> SameLinePromptElements {
let (
lprompt_top_and_padding_option,
lprompt_bottom_and_padding_option,
rprompt_and_padding_option,
) = self.render_prompt(model, appearance, app);
let lprompt_top = lprompt_top_and_padding_option.and_then(|lprompt_top_and_padding| {
self.render_prompt_area_helper(
model,
lprompt_top_and_padding,
appearance,
PromptSide::Left,
app,
)
});
let lprompt_bottom =
lprompt_bottom_and_padding_option.and_then(|lprompt_bottom_and_padding| {
self.render_prompt_area_helper(
model,
lprompt_bottom_and_padding,
appearance,
PromptSide::Left,
app,
)
});
let rprompt = rprompt_and_padding_option.and_then(|rprompt_and_padding| {
self.render_prompt_area_helper(
model,
rprompt_and_padding,
appearance,
PromptSide::Right,
app,
)
});
SameLinePromptElements {
lprompt_top,
lprompt_bottom,
rprompt,
}
}
#[cfg(feature = "integration_tests")]
pub fn git_branch(&self, ctx: &AppContext) -> Option<String> {
self.prompt_view
.read(ctx, |prompt_display, ctx| prompt_display.git_branch(ctx))
}
}