Files
galaxy/app/src/terminal/model/alt_screen.rs
T

692 lines
22 KiB
Rust

use std::collections::HashMap;
use std::io;
use std::ops::{Range, RangeInclusive};
use super::find::RegexDFAs;
use super::grid::RespectDisplayedOutput;
use super::image_map::StoredImageMetadata;
use super::index::Direction;
use super::kitty::{KittyAction, KittyResponse};
use super::secrets::RespectObfuscatedSecrets;
use super::selection::{ExpandedSelectionRange, ScrollDelta};
use crate::terminal::event::Event as TerminalEvent;
use crate::terminal::event_listener::ChannelEventListener;
use crate::terminal::grid_renderer::ColorSampler;
use crate::terminal::model::ansi;
use crate::terminal::model::ansi::{
Attr, CharsetIndex, ClearMode, CommandFinishedValue, CursorShape, CursorStyle, LineClearMode,
Mode, PrecmdValue, PreexecValue, StandardCharset, TabulationClearMode,
};
use crate::terminal::model::grid::grid_handler::{
FragmentBoundary, GridHandler, Link, PerformResetGridChecks, PossiblePath, TermMode,
};
use crate::terminal::model::grid::{Dimensions, GridStorage};
use crate::terminal::model::index::{Point, Side, VisibleRow};
use crate::terminal::model::iterm_image::ITermImage;
use crate::terminal::model::secrets::ObfuscateSecrets;
use crate::terminal::model::selection::{Selection, SelectionRange};
use crate::terminal::{SizeInfo, SizeUpdate};
use itertools::Itertools;
use num_traits::Float as _;
use parking_lot::Mutex;
use pathfinder_color::ColorU;
use std::sync::Arc;
use vec1::Vec1;
use galaxy_core::semantic_selection::SemanticSelection;
use galaxy_terminal::model::{KeyboardModes, KeyboardModesApplyBehavior};
use galaxyui::text::SelectionType;
use galaxyui::units::Lines;
pub struct AltScreen {
grid_handler: GridHandler,
// Number of fractional lines that have yet to scroll the alt grid.
// This number is always between 1. and -1, since the alt grid only supports
// line-based scrolling.
pending_lines_to_scroll: Lines,
/// The current alt screen selection.
/// Do not set this value directly - use [`Self::set_selection`] and [`Self::clear_selection`] instead.
selection: Option<Selection>,
/// If this is Some, and if smart-select is enabled, double-clicking within this range will
/// select this range instead of the normal smart-select logic. The purpose of this is to
/// allow double-click selection to work on the TerminalView::highlighted_link even when it
/// contains spaces. Smart-select never traverses across whitespace.
smart_select_override: Option<RangeInclusive<Point>>,
/// 'Sampler' that samples background color of output grid cells as the alt screen is rendered.
///
/// In some instances, we color-match the background of other UI elements outside the altscreen
/// against the altscreen background.
///
/// This has interior mutability because it's updated at render time, as we render each cell in
/// the output grid.
pub bg_color_sampler: Arc<Mutex<ColorSampler>>,
event_proxy: ChannelEventListener,
}
impl AltScreen {
pub fn new(
size_info: SizeInfo,
max_scroll_limit: usize,
event_proxy: ChannelEventListener,
obfuscate_secrets: ObfuscateSecrets,
) -> Self {
let grid_handler = GridHandler::new(
size_info,
max_scroll_limit,
event_proxy.clone(),
true,
obfuscate_secrets,
PerformResetGridChecks::default(),
);
AltScreen {
grid_handler,
pending_lines_to_scroll: Lines::zero(),
selection: None,
smart_select_override: None,
bg_color_sampler: Arc::new(Mutex::new(ColorSampler::new())),
event_proxy,
}
}
pub fn set_smart_select_override(&mut self, smart_select_override: RangeInclusive<Point>) {
self.smart_select_override = Some(smart_select_override);
}
pub fn clear_smart_select_override(&mut self) {
self.smart_select_override = None;
}
pub(super) fn grid_storage(&self) -> &GridStorage {
self.grid_handler.grid_storage()
}
pub fn cursor_style(&self) -> CursorStyle {
self.grid_handler.cursor_style()
}
pub(super) fn grid_storage_mut(&mut self) -> &mut GridStorage {
self.grid_handler.grid_storage_mut()
}
pub fn grid_handler(&self) -> &GridHandler {
&self.grid_handler
}
pub fn grid_handler_mut(&mut self) -> &mut GridHandler {
&mut self.grid_handler
}
/// Resize terminal to new dimensions.
pub fn resize(&mut self, size_update: &SizeUpdate) {
// Clear any selection on screen resize, to prevent stale anchors
if size_update.rows_or_columns_changed() {
self.clear_selection();
}
self.grid_handler.resize(size_update.new_size);
}
pub fn reset_pending_lines_to_scroll(&mut self) {
self.pending_lines_to_scroll = Lines::zero();
}
/// Accumulates scroll amounts, returning the integral number of whole lines
/// which should be scrolled and retaining any remaining fractional
/// component.
pub fn accumulate_lines_to_scroll(&mut self, delta: Lines) -> i32 {
self.pending_lines_to_scroll += delta;
let whole_lines = self.pending_lines_to_scroll.trunc();
self.pending_lines_to_scroll = self.pending_lines_to_scroll.fract();
whole_lines.as_f64() as i32
}
#[cfg(test)]
pub fn pending_lines_to_scroll(&self) -> Lines {
self.pending_lines_to_scroll
}
pub fn selection_range(
&self,
semantic_selection: &SemanticSelection,
) -> Option<ExpandedSelectionRange<Point>> {
let selection = self.selection().as_ref()?;
let SelectionRange {
start,
end,
is_reversed,
} = selection.to_range(&self.grid_handler, semantic_selection)?;
if start == end {
return Some(ExpandedSelectionRange::Regular {
start,
end,
reversed: is_reversed,
});
}
Some(match selection.ty {
SelectionType::Rect => {
let start_col = start.col.min(end.col);
let end_col = end.col.max(start.col);
// Skip rect selections that will have zero-width.
if start_col == end_col {
return None;
}
// Iterate over each row and create a vec of (start, end) points.
let rows = (start.row..=end.row)
.map(|row| {
(
Point {
row,
col: start_col,
},
Point { row, col: end_col },
)
})
.collect();
ExpandedSelectionRange::Rect {
rows: Vec1::try_from_vec(rows).ok()?,
}
}
_ => ExpandedSelectionRange::Regular {
start,
end,
reversed: is_reversed,
},
})
}
pub fn selection(&self) -> &Option<Selection> {
&self.selection
}
pub fn start_selection(&mut self, point: Point, selection_type: SelectionType, side: Side) {
let mut selection = Selection::new(selection_type, point, side);
selection.set_smart_select_side(Direction::Left);
if let Some(smart_select_override) = &self.smart_select_override {
// We only want to accept this override if it actually wraps around the cursor, so we
// do this comparison to make sure the point of the cursor is between the ends of the
// range of the potential override text.
if smart_select_override.contains(&point) {
selection.set_smart_select_override(smart_select_override.clone());
}
}
self.set_selection(selection);
}
pub fn update_selection(&mut self, point: Point, side: Side) {
let mut selection = match self.selection.take() {
None => return,
Some(selection) => selection,
};
selection.update(point, side);
if selection.is_tail_before_head() {
selection.set_smart_select_side(Direction::Right);
} else {
selection.set_smart_select_side(Direction::Left);
}
self.set_selection(selection);
}
fn set_selection(&mut self, value: Selection) {
self.selection = Some(value);
self.event_proxy
.send_terminal_event(TerminalEvent::TextSelectionChanged);
}
pub fn clear_selection(&mut self) {
self.selection = None;
self.event_proxy
.send_terminal_event(TerminalEvent::TextSelectionChanged);
}
pub fn selection_to_string(&self, semantic_selection: &SemanticSelection) -> Option<String> {
let selection_range = self.selection_range(semantic_selection)?;
Some(match selection_range {
ExpandedSelectionRange::Regular { start, end, .. } => {
self.grid_handler.bounds_to_string(
start,
end,
false, /* include_esc_sequences */
RespectObfuscatedSecrets::Yes,
false, /* force_obfuscated_secrets */
RespectDisplayedOutput::No,
)
}
ExpandedSelectionRange::Rect { rows } => {
rows.into_iter()
.map(|(start, end)| {
self.grid_handler.bounds_to_string(
start,
end,
false, /* include_esc_sequences */
RespectObfuscatedSecrets::Yes,
false, /* force_obfuscated_secrets */
RespectDisplayedOutput::No,
)
})
.join("\n")
}
})
}
pub fn possible_file_paths_at_point(&self, point: Point) -> impl Iterator<Item = PossiblePath> {
self.grid_handler
.possible_file_paths_at_point(point)
.into_iter()
}
pub fn url_at_point(&self, point: &Point) -> Option<Link> {
self.grid_handler.url_at_point(*point)
}
pub fn fragment_boundary_at_point(&self, point: &Point) -> FragmentBoundary {
self.grid_handler.fragment_boundary_at_point(point)
}
pub fn bounds_to_string(
&self,
start: Point,
end: Point,
respect_obfuscated_secrets: RespectObfuscatedSecrets,
) -> String {
self.grid_handler.bounds_to_string(
start,
end,
false, /* include_esc_sequences */
respect_obfuscated_secrets,
false, /* force_obfuscated_secrets */
RespectDisplayedOutput::No,
)
}
pub fn output_to_string(&self) -> String {
self.grid_handler.bounds_to_string(
Point::new(0, 0),
Point::new(
self.grid_handler.total_rows() - 1,
self.grid_handler.columns() - 1,
),
false, /* include_esc_sequences */
RespectObfuscatedSecrets::No,
false, /* force_obfuscated_secrets */
RespectDisplayedOutput::No,
)
}
pub fn needs_bracketed_paste(&self) -> bool {
self.grid_handler.needs_bracketed_paste()
}
pub fn is_mode_set(&self, mode: TermMode) -> bool {
self.grid_handler.is_mode_set(mode)
}
pub fn find(&self, dfas: &RegexDFAs) -> Vec<RangeInclusive<Point>> {
self.grid_handler.find(dfas).collect()
}
/// Sets whether any content within a grid that is "secret-like" should be obfuscated.
pub(super) fn set_obfuscate_secrets(&mut self, obfuscate_secrets: ObfuscateSecrets) {
self.grid_handler.set_obfuscate_secrets(obfuscate_secrets);
}
fn rotate_selection(&mut self, delta: ScrollDelta) {
self.selection = self.selection.take().and_then(|s| {
s.rotate(
self.grid_handler.scroll_region(),
delta,
self.grid_storage().columns(),
)
});
}
fn ansi_handler(&mut self) -> &mut impl ansi::Handler {
self.grid_handler.ansi_handler()
}
pub(super) fn set_marked_text(&mut self, marked_text: &str, selected_range: &Range<usize>) {
self.grid_handler
.set_marked_text(marked_text, selected_range);
}
pub(super) fn clear_marked_text(&mut self) {
self.grid_handler.clear_marked_text();
}
pub fn inferred_bg_color(&self) -> Option<ColorU> {
self.bg_color_sampler
.lock()
.most_common()
.filter(|color| !color.is_fully_transparent())
}
}
impl ansi::Handler for AltScreen {
fn set_title(&mut self, _: Option<String>) {
log::error!("Handler method AltScreen::set_title should never be called. This should be handled by TerminalModel.");
}
fn set_cursor_style(&mut self, style: Option<CursorStyle>) {
self.ansi_handler().set_cursor_style(style);
}
fn set_cursor_shape(&mut self, shape: CursorShape) {
self.ansi_handler().set_cursor_shape(shape);
}
fn input(&mut self, c: char) {
self.ansi_handler().input(c);
}
fn goto(&mut self, row: VisibleRow, col: usize) {
self.ansi_handler().goto(row, col);
}
fn goto_line(&mut self, row: VisibleRow) {
self.ansi_handler().goto_line(row);
}
fn goto_col(&mut self, col: usize) {
self.ansi_handler().goto_col(col);
}
fn insert_blank(&mut self, count: usize) {
self.ansi_handler().insert_blank(count);
}
fn move_up(&mut self, lines: usize) {
self.ansi_handler().move_up(lines);
}
fn move_down(&mut self, lines: usize) {
self.ansi_handler().move_down(lines);
}
fn identify_terminal<W: io::Write>(&mut self, writer: &mut W, intermediate: Option<char>) {
self.ansi_handler().identify_terminal(writer, intermediate);
}
fn report_xtversion<W: io::Write>(&mut self, writer: &mut W) {
self.ansi_handler().report_xtversion(writer);
}
fn device_status<W: io::Write>(&mut self, writer: &mut W, arg: usize) {
self.ansi_handler().device_status(writer, arg);
}
fn move_forward(&mut self, columns: usize) {
self.ansi_handler().move_forward(columns);
}
fn move_backward(&mut self, columns: usize) {
self.ansi_handler().move_backward(columns);
}
fn move_down_and_cr(&mut self, lines: usize) {
self.ansi_handler().move_down_and_cr(lines);
}
fn move_up_and_cr(&mut self, lines: usize) {
self.ansi_handler().move_up_and_cr(lines);
}
fn put_tab(&mut self, count: u16) {
self.ansi_handler().put_tab(count);
}
fn backspace(&mut self) {
self.ansi_handler().backspace();
}
fn carriage_return(&mut self) {
self.ansi_handler().carriage_return();
}
fn linefeed(&mut self) -> ScrollDelta {
let lines_scrolled = self.ansi_handler().linefeed();
self.rotate_selection(lines_scrolled);
lines_scrolled
}
fn bell(&mut self) {
self.ansi_handler().bell();
}
fn substitute(&mut self) {
self.ansi_handler().substitute();
}
fn newline(&mut self) {
self.ansi_handler().newline();
}
fn set_horizontal_tabstop(&mut self) {
self.ansi_handler().set_horizontal_tabstop();
}
fn scroll_up(&mut self, lines: usize) -> ScrollDelta {
let lines_scrolled = self.ansi_handler().scroll_up(lines);
self.rotate_selection(lines_scrolled);
lines_scrolled
}
fn scroll_down(&mut self, lines: usize) -> ScrollDelta {
let lines_scrolled = self.ansi_handler().scroll_down(lines);
self.rotate_selection(lines_scrolled);
lines_scrolled
}
fn insert_blank_lines(&mut self, lines: usize) -> ScrollDelta {
let lines_scrolled = self.ansi_handler().insert_blank_lines(lines);
self.rotate_selection(lines_scrolled);
lines_scrolled
}
fn delete_lines(&mut self, lines: usize) -> ScrollDelta {
let lines_scrolled = self.ansi_handler().delete_lines(lines);
self.rotate_selection(lines_scrolled);
lines_scrolled
}
fn erase_chars(&mut self, count: usize) {
self.ansi_handler().erase_chars(count);
}
fn delete_chars(&mut self, count: usize) {
self.ansi_handler().delete_chars(count)
}
fn move_backward_tabs(&mut self, count: u16) {
self.ansi_handler().move_backward_tabs(count);
}
fn move_forward_tabs(&mut self, count: u16) {
self.ansi_handler().move_forward_tabs(count);
}
fn save_cursor_position(&mut self) {
self.ansi_handler().save_cursor_position();
}
fn restore_cursor_position(&mut self) {
self.ansi_handler().restore_cursor_position();
}
fn clear_line(&mut self, mode: LineClearMode) {
self.ansi_handler().clear_line(mode);
}
fn clear_screen(&mut self, mode: ClearMode) {
self.ansi_handler().clear_screen(mode);
}
fn clear_tabs(&mut self, mode: TabulationClearMode) {
self.ansi_handler().clear_tabs(mode);
}
fn reset_state(&mut self) {
self.ansi_handler().reset_state();
}
fn reverse_index(&mut self) -> ScrollDelta {
let lines_scrolled = self.ansi_handler().reverse_index();
self.rotate_selection(lines_scrolled);
lines_scrolled
}
fn terminal_attribute(&mut self, attr: Attr) {
self.ansi_handler().terminal_attribute(attr);
}
fn set_mode(&mut self, mode: Mode) {
self.ansi_handler().set_mode(mode);
}
fn unset_mode(&mut self, mode: Mode) {
self.ansi_handler().unset_mode(mode);
}
fn set_scrolling_region(&mut self, top: usize, bottom: Option<usize>) {
self.ansi_handler().set_scrolling_region(top, bottom);
}
fn set_keypad_application_mode(&mut self) {
self.ansi_handler().set_keypad_application_mode();
}
fn unset_keypad_application_mode(&mut self) {
self.ansi_handler().unset_keypad_application_mode();
}
fn set_active_charset(&mut self, index: CharsetIndex) {
self.ansi_handler().set_active_charset(index);
}
fn configure_charset(&mut self, index: CharsetIndex, charset: StandardCharset) {
self.ansi_handler().configure_charset(index, charset);
}
fn set_color(&mut self, index: usize, color: ColorU) {
self.ansi_handler().set_color(index, color)
}
fn dynamic_color_sequence<W: io::Write>(
&mut self,
writer: &mut W,
code: u8,
index: usize,
terminator: &str,
) {
self.ansi_handler()
.dynamic_color_sequence(writer, code, index, terminator);
}
fn reset_color(&mut self, index: usize) {
self.ansi_handler().reset_color(index);
}
fn clipboard_store(&mut self, clipboard: u8, base64: &[u8]) {
self.ansi_handler().clipboard_store(clipboard, base64);
}
fn clipboard_load(&mut self, clipboard: u8, terminator: &str) {
self.ansi_handler().clipboard_load(clipboard, terminator);
}
fn decaln(&mut self) {
self.ansi_handler().decaln();
}
fn push_title(&mut self) {
log::error!("Handler method AltScreen::push_title should never be called. This should be handled by TerminalModel.");
}
fn pop_title(&mut self) {
log::error!("Handler method AltScreen::pop_title should never be called. This should be handled by TerminalModel.");
}
fn text_area_size_pixels<W: io::Write>(&mut self, writer: &mut W) {
self.ansi_handler().text_area_size_pixels(writer);
}
fn text_area_size_chars<W: io::Write>(&mut self, writer: &mut W) {
self.ansi_handler().text_area_size_chars(writer);
}
fn command_finished(&mut self, _: CommandFinishedValue) {}
fn precmd(&mut self, _: PrecmdValue) {}
fn preexec(&mut self, _: PreexecValue) {}
fn on_finish_byte_processing(&mut self, input: &ansi::ProcessorInput<'_>) {
self.ansi_handler().on_finish_byte_processing(input);
}
fn on_reset_grid(&mut self) {
self.ansi_handler().on_reset_grid();
}
fn handle_completed_iterm_image(&mut self, image: ITermImage) {
self.ansi_handler().handle_completed_iterm_image(image)
}
fn handle_completed_kitty_action(
&mut self,
action: KittyAction,
metadata: &mut HashMap<u32, StoredImageMetadata>,
) -> Option<KittyResponse> {
self.ansi_handler()
.handle_completed_kitty_action(action, metadata)
}
fn set_keyboard_enhancement_flags(
&mut self,
mode: KeyboardModes,
apply: KeyboardModesApplyBehavior,
) {
self.ansi_handler()
.set_keyboard_enhancement_flags(mode, apply);
}
fn push_keyboard_enhancement_flags(&mut self, mode: KeyboardModes) {
self.ansi_handler().push_keyboard_enhancement_flags(mode);
}
fn pop_keyboard_enhancement_flags(&mut self, count: u16) {
self.ansi_handler().pop_keyboard_enhancement_flags(count);
}
fn query_keyboard_enhancement_flags<W: io::Write>(&mut self, writer: &mut W) {
self.ansi_handler().query_keyboard_enhancement_flags(writer);
}
}
impl Dimensions for AltScreen {
#[inline]
fn total_rows(&self) -> usize {
self.grid_handler.total_rows()
}
#[inline]
fn visible_rows(&self) -> usize {
self.grid_handler.visible_rows()
}
#[inline]
fn columns(&self) -> usize {
self.grid_handler.columns()
}
}
#[cfg(test)]
#[path = "alt_screen_test.rs"]
mod tests;