Files
ashell/src/terminal/mod.rs
T

710 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
pub mod custom_blocks;
pub mod element;
pub mod input;
use std::sync::mpsc::Sender;
use alacritty_terminal::{
event::{Event, EventListener},
grid::{Dimensions, Scroll},
index::{Column, Point, Side},
selection::{Selection, SelectionRange, SelectionType},
term::{Config, Term, TermMode, cell::Cell, point_to_viewport, viewport_to_point},
vte::ansi::{CursorShape, Processor},
};
use gpui::Keystroke;
use crate::session::config::Session;
use crate::sftp::{PreviewData, RemoteEntry};
use crate::system::SystemSnapshot;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabKind {
Local,
Ssh,
}
#[derive(Debug)]
pub enum BackendCommand {
Input(Vec<u8>),
Resize { cols: u16, rows: u16 },
SampleMetrics,
Close,
}
#[derive(Debug, Clone)]
pub enum BackendEvent {
Output {
tab_id: String,
bytes: Vec<u8>,
},
Status {
tab_id: String,
text: String,
},
Connected {
tab_id: String,
},
SftpEntries {
tab_id: String,
path: String,
entries: Vec<RemoteEntry>,
},
SftpPreview {
tab_id: String,
preview: PreviewData,
},
SftpStatus {
tab_id: String,
text: String,
},
RemoteSystem {
tab_id: String,
snapshot: SystemSnapshot,
},
RemoteSystemUnavailable {
tab_id: String,
reason: String,
},
SftpHome {
tab_id: String,
home: String,
},
TransferProgress {
#[allow(dead_code)]
tab_id: String,
id: String,
transferred: u64,
total: Option<u64>,
state: TransferState,
},
TransferStarted {
tab_id: String,
info: TransferInfo,
},
Closed {
tab_id: String,
reason: String,
},
TerminalTitleChanged {
tab_id: String,
title: String,
},
}
#[derive(Clone)]
pub enum BackendTx {
Local(Sender<BackendCommand>),
Ssh(tokio::sync::mpsc::UnboundedSender<BackendCommand>),
}
impl BackendTx {
pub fn send(&self, command: BackendCommand) {
match self {
Self::Local(tx) => {
let _ = tx.send(command);
}
Self::Ssh(tx) => {
let _ = tx.send(command);
}
}
}
}
pub struct TerminalTab {
pub id: String,
pub title: String,
pub kind: TabKind,
pub status: String,
pub connected: bool,
pub session: Option<Session>,
processor: Processor,
term: Term<TerminalListener>,
cols: u16,
rows: u16,
pub backend: BackendTx,
pub scroll_pixel_y: f32,
}
#[derive(Clone, Copy)]
pub struct CursorState {
pub row: usize,
pub col: usize,
pub shape: CursorShape,
}
#[derive(Clone)]
pub struct RenderCell {
pub row: i32,
pub col: i32,
pub cell: Cell,
}
#[derive(Clone)]
pub struct RenderSnapshot {
pub cells: Vec<RenderCell>,
pub cursor: Option<CursorState>,
pub selection: Option<ViewportSelection>,
pub display_offset: usize,
pub history_size: usize,
pub rows: usize,
pub cols: usize,
}
#[derive(Clone, Copy)]
pub struct ViewportSelection {
pub start_row: usize,
pub start_col: usize,
pub end_row: usize,
pub end_col: usize,
pub is_block: bool,
}
#[derive(Clone, Default)]
pub struct SftpUiState {
pub current_path: String,
pub status: String,
pub entries: Vec<RemoteEntry>,
pub selected_path: Option<String>,
pub preview: Option<PreviewData>,
pub selected_entries: std::collections::HashSet<String>,
pub home_dir: String,
}
impl TerminalTab {
pub fn new_local(
id: String,
title: String,
backend: BackendTx,
events: std::sync::mpsc::Sender<BackendEvent>,
) -> Self {
Self::new(
id,
title,
TabKind::Local,
"local shell".into(),
backend,
events,
)
}
pub fn new_ssh(
id: String,
session: &Session,
backend: BackendTx,
events: std::sync::mpsc::Sender<BackendEvent>,
) -> Self {
let mut tab = Self::new(
id,
session.name.clone(),
TabKind::Ssh,
format!(
"connecting {}@{}:{}",
session.user, session.host, session.port
),
backend,
events,
);
tab.session = Some(session.clone());
tab.connected = false;
tab
}
fn new(
id: String,
title: String,
kind: TabKind,
status: String,
backend: BackendTx,
events: std::sync::mpsc::Sender<BackendEvent>,
) -> Self {
Self {
id: id.clone(),
title,
kind,
status,
connected: matches!(kind, TabKind::Local),
session: None,
processor: Processor::new(),
term: new_term(100, 30, backend.clone(), id, events),
cols: 100,
rows: 30,
backend,
scroll_pixel_y: 0.0,
}
}
pub fn feed(&mut self, bytes: &[u8]) {
self.processor.advance(&mut self.term, bytes);
}
pub fn resize(&mut self, cols: u16, rows: u16) {
let new_cols = cols.max(1);
let new_rows = rows.max(1);
if self.cols != new_cols || self.rows != new_rows {
self.cols = new_cols;
self.rows = new_rows;
tracing::info!(
"[ui] terminal resized to {}x{} (cols x rows)",
self.cols,
self.rows
);
self.term.resize(TerminalSize::new(self.cols, self.rows));
self.backend.send(BackendCommand::Resize { cols, rows });
}
}
pub fn cursor_state(&self) -> Option<CursorState> {
let content = self.term.renderable_content();
if matches!(content.cursor.shape, CursorShape::Hidden) || content.display_offset > 0 {
return None;
}
let row = content.cursor.point.line.0;
if row < 0 {
return None;
}
let row = row as usize;
if row >= self.rows as usize {
return None;
}
Some(CursorState {
row,
col: content.cursor.point.column.0,
shape: content.cursor.shape,
})
}
pub fn app_cursor_mode(&self) -> bool {
self.term.mode().contains(TermMode::APP_CURSOR)
}
pub fn render_snapshot(&self) -> RenderSnapshot {
let rows = self.rows;
let cols = self.cols;
let content = self.term.renderable_content();
let display_offset = content.display_offset as i32;
let mut cells = Vec::with_capacity((rows as usize) * (cols as usize));
for indexed in content.display_iter {
let line = indexed.point.line.0;
let row = line + display_offset;
if row < 0 {
continue;
}
if row >= rows as i32 {
continue;
}
let col = indexed.point.column.0 as i32;
if col >= cols as i32 {
continue;
}
cells.push(RenderCell {
row,
col,
cell: indexed.cell.clone(),
});
}
RenderSnapshot {
cells,
cursor: self.cursor_state(),
selection: viewport_selection_from_range(content.display_offset, &content.selection),
display_offset: content.display_offset,
history_size: self.term.grid().history_size(),
rows: self.rows as usize,
cols: self.cols as usize,
}
}
pub fn scroll_history(&mut self, delta: i32) {
if delta != 0 {
self.term.scroll_display(Scroll::Delta(delta));
}
}
pub fn scroll_up_by(&mut self, lines: usize) {
if lines != 0 {
self.term.scroll_display(Scroll::Delta(lines as i32));
}
}
pub fn scroll_down_by(&mut self, lines: usize) {
if lines != 0 {
self.term.scroll_display(Scroll::Delta(-(lines as i32)));
}
}
pub fn scroll_to_bottom(&mut self) {
self.term.scroll_display(Scroll::Bottom);
}
#[allow(dead_code)]
pub fn has_selection(&self) -> bool {
self.term
.selection_to_string()
.is_some_and(|text| !text.is_empty())
}
pub fn clear_selection(&mut self) {
self.term.selection = None;
}
pub fn selection_text(&self) -> Option<String> {
self.term
.selection_to_string()
.filter(|text| !text.is_empty())
}
pub fn begin_selection(
&mut self,
row: usize,
col: usize,
side: Side,
selection_type: SelectionType,
) {
let point = viewport_to_point(
self.term.grid().display_offset(),
Point::new(row, Column(col)),
);
self.term.selection = Some(Selection::new(selection_type, point, side));
}
pub fn update_selection(&mut self, row: usize, col: usize, side: Side) {
let point = viewport_to_point(
self.term.grid().display_offset(),
Point::new(row, Column(col)),
);
if let Some(selection) = self.term.selection.as_mut() {
selection.update(point, side);
}
}
pub fn paste_text(&mut self, text: &str) {
let paste_text = text
.replace('\x1b', "")
.replace("\r\n", "\r")
.replace('\n', "\r");
self.backend
.send(BackendCommand::Input(paste_text.into_bytes()));
}
}
fn viewport_selection_from_range(
display_offset: usize,
selection: &Option<SelectionRange>,
) -> Option<ViewportSelection> {
let SelectionRange {
start,
end,
is_block,
} = selection.as_ref().copied()?;
let start = point_to_viewport(display_offset, start)?;
let end = point_to_viewport(display_offset, end)?;
Some(ViewportSelection {
start_row: start.line,
start_col: start.column.0,
end_row: end.line,
end_col: end.column.0,
is_block,
})
}
#[derive(Clone)]
struct TerminalListener {
tab_id: String,
backend: BackendTx,
events: std::sync::mpsc::Sender<BackendEvent>,
}
impl EventListener for TerminalListener {
fn send_event(&self, event: Event) {
match event {
Event::PtyWrite(output) => self
.backend
.send(BackendCommand::Input(output.into_bytes())),
Event::TextAreaSizeRequest(format) => {
let size = alacritty_terminal::event::WindowSize {
num_lines: 30,
num_cols: 100,
cell_width: 8,
cell_height: 16,
};
self.backend
.send(BackendCommand::Input(format(size).into_bytes()));
}
Event::Title(title) => {
let _ = self.events.send(BackendEvent::TerminalTitleChanged {
tab_id: self.tab_id.clone(),
title,
});
}
_ => {}
}
}
}
fn new_term(
cols: u16,
rows: u16,
backend: BackendTx,
tab_id: String,
events: std::sync::mpsc::Sender<BackendEvent>,
) -> Term<TerminalListener> {
Term::new(
Config {
scrolling_history: 2000,
..Config::default()
},
&TerminalSize::new(cols, rows),
TerminalListener {
tab_id,
backend,
events,
},
)
}
struct TerminalSize {
cols: usize,
rows: usize,
}
impl TerminalSize {
fn new(cols: u16, rows: u16) -> Self {
Self {
cols: cols.max(1) as usize,
rows: rows.max(1) as usize,
}
}
}
impl Dimensions for TerminalSize {
fn total_lines(&self) -> usize {
self.rows
}
fn screen_lines(&self) -> usize {
self.rows
}
fn columns(&self) -> usize {
self.cols
}
}
pub fn encode_key(
keystroke: &Keystroke,
app_cursor_mode: bool,
option_as_meta: bool,
) -> Option<Vec<u8>> {
zed_like_to_esc_str(keystroke, app_cursor_mode, option_as_meta)
.map(|text| text.into_owned().into_bytes())
}
#[derive(Debug, PartialEq, Eq)]
enum TerminalModifiers {
None,
Alt,
Ctrl,
Shift,
CtrlShift,
Other,
}
impl TerminalModifiers {
fn new(ks: &Keystroke) -> Self {
match (
ks.modifiers.alt,
ks.modifiers.control,
ks.modifiers.shift,
ks.modifiers.platform,
) {
(false, false, false, false) => Self::None,
(true, false, false, false) => Self::Alt,
(false, true, false, false) => Self::Ctrl,
(false, false, true, false) => Self::Shift,
(false, true, true, false) => Self::CtrlShift,
_ => Self::Other,
}
}
fn any(&self) -> bool {
!matches!(self, Self::None)
}
}
fn zed_like_to_esc_str(
keystroke: &Keystroke,
app_cursor_mode: bool,
option_as_meta: bool,
) -> Option<std::borrow::Cow<'static, str>> {
let modifiers = TerminalModifiers::new(keystroke);
let key = keystroke.key.to_ascii_lowercase();
let manual_esc_str = match (key.as_str(), &modifiers) {
("tab", TerminalModifiers::None) => Some("\x09"),
("tab", TerminalModifiers::Shift) => Some("\x1b[Z"),
("escape", TerminalModifiers::None) => Some("\x1b"),
("enter", TerminalModifiers::None) => Some("\x0d"),
("enter", TerminalModifiers::Shift) => Some("\x0a"),
("enter", TerminalModifiers::Alt) => Some("\x1b\x0d"),
("backspace", TerminalModifiers::None) => Some("\x7f"),
("backspace", TerminalModifiers::Ctrl) => Some("\x08"),
("backspace", TerminalModifiers::Alt) => Some("\x1b\x7f"),
("backspace", TerminalModifiers::Shift) => Some("\x7f"),
("space", TerminalModifiers::Ctrl) => Some("\x00"),
("home", TerminalModifiers::None) if app_cursor_mode => Some("\x1bOH"),
("home", TerminalModifiers::None) if !app_cursor_mode => Some("\x1b[H"),
("end", TerminalModifiers::None) if app_cursor_mode => Some("\x1bOF"),
("end", TerminalModifiers::None) if !app_cursor_mode => Some("\x1b[F"),
("up", TerminalModifiers::None) if app_cursor_mode => Some("\x1bOA"),
("up", TerminalModifiers::None) if !app_cursor_mode => Some("\x1b[A"),
("down", TerminalModifiers::None) if app_cursor_mode => Some("\x1bOB"),
("down", TerminalModifiers::None) if !app_cursor_mode => Some("\x1b[B"),
("right", TerminalModifiers::None) if app_cursor_mode => Some("\x1bOC"),
("right", TerminalModifiers::None) if !app_cursor_mode => Some("\x1b[C"),
("left", TerminalModifiers::None) if app_cursor_mode => Some("\x1bOD"),
("left", TerminalModifiers::None) if !app_cursor_mode => Some("\x1b[D"),
("insert", TerminalModifiers::None) => Some("\x1b[2~"),
("delete", TerminalModifiers::None) => Some("\x1b[3~"),
("pageup", TerminalModifiers::None) => Some("\x1b[5~"),
("pagedown", TerminalModifiers::None) => Some("\x1b[6~"),
("a", TerminalModifiers::Ctrl) | ("A", TerminalModifiers::CtrlShift) => Some("\x01"),
("b", TerminalModifiers::Ctrl) | ("B", TerminalModifiers::CtrlShift) => Some("\x02"),
("c", TerminalModifiers::Ctrl) | ("C", TerminalModifiers::CtrlShift) => Some("\x03"),
("d", TerminalModifiers::Ctrl) | ("D", TerminalModifiers::CtrlShift) => Some("\x04"),
("e", TerminalModifiers::Ctrl) | ("E", TerminalModifiers::CtrlShift) => Some("\x05"),
("f", TerminalModifiers::Ctrl) | ("F", TerminalModifiers::CtrlShift) => Some("\x06"),
("g", TerminalModifiers::Ctrl) | ("G", TerminalModifiers::CtrlShift) => Some("\x07"),
("h", TerminalModifiers::Ctrl) | ("H", TerminalModifiers::CtrlShift) => Some("\x08"),
("i", TerminalModifiers::Ctrl) | ("I", TerminalModifiers::CtrlShift) => Some("\x09"),
("j", TerminalModifiers::Ctrl) | ("J", TerminalModifiers::CtrlShift) => Some("\x0a"),
("k", TerminalModifiers::Ctrl) | ("K", TerminalModifiers::CtrlShift) => Some("\x0b"),
("l", TerminalModifiers::Ctrl) | ("L", TerminalModifiers::CtrlShift) => Some("\x0c"),
("m", TerminalModifiers::Ctrl) | ("M", TerminalModifiers::CtrlShift) => Some("\x0d"),
("n", TerminalModifiers::Ctrl) | ("N", TerminalModifiers::CtrlShift) => Some("\x0e"),
("o", TerminalModifiers::Ctrl) | ("O", TerminalModifiers::CtrlShift) => Some("\x0f"),
("p", TerminalModifiers::Ctrl) | ("P", TerminalModifiers::CtrlShift) => Some("\x10"),
("q", TerminalModifiers::Ctrl) | ("Q", TerminalModifiers::CtrlShift) => Some("\x11"),
("r", TerminalModifiers::Ctrl) | ("R", TerminalModifiers::CtrlShift) => Some("\x12"),
("s", TerminalModifiers::Ctrl) | ("S", TerminalModifiers::CtrlShift) => Some("\x13"),
("t", TerminalModifiers::Ctrl) | ("T", TerminalModifiers::CtrlShift) => Some("\x14"),
("u", TerminalModifiers::Ctrl) | ("U", TerminalModifiers::CtrlShift) => Some("\x15"),
("v", TerminalModifiers::Ctrl) | ("V", TerminalModifiers::CtrlShift) => Some("\x16"),
("w", TerminalModifiers::Ctrl) | ("W", TerminalModifiers::CtrlShift) => Some("\x17"),
("x", TerminalModifiers::Ctrl) | ("X", TerminalModifiers::CtrlShift) => Some("\x18"),
("y", TerminalModifiers::Ctrl) | ("Y", TerminalModifiers::CtrlShift) => Some("\x19"),
("z", TerminalModifiers::Ctrl) | ("Z", TerminalModifiers::CtrlShift) => Some("\x1a"),
("@", TerminalModifiers::Ctrl) => Some("\x00"),
("[", TerminalModifiers::Ctrl) => Some("\x1b"),
("\\", TerminalModifiers::Ctrl) => Some("\x1c"),
("]", TerminalModifiers::Ctrl) => Some("\x1d"),
("^", TerminalModifiers::Ctrl) => Some("\x1e"),
("_", TerminalModifiers::Ctrl) => Some("\x1f"),
("?", TerminalModifiers::Ctrl) => Some("\x7f"),
_ => None,
};
if let Some(esc) = manual_esc_str {
return Some(esc.into());
}
if modifiers.any() {
let modifier_code = modifier_code(keystroke);
let modified = match key.as_str() {
"up" => Some(format!("\x1b[1;{}A", modifier_code)),
"down" => Some(format!("\x1b[1;{}B", modifier_code)),
"right" => Some(format!("\x1b[1;{}C", modifier_code)),
"left" => Some(format!("\x1b[1;{}D", modifier_code)),
"insert" => Some(format!("\x1b[2;{}~", modifier_code)),
"pageup" => Some(format!("\x1b[5;{}~", modifier_code)),
"pagedown" => Some(format!("\x1b[6;{}~", modifier_code)),
"end" => Some(format!("\x1b[1;{}F", modifier_code)),
"home" => Some(format!("\x1b[1;{}H", modifier_code)),
_ => None,
};
if let Some(esc) = modified {
return Some(esc.into());
}
}
if !cfg!(target_os = "macos") || option_as_meta {
let is_alt_lowercase_ascii =
modifiers == TerminalModifiers::Alt && keystroke.key.is_ascii();
let is_alt_uppercase_ascii =
keystroke.modifiers.alt && keystroke.modifiers.shift && keystroke.key.is_ascii();
if is_alt_lowercase_ascii || is_alt_uppercase_ascii {
let key = if is_alt_uppercase_ascii {
keystroke.key.to_ascii_uppercase()
} else {
keystroke.key.clone()
};
return Some(format!("\x1b{}", key).into());
}
}
if let Some(text) = &keystroke.key_char {
return Some(text.clone().into());
}
if keystroke.key.len() == 1 {
return Some(keystroke.key.clone().into());
}
None
}
fn modifier_code(keystroke: &Keystroke) -> u32 {
let mut modifier_code = 0;
if keystroke.modifiers.shift {
modifier_code |= 1;
}
if keystroke.modifiers.alt {
modifier_code |= 1 << 1;
}
if keystroke.modifiers.control {
modifier_code |= 1 << 2;
}
modifier_code + 1
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TransferType {
Upload,
Download,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TransferState {
Running,
Paused,
Completed,
Failed(String),
Interrupted(String), // 中断传输:包含原因(例如 "User cancelled", "Network timeout")
Zombie(String), // 程序重启后残留的 Running/Paused 任务
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TransferInfo {
pub id: String,
pub name: String,
pub source: String,
pub target: String,
pub kind: TransferType,
pub total_bytes: Option<u64>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Transfer {
pub tab_id: String,
pub tab_title: String,
pub info: TransferInfo,
pub transferred: u64,
pub total: Option<u64>,
pub state: TransferState,
}