Add breadcrumb_location and breadcrumbs functions to item trait which handles rendering of the breadcrumb elements Change breadcrumb toolbar to use these new functions rather than having hard coded breadcrumb logic Add breadcrumb support to the terminal tabs Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
1050 lines
34 KiB
Rust
1050 lines
34 KiB
Rust
pub mod mappings;
|
|
pub mod modal;
|
|
pub mod terminal_container_view;
|
|
pub mod terminal_element;
|
|
pub mod terminal_view;
|
|
|
|
use alacritty_terminal::{
|
|
ansi::{ClearMode, Handler},
|
|
config::{Config, Program, PtyConfig, Scrolling},
|
|
event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
|
|
event_loop::{EventLoop, Msg, Notifier},
|
|
grid::{Dimensions, Scroll as AlacScroll},
|
|
index::{Column, Direction as AlacDirection, Line, Point},
|
|
selection::{Selection, SelectionRange, SelectionType},
|
|
sync::FairMutex,
|
|
term::{
|
|
cell::Cell,
|
|
color::Rgb,
|
|
search::{Match, RegexIter, RegexSearch},
|
|
RenderableCursor, TermMode,
|
|
},
|
|
tty::{self, setup_env},
|
|
Term,
|
|
};
|
|
use anyhow::{bail, Result};
|
|
|
|
use futures::{
|
|
channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
|
|
FutureExt,
|
|
};
|
|
|
|
use mappings::mouse::{
|
|
alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
|
|
};
|
|
use modal::deploy_modal;
|
|
|
|
use procinfo::LocalProcessInfo;
|
|
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
|
|
|
|
use std::{
|
|
collections::{HashMap, VecDeque},
|
|
fmt::Display,
|
|
ops::{Deref, RangeInclusive, Sub},
|
|
os::unix::prelude::AsRawFd,
|
|
path::PathBuf,
|
|
sync::Arc,
|
|
time::{Duration, Instant},
|
|
};
|
|
use thiserror::Error;
|
|
|
|
use gpui::{
|
|
geometry::vector::{vec2f, Vector2F},
|
|
keymap::Keystroke,
|
|
scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
|
|
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
|
|
ScrollWheelEvent, Task,
|
|
};
|
|
|
|
use crate::mappings::{
|
|
colors::{get_color_at_index, to_alac_rgb},
|
|
keys::to_esc_str,
|
|
};
|
|
|
|
///Initialize and register all of our action handlers
|
|
pub fn init(cx: &mut MutableAppContext) {
|
|
cx.add_action(deploy_modal);
|
|
|
|
terminal_view::init(cx);
|
|
terminal_container_view::init(cx);
|
|
}
|
|
|
|
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
|
|
///Scroll multiplier that is set to 3 by default. This will be removed when I
|
|
///Implement scroll bars.
|
|
const SCROLL_MULTIPLIER: f32 = 4.;
|
|
// const MAX_SEARCH_LINES: usize = 100;
|
|
const DEBUG_TERMINAL_WIDTH: f32 = 500.;
|
|
const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
|
|
const DEBUG_CELL_WIDTH: f32 = 5.;
|
|
const DEBUG_LINE_HEIGHT: f32 = 5.;
|
|
|
|
///Upward flowing events, for changing the title and such
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub enum Event {
|
|
TitleChanged,
|
|
BreadcrumbsChanged,
|
|
CloseTerminal,
|
|
Bell,
|
|
Wakeup,
|
|
BlinkChanged,
|
|
SelectionsChanged,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum InternalEvent {
|
|
ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
|
|
Resize(TerminalSize),
|
|
Clear,
|
|
// FocusNextMatch,
|
|
Scroll(AlacScroll),
|
|
ScrollToPoint(Point),
|
|
SetSelection(Option<(Selection, Point)>),
|
|
UpdateSelection(Vector2F),
|
|
Copy,
|
|
}
|
|
|
|
///A translation struct for Alacritty to communicate with us from their event loop
|
|
#[derive(Clone)]
|
|
pub struct ZedListener(UnboundedSender<AlacTermEvent>);
|
|
|
|
impl EventListener for ZedListener {
|
|
fn send_event(&self, event: AlacTermEvent) {
|
|
self.0.unbounded_send(event).ok();
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct TerminalSize {
|
|
cell_width: f32,
|
|
line_height: f32,
|
|
height: f32,
|
|
width: f32,
|
|
}
|
|
|
|
impl TerminalSize {
|
|
pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
|
|
TerminalSize {
|
|
cell_width,
|
|
line_height,
|
|
width: size.x(),
|
|
height: size.y(),
|
|
}
|
|
}
|
|
|
|
pub fn num_lines(&self) -> usize {
|
|
(self.height / self.line_height).floor() as usize
|
|
}
|
|
|
|
pub fn num_columns(&self) -> usize {
|
|
(self.width / self.cell_width).floor() as usize
|
|
}
|
|
|
|
pub fn height(&self) -> f32 {
|
|
self.height
|
|
}
|
|
|
|
pub fn width(&self) -> f32 {
|
|
self.width
|
|
}
|
|
|
|
pub fn cell_width(&self) -> f32 {
|
|
self.cell_width
|
|
}
|
|
|
|
pub fn line_height(&self) -> f32 {
|
|
self.line_height
|
|
}
|
|
}
|
|
impl Default for TerminalSize {
|
|
fn default() -> Self {
|
|
TerminalSize::new(
|
|
DEBUG_LINE_HEIGHT,
|
|
DEBUG_CELL_WIDTH,
|
|
vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl From<TerminalSize> for WindowSize {
|
|
fn from(val: TerminalSize) -> Self {
|
|
WindowSize {
|
|
num_lines: val.num_lines() as u16,
|
|
num_cols: val.num_columns() as u16,
|
|
cell_width: val.cell_width() as u16,
|
|
cell_height: val.line_height() as u16,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Dimensions for TerminalSize {
|
|
/// Note: this is supposed to be for the back buffer's length,
|
|
/// but we exclusively use it to resize the terminal, which does not
|
|
/// use this method. We still have to implement it for the trait though,
|
|
/// hence, this comment.
|
|
fn total_lines(&self) -> usize {
|
|
self.screen_lines()
|
|
}
|
|
|
|
fn screen_lines(&self) -> usize {
|
|
self.num_lines()
|
|
}
|
|
|
|
fn columns(&self) -> usize {
|
|
self.num_columns()
|
|
}
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub struct TerminalError {
|
|
pub directory: Option<PathBuf>,
|
|
pub shell: Option<Shell>,
|
|
pub source: std::io::Error,
|
|
}
|
|
|
|
impl TerminalError {
|
|
pub fn fmt_directory(&self) -> String {
|
|
self.directory
|
|
.clone()
|
|
.map(|path| {
|
|
match path
|
|
.into_os_string()
|
|
.into_string()
|
|
.map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
|
|
{
|
|
Ok(s) => s,
|
|
Err(s) => s,
|
|
}
|
|
})
|
|
.unwrap_or_else(|| {
|
|
let default_dir =
|
|
dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
|
|
match default_dir {
|
|
Some(dir) => format!("<none specified, using home directory> {}", dir),
|
|
None => "<none specified, could not find home directory>".to_string(),
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn shell_to_string(&self) -> Option<String> {
|
|
self.shell.as_ref().map(|shell| match shell {
|
|
Shell::System => "<system shell>".to_string(),
|
|
Shell::Program(p) => p.to_string(),
|
|
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
|
|
})
|
|
}
|
|
|
|
pub fn fmt_shell(&self) -> String {
|
|
self.shell
|
|
.clone()
|
|
.map(|shell| match shell {
|
|
Shell::System => "<system defined shell>".to_string(),
|
|
|
|
Shell::Program(s) => s,
|
|
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
|
|
})
|
|
.unwrap_or_else(|| "<none specified, using system defined shell>".to_string())
|
|
}
|
|
}
|
|
|
|
impl Display for TerminalError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let dir_string: String = self.fmt_directory();
|
|
let shell = self.fmt_shell();
|
|
|
|
write!(
|
|
f,
|
|
"Working directory: {} Shell command: `{}`, IOError: {}",
|
|
dir_string, shell, self.source
|
|
)
|
|
}
|
|
}
|
|
|
|
pub struct TerminalBuilder {
|
|
terminal: Terminal,
|
|
events_rx: UnboundedReceiver<AlacTermEvent>,
|
|
}
|
|
|
|
impl TerminalBuilder {
|
|
pub fn new(
|
|
working_directory: Option<PathBuf>,
|
|
shell: Option<Shell>,
|
|
env: Option<HashMap<String, String>>,
|
|
initial_size: TerminalSize,
|
|
blink_settings: Option<TerminalBlink>,
|
|
alternate_scroll: &AlternateScroll,
|
|
window_id: usize,
|
|
) -> Result<TerminalBuilder> {
|
|
let pty_config = {
|
|
let alac_shell = shell.clone().and_then(|shell| match shell {
|
|
Shell::System => None,
|
|
Shell::Program(program) => Some(Program::Just(program)),
|
|
Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
|
|
});
|
|
|
|
PtyConfig {
|
|
shell: alac_shell,
|
|
working_directory: working_directory.clone(),
|
|
hold: false,
|
|
}
|
|
};
|
|
|
|
let mut env = env.unwrap_or_default();
|
|
|
|
//TODO: Properly set the current locale,
|
|
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
|
|
|
|
let alac_scrolling = Scrolling::default();
|
|
// alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
|
|
|
|
let config = Config {
|
|
pty_config: pty_config.clone(),
|
|
env,
|
|
scrolling: alac_scrolling,
|
|
..Default::default()
|
|
};
|
|
|
|
setup_env(&config);
|
|
|
|
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
|
|
//TODO: Remove with a bounded sender which can be dispatched on &self
|
|
let (events_tx, events_rx) = unbounded();
|
|
//Set up the terminal...
|
|
let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
|
|
|
|
//Start off blinking if we need to
|
|
if let Some(TerminalBlink::On) = blink_settings {
|
|
term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
|
|
}
|
|
|
|
//Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
|
|
if let AlternateScroll::Off = alternate_scroll {
|
|
term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
|
|
}
|
|
|
|
let term = Arc::new(FairMutex::new(term));
|
|
|
|
//Setup the pty...
|
|
let pty = match tty::new(&pty_config, initial_size.into(), window_id as u64) {
|
|
Ok(pty) => pty,
|
|
Err(error) => {
|
|
bail!(TerminalError {
|
|
directory: working_directory,
|
|
shell,
|
|
source: error,
|
|
});
|
|
}
|
|
};
|
|
|
|
let fd = pty.file().as_raw_fd();
|
|
let shell_pid = pty.child().id();
|
|
|
|
//And connect them together
|
|
let event_loop = EventLoop::new(
|
|
term.clone(),
|
|
ZedListener(events_tx.clone()),
|
|
pty,
|
|
pty_config.hold,
|
|
false,
|
|
);
|
|
|
|
//Kick things off
|
|
let pty_tx = event_loop.channel();
|
|
let _io_thread = event_loop.spawn();
|
|
|
|
let terminal = Terminal {
|
|
pty_tx: Notifier(pty_tx),
|
|
term,
|
|
events: VecDeque::with_capacity(10), //Should never get this high.
|
|
last_content: Default::default(),
|
|
cur_size: initial_size,
|
|
last_mouse: None,
|
|
matches: Vec::new(),
|
|
last_synced: Instant::now(),
|
|
sync_task: None,
|
|
selection_head: None,
|
|
shell_fd: fd as u32,
|
|
shell_pid,
|
|
foreground_process_info: None,
|
|
breadcrumb_text: String::new(),
|
|
scroll_px: 0.,
|
|
};
|
|
|
|
Ok(TerminalBuilder {
|
|
terminal,
|
|
events_rx,
|
|
})
|
|
}
|
|
|
|
pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
|
|
//Event loop
|
|
cx.spawn_weak(|this, mut cx| async move {
|
|
use futures::StreamExt;
|
|
|
|
while let Some(event) = self.events_rx.next().await {
|
|
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
|
|
//Process the first event immediately for lowered latency
|
|
this.process_event(&event, cx);
|
|
});
|
|
|
|
'outer: loop {
|
|
let mut events = vec![];
|
|
let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
|
|
|
|
loop {
|
|
futures::select_biased! {
|
|
_ = timer => break,
|
|
event = self.events_rx.next() => {
|
|
if let Some(event) = event {
|
|
events.push(event);
|
|
if events.len() > 100 {
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
if events.is_empty() {
|
|
smol::future::yield_now().await;
|
|
break 'outer;
|
|
} else {
|
|
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
|
|
for event in events {
|
|
this.process_event(&event, cx);
|
|
}
|
|
});
|
|
smol::future::yield_now().await;
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(())
|
|
})
|
|
.detach();
|
|
|
|
self.terminal
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct IndexedCell {
|
|
point: Point,
|
|
cell: Cell,
|
|
}
|
|
|
|
impl Deref for IndexedCell {
|
|
type Target = Cell;
|
|
|
|
#[inline]
|
|
fn deref(&self) -> &Cell {
|
|
&self.cell
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct TerminalContent {
|
|
cells: Vec<IndexedCell>,
|
|
mode: TermMode,
|
|
display_offset: usize,
|
|
selection_text: Option<String>,
|
|
selection: Option<SelectionRange>,
|
|
cursor: RenderableCursor,
|
|
cursor_char: char,
|
|
}
|
|
|
|
impl Default for TerminalContent {
|
|
fn default() -> Self {
|
|
TerminalContent {
|
|
cells: Default::default(),
|
|
mode: Default::default(),
|
|
display_offset: Default::default(),
|
|
selection_text: Default::default(),
|
|
selection: Default::default(),
|
|
cursor: RenderableCursor {
|
|
shape: alacritty_terminal::ansi::CursorShape::Block,
|
|
point: Point::new(Line(0), Column(0)),
|
|
},
|
|
cursor_char: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Terminal {
|
|
pty_tx: Notifier,
|
|
term: Arc<FairMutex<Term<ZedListener>>>,
|
|
events: VecDeque<InternalEvent>,
|
|
last_mouse: Option<(Point, AlacDirection)>,
|
|
pub matches: Vec<RangeInclusive<Point>>,
|
|
cur_size: TerminalSize,
|
|
last_content: TerminalContent,
|
|
last_synced: Instant,
|
|
sync_task: Option<Task<()>>,
|
|
selection_head: Option<Point>,
|
|
breadcrumb_text: String,
|
|
shell_pid: u32,
|
|
shell_fd: u32,
|
|
foreground_process_info: Option<LocalProcessInfo>,
|
|
scroll_px: f32,
|
|
}
|
|
|
|
impl Terminal {
|
|
fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
|
|
match event {
|
|
AlacTermEvent::Title(title) => {
|
|
self.breadcrumb_text = title.to_string();
|
|
cx.emit(Event::BreadcrumbsChanged);
|
|
}
|
|
AlacTermEvent::ResetTitle => {
|
|
self.breadcrumb_text = String::new();
|
|
cx.emit(Event::BreadcrumbsChanged);
|
|
}
|
|
AlacTermEvent::ClipboardStore(_, data) => {
|
|
cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
|
|
}
|
|
AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
|
|
&cx.read_from_clipboard()
|
|
.map(|ci| ci.text().to_string())
|
|
.unwrap_or_else(|| "".to_string()),
|
|
)),
|
|
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
|
|
AlacTermEvent::TextAreaSizeRequest(format) => {
|
|
self.write_to_pty(format(self.cur_size.into()))
|
|
}
|
|
AlacTermEvent::CursorBlinkingChange => {
|
|
cx.emit(Event::BlinkChanged);
|
|
}
|
|
AlacTermEvent::Bell => {
|
|
cx.emit(Event::Bell);
|
|
}
|
|
AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
|
|
AlacTermEvent::MouseCursorDirty => {
|
|
//NOOP, Handled in render
|
|
}
|
|
AlacTermEvent::Wakeup => {
|
|
cx.emit(Event::Wakeup);
|
|
|
|
if self.update_process_info() {
|
|
cx.emit(Event::TitleChanged)
|
|
}
|
|
}
|
|
AlacTermEvent::ColorRequest(idx, fun_ptr) => {
|
|
self.events
|
|
.push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Update the cached process info, returns whether the Zed-relevant info has changed
|
|
fn update_process_info(&mut self) -> bool {
|
|
let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
|
|
if pid < 0 {
|
|
pid = self.shell_pid as i32;
|
|
}
|
|
|
|
if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
|
|
let res = self
|
|
.foreground_process_info
|
|
.as_ref()
|
|
.map(|old_info| {
|
|
process_info.cwd != old_info.cwd || process_info.name != old_info.name
|
|
})
|
|
.unwrap_or(true);
|
|
|
|
self.foreground_process_info = Some(process_info.clone());
|
|
|
|
res
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
///Takes events from Alacritty and translates them to behavior on this view
|
|
fn process_terminal_event(
|
|
&mut self,
|
|
event: &InternalEvent,
|
|
term: &mut Term<ZedListener>,
|
|
cx: &mut ModelContext<Self>,
|
|
) {
|
|
match event {
|
|
InternalEvent::ColorRequest(index, format) => {
|
|
let color = term.colors()[*index].unwrap_or_else(|| {
|
|
let term_style = &cx.global::<Settings>().theme.terminal;
|
|
to_alac_rgb(get_color_at_index(index, &term_style.colors))
|
|
});
|
|
self.write_to_pty(format(color))
|
|
}
|
|
InternalEvent::Resize(mut new_size) => {
|
|
new_size.height = f32::max(new_size.line_height, new_size.height);
|
|
new_size.width = f32::max(new_size.cell_width, new_size.width);
|
|
|
|
self.cur_size = new_size.clone();
|
|
|
|
self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
|
|
|
|
// When this resize happens
|
|
// We go from 737px -> 703px height
|
|
// This means there is 1 less line
|
|
// that means the delta is 1
|
|
// That means the selection is rotated by -1
|
|
|
|
term.resize(new_size);
|
|
}
|
|
InternalEvent::Clear => {
|
|
self.write_to_pty("\x0c".to_string());
|
|
term.clear_screen(ClearMode::Saved);
|
|
}
|
|
InternalEvent::Scroll(scroll) => {
|
|
term.scroll_display(*scroll);
|
|
}
|
|
InternalEvent::SetSelection(selection) => {
|
|
term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
|
|
|
|
if let Some((_, head)) = selection {
|
|
self.selection_head = Some(*head);
|
|
}
|
|
cx.emit(Event::SelectionsChanged)
|
|
}
|
|
InternalEvent::UpdateSelection(position) => {
|
|
if let Some(mut selection) = term.selection.take() {
|
|
let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
|
|
let side = mouse_side(*position, self.cur_size);
|
|
|
|
selection.update(point, side);
|
|
term.selection = Some(selection);
|
|
|
|
self.selection_head = Some(point);
|
|
cx.emit(Event::SelectionsChanged)
|
|
}
|
|
}
|
|
|
|
InternalEvent::Copy => {
|
|
if let Some(txt) = term.selection_to_string() {
|
|
cx.write_to_clipboard(ClipboardItem::new(txt))
|
|
}
|
|
}
|
|
InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
|
|
}
|
|
}
|
|
|
|
pub fn last_content(&self) -> &TerminalContent {
|
|
&self.last_content
|
|
}
|
|
|
|
//To test:
|
|
//- Activate match on terminal (scrolling and selection)
|
|
//- Editor search snapping behavior
|
|
|
|
pub fn activate_match(&mut self, index: usize) {
|
|
if let Some(search_match) = self.matches.get(index).cloned() {
|
|
self.set_selection(Some((make_selection(&search_match), *search_match.end())));
|
|
|
|
self.events
|
|
.push_back(InternalEvent::ScrollToPoint(*search_match.start()));
|
|
}
|
|
}
|
|
|
|
fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
|
|
self.events
|
|
.push_back(InternalEvent::SetSelection(selection));
|
|
}
|
|
|
|
pub fn copy(&mut self) {
|
|
self.events.push_back(InternalEvent::Copy);
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.events.push_back(InternalEvent::Clear)
|
|
}
|
|
|
|
///Resize the terminal and the PTY.
|
|
pub fn set_size(&mut self, new_size: TerminalSize) {
|
|
self.events.push_back(InternalEvent::Resize(new_size))
|
|
}
|
|
|
|
///Write the Input payload to the tty.
|
|
fn write_to_pty(&self, input: String) {
|
|
self.pty_tx.notify(input.into_bytes());
|
|
}
|
|
|
|
pub fn input(&mut self, input: String) {
|
|
self.events
|
|
.push_back(InternalEvent::Scroll(AlacScroll::Bottom));
|
|
self.events.push_back(InternalEvent::SetSelection(None));
|
|
|
|
self.write_to_pty(input);
|
|
}
|
|
|
|
pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
|
|
let esc = to_esc_str(keystroke, &self.last_content.mode);
|
|
if let Some(esc) = esc {
|
|
self.input(esc);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
///Paste text into the terminal
|
|
pub fn paste(&mut self, text: &str) {
|
|
let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
|
|
format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
|
|
} else {
|
|
text.replace("\r\n", "\r").replace('\n', "\r")
|
|
};
|
|
self.input(paste_text)
|
|
}
|
|
|
|
pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
|
|
let term = self.term.clone();
|
|
|
|
let mut terminal = if let Some(term) = term.try_lock_unfair() {
|
|
term
|
|
} else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
|
|
term.lock_unfair() //It's been too long, force block
|
|
} else if let None = self.sync_task {
|
|
//Skip this frame
|
|
let delay = cx.background().timer(Duration::from_millis(16));
|
|
self.sync_task = Some(cx.spawn_weak(|weak_handle, mut cx| async move {
|
|
delay.await;
|
|
cx.update(|cx| {
|
|
if let Some(handle) = weak_handle.upgrade(cx) {
|
|
handle.update(cx, |terminal, cx| {
|
|
terminal.sync_task.take();
|
|
cx.notify();
|
|
});
|
|
}
|
|
});
|
|
}));
|
|
return;
|
|
} else {
|
|
//No lock and delayed rendering already scheduled, nothing to do
|
|
return;
|
|
};
|
|
|
|
if self.update_process_info() {
|
|
cx.emit(Event::TitleChanged);
|
|
}
|
|
|
|
//Note that the ordering of events matters for event processing
|
|
while let Some(e) = self.events.pop_front() {
|
|
self.process_terminal_event(&e, &mut terminal, cx)
|
|
}
|
|
|
|
self.last_content = Self::make_content(&terminal);
|
|
self.last_synced = Instant::now();
|
|
}
|
|
|
|
fn make_content(term: &Term<ZedListener>) -> TerminalContent {
|
|
let content = term.renderable_content();
|
|
TerminalContent {
|
|
cells: content
|
|
.display_iter
|
|
//TODO: Add this once there's a way to retain empty lines
|
|
// .filter(|ic| {
|
|
// !ic.flags.contains(Flags::HIDDEN)
|
|
// && !(ic.bg == Named(NamedColor::Background)
|
|
// && ic.c == ' '
|
|
// && !ic.flags.contains(Flags::INVERSE))
|
|
// })
|
|
.map(|ic| IndexedCell {
|
|
point: ic.point,
|
|
cell: ic.cell.clone(),
|
|
})
|
|
.collect::<Vec<IndexedCell>>(),
|
|
mode: content.mode,
|
|
display_offset: content.display_offset,
|
|
selection_text: term.selection_to_string(),
|
|
selection: content.selection,
|
|
cursor: content.cursor,
|
|
cursor_char: term.grid()[content.cursor.point].c,
|
|
}
|
|
}
|
|
|
|
pub fn focus_in(&self) {
|
|
if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
|
|
self.write_to_pty("\x1b[I".to_string());
|
|
}
|
|
}
|
|
|
|
pub fn focus_out(&self) {
|
|
if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
|
|
self.write_to_pty("\x1b[O".to_string());
|
|
}
|
|
}
|
|
|
|
pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
|
|
match self.last_mouse {
|
|
Some((old_point, old_side)) => {
|
|
if old_point == point && old_side == side {
|
|
false
|
|
} else {
|
|
self.last_mouse = Some((point, side));
|
|
true
|
|
}
|
|
}
|
|
None => {
|
|
self.last_mouse = Some((point, side));
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn mouse_mode(&self, shift: bool) -> bool {
|
|
self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
|
|
}
|
|
|
|
pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
|
|
let position = e.position.sub(origin);
|
|
|
|
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
|
|
let side = mouse_side(position, self.cur_size);
|
|
|
|
if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
|
|
if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
|
|
self.pty_tx.notify(bytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
|
|
let position = e.position.sub(origin);
|
|
|
|
if !self.mouse_mode(e.shift) {
|
|
// Alacritty has the same ordering, of first updating the selection
|
|
// then scrolling 15ms later
|
|
self.events
|
|
.push_back(InternalEvent::UpdateSelection(position));
|
|
|
|
// Doesn't make sense to scroll the alt screen
|
|
if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
|
|
let scroll_delta = match self.drag_line_delta(e) {
|
|
Some(value) => value,
|
|
None => return,
|
|
};
|
|
|
|
let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
|
|
|
|
self.events
|
|
.push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
|
|
self.events
|
|
.push_back(InternalEvent::UpdateSelection(position))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
|
|
//TODO: Why do these need to be doubled? Probably the same problem that the IME has
|
|
let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
|
|
let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
|
|
let scroll_delta = if e.position.y() < top {
|
|
(top - e.position.y()).powf(1.1)
|
|
} else if e.position.y() > bottom {
|
|
-((e.position.y() - bottom).powf(1.1))
|
|
} else {
|
|
return None; //Nothing to do
|
|
};
|
|
Some(scroll_delta)
|
|
}
|
|
|
|
pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
|
|
let position = e.position.sub(origin);
|
|
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
|
|
let side = mouse_side(position, self.cur_size);
|
|
|
|
if self.mouse_mode(e.shift) {
|
|
if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
|
|
self.pty_tx.notify(bytes);
|
|
}
|
|
} else if e.button == MouseButton::Left {
|
|
self.events.push_back(InternalEvent::SetSelection(Some((
|
|
Selection::new(SelectionType::Simple, point, side),
|
|
point,
|
|
))));
|
|
}
|
|
}
|
|
|
|
pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
|
|
let position = e.position.sub(origin);
|
|
|
|
if !self.mouse_mode(e.shift) {
|
|
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
|
|
let side = mouse_side(position, self.cur_size);
|
|
|
|
let selection_type = match e.click_count {
|
|
0 => return, //This is a release
|
|
1 => Some(SelectionType::Simple),
|
|
2 => Some(SelectionType::Semantic),
|
|
3 => Some(SelectionType::Lines),
|
|
_ => None,
|
|
};
|
|
|
|
let selection =
|
|
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
|
|
|
|
if let Some(sel) = selection {
|
|
self.events
|
|
.push_back(InternalEvent::SetSelection(Some((sel, point))));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
|
|
let position = e.position.sub(origin);
|
|
if self.mouse_mode(e.shift) {
|
|
let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
|
|
|
|
if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
|
|
self.pty_tx.notify(bytes);
|
|
}
|
|
} else if e.button == MouseButton::Left {
|
|
// Seems pretty standard to automatically copy on mouse_up for terminals,
|
|
// so let's do that here
|
|
self.copy();
|
|
}
|
|
self.last_mouse = None;
|
|
}
|
|
|
|
///Scroll the terminal
|
|
pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
|
|
let mouse_mode = self.mouse_mode(e.shift);
|
|
|
|
if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
|
|
if mouse_mode {
|
|
let point = mouse_point(
|
|
e.position.sub(origin),
|
|
self.cur_size,
|
|
self.last_content.display_offset,
|
|
);
|
|
|
|
if let Some(scrolls) =
|
|
scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
|
|
{
|
|
for scroll in scrolls {
|
|
self.pty_tx.notify(scroll);
|
|
}
|
|
};
|
|
} else if self
|
|
.last_content
|
|
.mode
|
|
.contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
|
|
&& !e.shift
|
|
{
|
|
self.pty_tx.notify(alt_scroll(scroll_lines))
|
|
} else {
|
|
if scroll_lines != 0 {
|
|
let scroll = AlacScroll::Delta(scroll_lines);
|
|
|
|
self.events.push_back(InternalEvent::Scroll(scroll));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
|
|
let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
|
|
|
|
match e.phase {
|
|
/* Reset scroll state on started */
|
|
Some(gpui::TouchPhase::Started) => {
|
|
self.scroll_px = 0.;
|
|
None
|
|
}
|
|
/* Calculate the appropriate scroll lines */
|
|
Some(gpui::TouchPhase::Moved) => {
|
|
let old_offset = (self.scroll_px / self.cur_size.line_height) as i32;
|
|
|
|
self.scroll_px += e.delta.y() * scroll_multiplier;
|
|
|
|
let new_offset = (self.scroll_px / self.cur_size.line_height) as i32;
|
|
|
|
// Whenever we hit the edges, reset our stored scroll to 0
|
|
// so we can respond to changes in direction quickly
|
|
self.scroll_px %= self.cur_size.height;
|
|
|
|
Some(new_offset - old_offset)
|
|
}
|
|
/* Fall back to delta / line_height */
|
|
None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn find_matches(
|
|
&mut self,
|
|
query: project::search::SearchQuery,
|
|
cx: &mut ModelContext<Self>,
|
|
) -> Task<Vec<RangeInclusive<Point>>> {
|
|
let term = self.term.clone();
|
|
cx.background().spawn(async move {
|
|
let searcher = match query {
|
|
project::search::SearchQuery::Text { query, .. } => {
|
|
RegexSearch::new(query.as_ref())
|
|
}
|
|
project::search::SearchQuery::Regex { query, .. } => {
|
|
RegexSearch::new(query.as_ref())
|
|
}
|
|
};
|
|
|
|
if searcher.is_err() {
|
|
return Vec::new();
|
|
}
|
|
let searcher = searcher.unwrap();
|
|
|
|
let term = term.lock();
|
|
|
|
all_search_matches(&term, &searcher).collect()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for Terminal {
|
|
fn drop(&mut self) {
|
|
self.pty_tx.0.send(Msg::Shutdown).ok();
|
|
}
|
|
}
|
|
|
|
impl Entity for Terminal {
|
|
type Event = Event;
|
|
}
|
|
|
|
fn make_selection(range: &RangeInclusive<Point>) -> Selection {
|
|
let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
|
|
selection.update(*range.end(), AlacDirection::Right);
|
|
selection
|
|
}
|
|
|
|
/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
|
|
/// Iterate over all visible regex matches.
|
|
// fn visible_search_matches<'a, T>(
|
|
// term: &'a Term<T>,
|
|
// regex: &'a RegexSearch,
|
|
// ) -> impl Iterator<Item = Match> + 'a {
|
|
// let viewport_start = Line(-(term.grid().display_offset() as i32));
|
|
// let viewport_end = viewport_start + term.bottommost_line();
|
|
// let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
|
|
// let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
|
|
// start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
|
|
// end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
|
|
|
|
// RegexIter::new(start, end, AlacDirection::Right, term, regex)
|
|
// .skip_while(move |rm| rm.end().line < viewport_start)
|
|
// .take_while(move |rm| rm.start().line <= viewport_end)
|
|
// }
|
|
|
|
fn all_search_matches<'a, T>(
|
|
term: &'a Term<T>,
|
|
regex: &'a RegexSearch,
|
|
) -> impl Iterator<Item = Match> + 'a {
|
|
let start = Point::new(term.grid().topmost_line(), Column(0));
|
|
let end = Point::new(term.grid().bottommost_line(), term.grid().last_column());
|
|
RegexIter::new(start, end, AlacDirection::Right, term, regex)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
pub mod terminal_test_context;
|
|
}
|