mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 00:00:59 +00:00
794 lines
24 KiB
Rust
794 lines
24 KiB
Rust
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, Line, 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, Clone, Copy, PartialEq, Eq)]
|
||
pub enum PromptType {
|
||
KeyboardInteractive,
|
||
Passphrase,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct PromptInfo {
|
||
pub prompt: String,
|
||
pub echo: bool,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub enum BackendCommand {
|
||
Input(Vec<u8>),
|
||
Resize { cols: u16, rows: u16 },
|
||
SampleMetrics,
|
||
Close,
|
||
PromptResponse(Vec<String>),
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub enum BackendEvent {
|
||
PromptRequest {
|
||
tab_id: String,
|
||
prompt_type: PromptType,
|
||
instruction: String,
|
||
prompts: Vec<PromptInfo>,
|
||
},
|
||
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,
|
||
},
|
||
SyncFinished(crate::sync::SyncResult),
|
||
}
|
||
|
||
#[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,
|
||
}
|
||
}
|
||
|
||
/// Return `(grid_line_base, rows_data)` for the **entire** terminal buffer
|
||
/// including scrollback history. `grid_line_base` is the grid line index of
|
||
/// the first row (typically `-history_size`). Each entry in `rows_data` is
|
||
/// a sorted `Vec<(col, char)>` for that row.
|
||
pub fn full_grid_rows(&self) -> (i32, Vec<Vec<(i32, char)>>) {
|
||
let grid = self.term.grid();
|
||
let history = grid.history_size() as i32;
|
||
let screen = grid.screen_lines() as i32;
|
||
let total = history + screen;
|
||
let cols = self.cols as i32;
|
||
let start_line = -history;
|
||
|
||
let mut rows_data: Vec<Vec<(i32, char)>> = Vec::with_capacity(total as usize);
|
||
for line_idx in start_line..(start_line + total) {
|
||
let line = Line(line_idx);
|
||
let mut cells: Vec<(i32, char)> = Vec::new();
|
||
for col_idx in 0..cols {
|
||
let point = Point::new(line, Column(col_idx as usize));
|
||
let c = grid[point].c;
|
||
if c != ' ' && c != '\0' {
|
||
cells.push((col_idx, c));
|
||
}
|
||
}
|
||
rows_data.push(cells);
|
||
}
|
||
(start_line, rows_data)
|
||
}
|
||
|
||
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)]
|
||
pub enum TransferState {
|
||
Running,
|
||
Paused,
|
||
Completed,
|
||
Failed(String),
|
||
Interrupted(String), // 中断传输:包含原因(例如 "User cancelled", "Network timeout")
|
||
Zombie(String), // 程序重启后残留的 Running/Paused 任务
|
||
// 兼容 v0.3.11 -> v0.4.x:旧配置里曾保存过 `Cancelled`,
|
||
// 新版本改成了带原因的状态,因此要手动接住旧枚举值。
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
||
enum TransferStateCompat {
|
||
Running,
|
||
Paused,
|
||
Completed,
|
||
Failed(String),
|
||
Interrupted(String),
|
||
Zombie(String),
|
||
Cancelled,
|
||
}
|
||
|
||
impl From<TransferStateCompat> for TransferState {
|
||
fn from(value: TransferStateCompat) -> Self {
|
||
match value {
|
||
TransferStateCompat::Running => Self::Running,
|
||
TransferStateCompat::Paused => Self::Paused,
|
||
TransferStateCompat::Completed => Self::Completed,
|
||
TransferStateCompat::Failed(reason) => Self::Failed(reason),
|
||
TransferStateCompat::Interrupted(reason) => Self::Interrupted(reason),
|
||
TransferStateCompat::Zombie(reason) => Self::Zombie(reason),
|
||
TransferStateCompat::Cancelled => Self::Interrupted("Cancelled".to_string()),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<'de> serde::Deserialize<'de> for TransferState {
|
||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||
where
|
||
D: serde::Deserializer<'de>,
|
||
{
|
||
TransferStateCompat::deserialize(deserializer).map(Into::into)
|
||
}
|
||
}
|
||
|
||
#[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,
|
||
}
|