feature:1、支持断开重连功能;2、支持更多高亮关键词 (#59)

This commit is contained in:
phygerr
2026-06-26 11:07:25 +08:00
committed by GitHub
parent f0e02e0ce7
commit 273bc2fe0c
11 changed files with 612 additions and 420 deletions
+2
View File
@@ -87,6 +87,8 @@ local_terminal_opened: "local terminal opened"
connecting: "Connecting"
starting_connection: "starting connection..."
connection_failed: "Connection Failed"
session_disconnected: "Session disconnected: %{reason}"
press_enter_to_reconnect: "Press Enter to reconnect"
preview_failed: "Preview failed: %{err}"
downloading_file: "Downloading %{base}..."
+2
View File
@@ -88,6 +88,8 @@ local_terminal_opened: "已打开本地终端"
connecting: "正在连接"
starting_connection: "开始连接..."
connection_failed: "连接失败"
session_disconnected: "会话已断开: %{reason}"
press_enter_to_reconnect: "按 Enter 重新连接"
preview_failed: "预览失败: %{err}"
downloading_file: "正在下载 %{base}..."
+2 -2
View File
@@ -2105,7 +2105,7 @@ impl Ashell {
this.active_dialog = None;
// Send Close command to abort connection if they close the dialog without OK
if let Some(tab) = this.tabs.iter().find(|t| t.id == tab_id_for_close) {
tab.backend.send(crate::terminal::BackendCommand::Close);
tab.send_backend(crate::terminal::BackendCommand::Close);
}
cx.notify();
});
@@ -2118,7 +2118,7 @@ impl Ashell {
responses.push(state.read(cx).text().to_string());
}
if let Some(tab) = this.tabs.iter().find(|t| t.id == tab_id_for_ok) {
tab.backend.send(crate::terminal::BackendCommand::PromptResponse(responses));
tab.send_backend(crate::terminal::BackendCommand::PromptResponse(responses));
}
cx.notify();
});
+23 -88
View File
@@ -259,8 +259,6 @@ pub(crate) struct Ashell {
pub(crate) tabs_scroll_handle: gpui::ScrollHandle,
pub(crate) selector_scroll_handle: gpui::ScrollHandle,
pub(crate) saved_scroll_handle: gpui::ScrollHandle,
pub(crate) connection_scroll_handle: gpui::ScrollHandle,
pub(crate) connection_progress: Option<ConnectionProgress>,
pub(crate) pending_sftp_path_sync: Option<String>,
pub(crate) sftp_context_menu: Option<SftpContextMenuState>,
pub(crate) sftp_creating_folder: bool,
@@ -327,14 +325,6 @@ pub(crate) enum SelectorEntry {
Saved(String),
}
#[derive(Clone)]
pub(crate) struct ConnectionProgress {
pub(crate) tab_id: String,
pub(crate) title: SharedString,
pub(crate) lines: Vec<SharedString>,
pub(crate) failed: bool,
}
#[derive(Clone)]
pub(crate) struct SftpContextMenuState {
pub(crate) remote_path: String,
@@ -615,8 +605,6 @@ impl Ashell {
tabs_scroll_handle: gpui::ScrollHandle::new(),
selector_scroll_handle: gpui::ScrollHandle::new(),
saved_scroll_handle: gpui::ScrollHandle::new(),
connection_scroll_handle: gpui::ScrollHandle::new(),
connection_progress: None,
pending_sftp_path_sync: Some("/".into()),
sftp_context_menu: None,
sftp_creating_folder: false,
@@ -800,36 +788,25 @@ impl Ashell {
match event {
BackendEvent::Output { tab_id, bytes } => {
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
tab.backend_initialized = true;
tab.feed(&bytes);
}
}
BackendEvent::Status { tab_id, text } => {
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
tab.backend_initialized = true;
tab.status = text.clone();
}
if let Some(progress) = self.connection_progress.as_mut() {
if progress.tab_id == tab_id {
progress.lines.push(text.clone().into());
let _idx = progress.lines.len().saturating_sub(1);
self.connection_scroll_handle
.set_offset(point(px(0.), px(-99999.0)));
}
}
self.status = text.into();
}
BackendEvent::Connected { tab_id } => {
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
tab.backend_initialized = true;
tab.connected = true;
tab.disconnected_reason = None;
}
self.sync_system_tab_to_active_group();
self.request_active_system_snapshot();
if self
.connection_progress
.as_ref()
.is_some_and(|progress| progress.tab_id == tab_id && !progress.failed)
{
self.connection_progress = None;
}
}
BackendEvent::PromptRequest {
tab_id,
@@ -905,73 +882,29 @@ impl Ashell {
}
BackendEvent::Closed { tab_id, reason } => {
self.remote_sample_in_flight = false;
let mut tab_title = None;
let mut session_label = None;
let is_stale = self
.tabs
.iter()
.find(|t| t.id == tab_id)
.is_some_and(|tab| {
// After retry_disconnected_tab, the old backend's threads
// may still send Closed events. Skip those — they arrive
// before the new backend sends its first Output/Connected.
// Once backend_initialized is set, any Closed is from the
// current backend and should be processed.
tab.backend_generation > 0 && !tab.backend_initialized
});
if is_stale {
continue;
}
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
tab.connected = false;
tab.status = reason.clone();
tab_title = Some(tab.title.clone());
session_label = tab.session.as_ref().map(|session| {
format!("{}@{}:{}", session.user, session.host, session.port)
});
}
let is_sftp = self.tab_groups.iter().any(|g| g.id == tab_id);
if is_sftp {
tab_title = self.tab_groups.iter().find(|g| g.id == tab_id).map(|g| g.title.clone());
session_label = Some("sftp".to_string());
tab.disconnected_reason = Some(reason.clone());
}
if self.system_tab_id.as_deref() == Some(tab_id.as_str()) {
self.system_status = Some(reason.clone().into());
}
let is_graceful_exit =
reason == "local shell closed" || reason == "ssh session closed";
// Auto-close the pane on graceful exit (e.g. user typed exit)
if is_graceful_exit {
self.handle_tab_close(tab_id.clone());
self.status = reason.into();
self.remote_sample_in_flight = false;
return changed;
}
let mut needs_new_progress = false;
if let Some(progress) = self.connection_progress.as_mut() {
if progress.tab_id == tab_id {
progress.lines.push(reason.clone().into());
let _idx = progress.lines.len().saturating_sub(1);
self.connection_scroll_handle
.set_offset(point(px(0.), px(-99999.0)));
let _ = session_label;
let _ = tab_title;
progress.title = t!("connection_failed").into();
progress.failed = true;
} else if !progress.failed {
// We were showing connecting progress, but another tab dropped!
// Switch to failed state so the user can see it and retry.
progress.tab_id = tab_id.clone();
let msg = format!("{}: {}", tab_title.unwrap_or_default(), reason);
progress.lines.push(msg.into());
self.connection_scroll_handle
.set_offset(point(px(0.), px(-99999.0)));
progress.title = t!("connection_failed").into();
progress.failed = true;
} else {
// Already showing a failure dialog, just append the new failure
let msg = format!("{}: {}", tab_title.unwrap_or_default(), reason);
progress.lines.push(msg.into());
self.connection_scroll_handle
.set_offset(point(px(0.), px(-99999.0)));
}
} else if let Some(_) = session_label {
needs_new_progress = true;
}
if needs_new_progress && !is_graceful_exit {
self.connection_progress = Some(ConnectionProgress {
tab_id: tab_id.clone(),
title: t!("connection_failed").into(),
lines: vec![reason.clone().into()],
failed: true,
});
}
self.status = reason.into();
}
BackendEvent::TransferProgress {
@@ -1114,7 +1047,9 @@ impl Ashell {
return;
}
self.remote_sample_in_flight = true;
backend.send(crate::terminal::BackendCommand::SampleMetrics);
if let Ok(backend) = backend.lock() {
backend.send(crate::terminal::BackendCommand::SampleMetrics);
}
}
pub(crate) fn terminal_ime_bounds_for_range(
+50 -109
View File
@@ -2365,6 +2365,56 @@ impl Ashell {
));
let scrollbar = this.terminal_scrollbars.entry(tab_id.clone()).or_default();
el = el.vertical_scrollbar(scrollbar);
// When disconnected, overlay a reconnect bar at the bottom of the terminal.
// Uses absolute positioning so the terminal element itself is unchanged,
// keeping panel size stable in multi-panel layouts.
let disconnected_reason = this
.tabs
.iter()
.find(|t| t.id == *tab_id)
.and_then(|tab| tab.disconnected_reason.clone());
if let Some(reason) = disconnected_reason {
let tab_id_for_reconnect = tab_id.clone();
el = div()
.size_full()
.relative()
.child(el)
.child(
div()
.absolute()
.bottom_0()
.left_0()
.right_0()
.child(
h_flex()
.w_full()
.items_center()
.gap_2()
.px_3()
.py_1()
.bg(cx.theme().danger.opacity(0.15))
.child(
div()
.text_size(rems(0.85))
.text_color(cx.theme().danger)
.child(t!("session_disconnected", "reason" = reason).to_string()),
)
.child(
div()
.text_size(rems(0.85))
.text_color(cx.theme().muted_foreground)
.child(format!("{}", t!("press_enter_to_reconnect"))),
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, _, cx| {
this.retry_disconnected_tab(&tab_id_for_reconnect, cx);
}),
),
),
);
}
let indicator_color = this
.tabs
.iter()
@@ -2853,115 +2903,6 @@ impl Render for Ashell {
),
)
})
.when_some(self.connection_progress.clone(), |this, progress| {
this.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.bg(Hsla {
h: 0.0,
s: 0.0,
l: 0.0,
a: 0.48,
})
.flex()
.items_center()
.justify_center()
.child(
div()
.w(px(420.))
.p_5()
.rounded_lg()
.border_1()
.border_color(cx.theme().border)
.bg(cx.theme().popover)
.shadow_lg()
.child(
v_flex()
.gap_4()
.child(
Button::new("ssh-connect-progress")
.primary()
.loading(!progress.failed)
.label(progress.title.clone()),
)
.child(
div()
.relative()
.min_h(px(0.))
.max_h(px(220.))
.child(
div()
.id("connection-progress-scroll")
.max_h(px(220.))
.overflow_hidden()
.overflow_y_scroll()
.track_scroll(&self.connection_scroll_handle)
.child(
v_flex().gap_2().children(
progress.lines.iter().cloned().map(|line| {
div()
.text_size(rems(1.0))
.text_color(if progress.failed {
cx.theme().danger
} else {
cx.theme().muted_foreground
})
.child(line)
}),
),
)
)
.child(
div()
.absolute()
.top_0()
.right_0()
.bottom_0()
.w(px(16.))
.child(
Scrollbar::vertical(&self.connection_scroll_handle)
.scrollbar_show(ScrollbarShow::Scrolling)
)
)
)
.when(progress.failed, |this| {
this.child(
h_flex()
.justify_end()
.gap_2()
.child(
Button::new("ssh-connect-progress-retry")
.primary()
.label(t!("retry").to_string())
.on_click(cx.listener(
|this, _, _, cx| {
this.retry_connection_progress(
cx,
)
},
)),
)
.child(
Button::new("ssh-connect-progress-close")
.label(t!("cancel").to_string())
.on_click(cx.listener(
|this, _, _, cx| {
this.cancel_connection_progress(
cx,
)
},
)),
),
)
}),
),
),
)
})
.on_prepaint({
let view = cx.entity().clone();
move |_, window, cx| {
+1 -1
View File
@@ -22,7 +22,7 @@ pub(crate) use app::keybinding_recorder::{
};
pub(crate) use app::{
Ashell, ConnectionProgress, PaneLayout, SelectorEntry, SftpContextMenuState, TabGroup,
Ashell, PaneLayout, SelectorEntry, SftpContextMenuState, TabGroup,
};
fn main() {
+75 -85
View File
@@ -11,7 +11,7 @@ use uuid::Uuid;
use self::config::{AuthMethod, Session};
use crate::{
Ashell, ConnectionProgress, PaneLayout, SelectorEntry, TabGroup,
Ashell, PaneLayout, SelectorEntry, TabGroup,
app::constants::{
DEFAULT_COLS, DEFAULT_ROWS, SIDEBAR_WIDTH, TAB_BAR_HEIGHT, TERMINAL_PADDING_X,
TERMINAL_PADDING_Y,
@@ -533,12 +533,6 @@ impl Ashell {
self.sftp_handles.insert(group_id.clone(), sftp_handle);
self.active_tab = Some(id.clone());
self.pending_sftp_path_sync = Some("/".into());
self.connection_progress = Some(ConnectionProgress {
tab_id: id,
title: t!("connecting").into(),
lines: vec![t!("starting_connection").into()],
failed: false,
});
self.status = "ssh tab opened".into();
cx.notify();
}
@@ -552,60 +546,57 @@ impl Ashell {
cx.notify();
}
pub(crate) fn retry_connection_progress(&mut self, cx: &mut Context<Self>) {
let Some(progress) = self.connection_progress.clone() else {
/// Retry a single disconnected tab by its ID.
/// For SSH tabs: spawns a new SSH connection and restarts SFTP.
/// For local tabs: spawns a new local shell.
///
/// The existing `TerminalTab` (including its `term` scrollback history)
/// is preserved — only the backend is swapped via `set_backend()`.
pub(crate) fn retry_disconnected_tab(&mut self, tab_id: &str, cx: &mut Context<Self>) {
let Some(ix) = self.tabs.iter().position(|t| t.id == tab_id) else {
return;
};
self.connection_progress = None;
let mut groups_to_restart_sftp = std::collections::HashSet::new();
if self.tab_groups.iter().any(|g| g.id == progress.tab_id) {
groups_to_restart_sftp.insert(progress.tab_id.clone());
}
let mut retry_tabs = Vec::new();
for (ix, tab) in self.tabs.iter().enumerate() {
if !tab.connected && tab.session.is_some() {
retry_tabs.push((ix, tab.id.clone(), tab.session.clone().unwrap()));
}
}
if retry_tabs.is_empty() && groups_to_restart_sftp.is_empty() {
cx.notify();
if self.tabs[ix].connected || self.tabs[ix].disconnected_reason.is_none() {
return;
}
for (ix, tab_id, session) in retry_tabs {
// Close old backend
self.tabs[ix].backend.send(BackendCommand::Close);
let is_ssh = self.tabs[ix].session.is_some();
let session = self.tabs[ix].session.clone();
let new_generation = self.tabs[ix].backend_generation + 1;
let cols = self.tabs[ix].cols;
let rows = self.tabs[ix].rows;
// Spawn new backend
// Close old backend (sends Close through the shared Arc<Mutex>)
self.tabs[ix].send_backend(BackendCommand::Close);
if let Some(session) = session {
// SSH tab: spawn new SSH connection
let backend = ssh::spawn_ssh_terminal(
self.runtime.handle(),
tab_id.clone(),
tab_id.to_string(),
session.clone(),
DEFAULT_COLS,
DEFAULT_ROWS,
cols,
rows,
self.events_tx.clone(),
);
// Replace tab state in-place to reuse the UI component
self.tabs[ix] =
TerminalTab::new_ssh(tab_id.clone(), &session, backend, self.events_tx.clone());
// Swap the backend — the Term's internal listener shares the
// same Arc<Mutex<BackendTx>>, so user input is automatically
// routed to the new backend. Terminal history is preserved.
self.tabs[ix].set_backend(backend);
self.tabs[ix].connected = false;
self.tabs[ix].status = "connecting".into();
self.tabs[ix].disconnected_reason = None;
self.tabs[ix].backend_generation = new_generation;
self.tabs[ix].backend_initialized = false;
// Find group to restart SFTP
// Restart SFTP for the group containing this tab
if let Some(group) = self
.tab_groups
.iter()
.find(|g| g.pane_root.contains(&tab_id))
.find(|g| g.pane_root.contains(tab_id))
{
groups_to_restart_sftp.insert(group.id.clone());
}
}
// Restart SFTP for affected groups
for group_id in groups_to_restart_sftp {
if let Some(group) = self.tab_groups.iter_mut().find(|g| g.id == group_id) {
// Use the session of any tab in that group
let group_id = group.id.clone();
let group_session = self
.tabs
.iter()
@@ -613,58 +604,57 @@ impl Ashell {
.and_then(|t| t.session.clone());
if let Some(session) = group_session {
if let Some(old_handle) = self.sftp_handles.remove(&group.id) {
if let Some(old_handle) = self.sftp_handles.remove(&group_id) {
old_handle.close();
}
let sftp_handle = crate::sftp::spawn_sftp(
self.runtime.handle(),
group.id.clone(),
group_id.clone(),
session,
self.events_tx.clone(),
);
self.sftp_handles.insert(group.id.clone(), sftp_handle);
self.sftp_handles.insert(group_id.clone(), sftp_handle);
if let Some(sftp) = group.sftp.as_mut() {
sftp.status = rust_i18n::t!("sftp_connecting").to_string();
if let Some(group) = self.tab_groups.iter_mut().find(|g| g.id == group_id) {
if let Some(sftp) = group.sftp.as_mut() {
sftp.status = rust_i18n::t!("sftp_connecting").to_string();
}
}
}
}
} else {
// Local tab: spawn new local shell
match local::spawn_local_terminal(
tab_id.to_string(),
cols,
rows,
self.events_tx.clone(),
) {
Ok(backend) => {
// Swap the backend — preserves terminal history.
self.tabs[ix].set_backend(backend);
self.tabs[ix].connected = true;
self.tabs[ix].status = "local shell".into();
self.tabs[ix].disconnected_reason = None;
self.tabs[ix].backend_generation = new_generation;
self.tabs[ix].backend_initialized = false;
// Resize the new PTY to match the pane dimensions.
self.tabs[ix].send_backend(BackendCommand::Resize { cols, rows });
}
Err(err) => {
self.status = format!("failed to reopen local terminal: {err:#}").into();
cx.notify();
return;
}
}
}
self.connection_progress = Some(ConnectionProgress {
tab_id: progress.tab_id.clone(),
title: t!("connecting").into(),
lines: vec![t!("starting_connection").into()],
failed: false,
});
self.status = "ssh tabs retrying".into();
cx.notify();
}
pub(crate) fn cancel_connection_progress(&mut self, cx: &mut Context<Self>) {
let Some(progress) = self.connection_progress.clone() else {
return;
};
self.connection_progress = None;
if self.tab_groups.iter().any(|g| g.id == progress.tab_id) {
if let Some(group) = self.tab_groups.iter_mut().find(|g| g.id == progress.tab_id) {
group.sftp = None;
}
self.sftp_handles.remove(&progress.tab_id);
cx.notify();
return;
}
let tabs_to_close: Vec<_> = self
.tabs
.iter()
.filter(|tab| !tab.connected && tab.session.is_some())
.map(|tab| tab.id.clone())
.collect();
for id in tabs_to_close {
self.handle_tab_close(id);
self.status = if is_ssh {
"ssh tab retrying"
} else {
"local tab reopened"
}
.into();
cx.notify();
}
@@ -728,7 +718,7 @@ impl Ashell {
id
);
if let Some(ix) = self.tabs.iter().position(|tab| tab.id == id) {
self.tabs[ix].backend.send(BackendCommand::Close);
self.tabs[ix].send_backend(BackendCommand::Close);
self.tabs.remove(ix);
}
return;
@@ -787,7 +777,7 @@ impl Ashell {
.collect();
for tab_id in &tab_ids {
if let Some(ix) = self.tabs.iter().position(|tab| tab.id == *tab_id) {
self.tabs[ix].backend.send(BackendCommand::Close);
self.tabs[ix].send_backend(BackendCommand::Close);
self.tabs.retain(|t| t.id != *tab_id);
}
}
@@ -799,7 +789,7 @@ impl Ashell {
} else {
// Just remove this tab from the group
if let Some(ix) = self.tabs.iter().position(|tab| tab.id == id) {
self.tabs[ix].backend.send(BackendCommand::Close);
self.tabs[ix].send_backend(BackendCommand::Close);
self.tabs.retain(|t| t.id != id);
}
if let Some(g) = self
-2
View File
@@ -335,5 +335,3 @@ pub fn paint_custom_block(
painted
}
+383 -109
View File
@@ -32,15 +32,59 @@ impl HslaExt for Hsla {
#[derive(Debug, Clone)]
struct HighlightColors {
error: Hsla,
success: Hsla,
warning: Hsla,
info: Hsla,
failure: Hsla,
network: Hsla,
url: Hsla,
port: Hsla,
debug: Hsla,
// Log levels
error: Hsla, // ERROR, ERR
critical: Hsla, // PANIC, FATAL, EMERGENCY, CRITICAL
warning: Hsla, // WARNING, WARN
info: Hsla, // INFO, NOTICE
debug: Hsla, // DEBUG, TRACE, DBG
alert: Hsla, // ALERT
// Status indicators
success: Hsla, // SUCCESS, OK, PASS, DONE, COMPLETED
failure: Hsla, // FAILED, FAIL, FAILURE
pending: Hsla, // PENDING, WAITING, PROCESSING
running: Hsla, // RUNNING, ACTIVE, EXECUTING
stopped: Hsla, // STOPPED, INACTIVE, HALTED, IDLE
skipped: Hsla, // SKIPPED, SKIP
// Network
network_up: Hsla, // UP, ONLINE, CONNECTED
network_down: Hsla, // DOWN, OFFLINE, UNREACHABLE
timeout: Hsla, // TIMEOUT, TIMED OUT
refused: Hsla, // REFUSED, REJECTED, DENIED
// Security & Auth
security: Hsla, // SSH, SSL, TLS, CERTIFICATE
auth: Hsla, // AUTHENTICATED, AUTHORIZED, LOGIN
danger: Hsla, // ROOT, SUDO, PASSWORD, SECRET
// Operations
started: Hsla, // START, BOOT, STARTING
stopped_op: Hsla, // STOP, SHUTDOWN, STOPPING
restart: Hsla, // RESTART, RESTARTING
deploy: Hsla, // DEPLOY, DEPLOYED, DEPLOYMENT
crashed: Hsla, // CRASH, CRASHED, SIGSEGV
// Resources
memory: Hsla, // MEMORY, RAM, SWAP, HEAP
cpu: Hsla, // CPU, PROCESSOR, CORE
disk: Hsla, // DISK, STORAGE, PARTITION, MOUNT
// HTTP codes
http_2xx: Hsla, // 200-299 Success
http_3xx: Hsla, // 300-399 Redirect
http_4xx: Hsla, // 400-499 Client Error
http_5xx: Hsla, // 500-599 Server Error
// Dev / Exceptions
exception: Hsla, // Exception, Traceback, Error type
deprecated: Hsla, // DEPRECATED, TODO, FIXME
// Existing
network: Hsla, // IP addresses
url: Hsla, // http://, https://
port: Hsla, // :22, :443, etc.
}
fn hsla(r: u8, g: u8, b: u8) -> Hsla {
@@ -55,20 +99,154 @@ fn hsla(r: u8, g: u8, b: u8) -> Hsla {
fn highlight_colors() -> HighlightColors {
HighlightColors {
error: hsla(224, 96, 96), // #E06060 red
success: hsla(126, 198, 153), // #7EC699 green
warning: hsla(232, 201, 122), // #E8C97A yellow
info: hsla(108, 180, 238), // #6CB4EE blue
failure: hsla(232, 168, 124), // #E8A87C orange
network: hsla(199, 146, 234), // #C792EA purple
url: hsla( 86, 212, 199), // #56D4C7 teal
port: hsla(130, 170, 200), // #82AAC8 muted teal
debug: hsla(130, 140, 155), // #828C9B gray
// Log levels
error: hsla(224, 96, 96), // #E06060 red
critical: hsla(255, 50, 50), // #FF3232 bright red
warning: hsla(232, 201, 122), // #E8C97A yellow
info: hsla(108, 180, 238), // #6CB4EE blue
debug: hsla(130, 140, 155), // #828C9B gray
alert: hsla(213, 126, 234), // #D57EEA bright magenta
// Status
success: hsla(126, 198, 153), // #7EC699 green
failure: hsla(232, 168, 124), // #E8A87C orange
pending: hsla(232, 201, 122), // #E8C97A yellow
running: hsla( 86, 206, 234), // #56CEEA cyan
stopped: hsla(160, 165, 175), // #A0A5AF gray
skipped: hsla(199, 146, 234), // #C792EA purple
// Network
network_up: hsla(126, 198, 153), // #7EC699 green
network_down: hsla(224, 96, 96), // #E06060 red
timeout: hsla(213, 126, 234), // #D57EEA magenta
refused: hsla(245, 160, 80), // #F5A050 orange
// Security
security: hsla( 86, 206, 234), // #56CEEA cyan
auth: hsla(126, 198, 153), // #7EC699 green
danger: hsla(180, 50, 50), // #B43232 dark red
// Operations
started: hsla(126, 198, 153), // #7EC699 green
stopped_op: hsla(224, 96, 96), // #E06060 red
restart: hsla(232, 201, 122), // #E8C97A yellow
deploy: hsla(100, 210, 140), // #64D28C bright green
crashed: hsla(255, 50, 50), // #FF3232 bright red
// Resources
memory: hsla(199, 146, 234), // #C792EA purple
cpu: hsla( 86, 206, 234), // #56CEEA cyan
disk: hsla(108, 180, 238), // #6CB4EE blue
// HTTP codes
http_2xx: hsla(126, 198, 153), // #7EC699 green
http_3xx: hsla( 86, 206, 234), // #56CEEA cyan
http_4xx: hsla(232, 201, 122), // #E8C97A yellow
http_5xx: hsla(224, 96, 96), // #E06060 red
// Dev
exception: hsla(224, 96, 96), // #E06060 red
deprecated: hsla(245, 160, 80), // #F5A050 orange
// Existing
network: hsla(199, 146, 234), // #C792EA purple
url: hsla( 86, 212, 199), // #56D4C7 teal
port: hsla(130, 170, 200), // #82AAC8 muted teal
}
}
fn is_boundary(c: char) -> bool {
!c.is_ascii_alphanumeric() && c != '_'
/// Highlight all occurrences of keyword list in `text`, writing to `map`.
/// Case-insensitive, matches inside larger words (e.g. "my_ERROR" highlights "ERROR").
/// Each keyword only matches once per position (no overlapping highlights).
fn highlight_keywords(
map: &mut HashMap<(i32, i32), Hsla>,
text: &str,
byte_to_col: &[i32],
row_i32: i32,
keywords: &[&str],
color: Hsla,
) {
for &kw in keywords {
let kw_lower: Vec<u8> = kw.bytes().map(|b| b.to_ascii_lowercase()).collect();
let text_bytes = text.as_bytes();
let text_lower: Vec<u8> = text_bytes.iter().map(|b| b.to_ascii_lowercase()).collect();
let mut start = 0;
while start + kw_lower.len() <= text_lower.len() {
if text_lower[start..].starts_with(&kw_lower) {
let abs = start;
let start_col = byte_to_col[abs];
let end_col = byte_to_col[(abs + kw.len() - 1).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(color);
}
start = abs + kw.len();
} else {
start += 1;
}
}
}
}
/// Highlight HTTP status codes (200, 301, 404, 500, etc.)
/// Only matches specific common HTTP codes, not all 3-digit numbers.
fn highlight_http_codes(
map: &mut HashMap<(i32, i32), Hsla>,
text: &str,
byte_to_col: &[i32],
row_i32: i32,
colors: &HighlightColors,
) {
let bytes = text.as_bytes();
let len = bytes.len();
// Specific HTTP codes to highlight
const HTTP_CODES: &[(u16, bool)] = &[
// 2xx
(200, true), (201, true), (202, true), (204, true), (206, true),
// 3xx
(301, true), (302, true), (304, true), (307, true), (308, true),
// 4xx
(400, true), (401, true), (403, true), (404, true), (405, true),
(408, true), (409, true), (410, true), (422, true), (429, true),
// 5xx
(500, true), (502, true), (503, true), (504, true),
];
for i in 0..len.saturating_sub(2) {
if !bytes[i].is_ascii_digit() || !bytes[i + 1].is_ascii_digit() || !bytes[i + 2].is_ascii_digit() {
continue;
}
let code: u16 = ((bytes[i] - b'0') as u16) * 100
+ ((bytes[i + 1] - b'0') as u16) * 10
+ ((bytes[i + 2] - b'0') as u16);
// Only match specific codes
if !HTTP_CODES.iter().any(|&(c, _)| c == code) {
continue;
}
// Must be at a boundary (not part of a longer number)
let before_ok = i == 0 || !bytes[i - 1].is_ascii_digit();
let after_ok = i + 3 >= len || !bytes[i + 3].is_ascii_digit();
if !before_ok || !after_ok {
continue;
}
let color = match code {
200..=299 => colors.http_2xx,
300..=399 => colors.http_3xx,
400..=499 => colors.http_4xx,
500..=599 => colors.http_5xx,
_ => continue,
};
let start_col = byte_to_col[i];
let end_col = byte_to_col[(i + 2).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(color);
}
}
}
pub fn highlight_cells(
@@ -77,7 +255,6 @@ pub fn highlight_cells(
) -> HashMap<(i32, i32), Hsla> {
let colors = highlight_colors();
// Pre-allocate the outer vector to the size of rows.
let mut row_chars: Vec<Vec<(i32, char)>> = vec![Vec::with_capacity(128); rows];
for rc in cells {
if rc.row < 0 || (rc.row as usize) >= rows {
@@ -91,7 +268,6 @@ pub fn highlight_cells(
let mut map = HashMap::new();
// Reusable buffers to avoid allocation inside the loop
let mut chars_buf = String::with_capacity(128);
let mut byte_to_col: Vec<i32> = Vec::with_capacity(128);
@@ -112,96 +288,210 @@ pub fn highlight_cells(
}
let text = chars_buf.as_str();
// ── 1. Error keywords ──────────────────────────
for kw in &["EMERGENCY", "CRITICAL", "FATAL", "PANIC", "ERROR", "ERR"] {
for m in find_keyword(text, kw) {
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.error);
}
}
}
// ── 1. Critical errors (highest priority) ──────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["PANIC", "EMERGENCY", "FATAL", "SEGFAULT", "CRITICAL",
"OOM", "OUT OF MEMORY", "KERNEL PANIC", "CORE DUMPED", "BUS ERROR"],
colors.critical,
);
// ── 2. Success keywords ───────────────────────────
for kw in &["SUCCESS", "SUCCEEDED", "PASSED", "PASS", "OK"] {
for m in find_keyword(text, kw) {
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.success);
}
}
}
// ── 2. Error keywords ──────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["ERROR", "ERR"], colors.error);
// ── 3. Failure keywords ───────────────────────────
for kw in &["FAILED", "FAILURE", "DENIED", "REJECTED", "TIMEOUT", "FAIL"] {
for m in find_keyword(text, kw) {
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.failure);
}
}
}
// ── 3. Alert ───────────────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["ALERT"], colors.alert);
// ── 4. Warning keywords ───────────────────────────
for kw in &["WARNING", "WARN"] {
for m in find_keyword(text, kw) {
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.warning);
}
}
}
// ── 4. Warning keywords ────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["WARNING", "WARN"], colors.warning);
// ── 5. Info keywords ──────────────────────────────
for kw in &["NOTICE", "INFO"] {
for m in find_keyword(text, kw) {
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.info);
}
}
}
// ── 5. Info keywords ───────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["INFO", "INFORMATION", "NOTICE"], colors.info);
// ── 6. Debug keywords ─────────────────────────────
for kw in &["DEBUG", "DBG", "TRACE"] {
for m in find_keyword(text, kw) {
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + kw.len()).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.debug);
}
}
}
// ── 6. Debug keywords ──────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["DEBUG", "DBG", "TRACE"], colors.debug);
// ── 7. IP addresses ───────────────────────────────
// ── 7. Success status ──────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["SUCCESS", "SUCCEEDED", "SUCCESSFUL", "PASSED", "PASS",
"OK", "DONE", "COMPLETED", "FINISHED", "COMPLETE"],
colors.success,
);
// ── 8. Failure status ──────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["FAILED", "FAILURE", "FAIL", "NOT OK"],
colors.failure,
);
// ── 9. Pending / Waiting ───────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["PENDING", "WAITING", "PROCESSING", "IN PROGRESS", "QUEUED"],
colors.pending,
);
// ── 10. Running / Active ───────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["RUNNING", "ACTIVE", "EXECUTING", "IN_PROGRESS", "LIVE"],
colors.running,
);
// ── 11. Stopped / Inactive ─────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["STOPPED", "INACTIVE", "HALTED", "IDLE", "PAUSED"],
colors.stopped,
);
// ── 12. Skipped ────────────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["SKIPPED", "SKIP", "SKIPPING"], colors.skipped);
// ── 13. Network UP ─────────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["UP", "ONLINE", "CONNECTED", "REACHABLE", "LISTENING",
"ESTABLISHED", "LINK UP"],
colors.network_up,
);
// ── 14. Network DOWN ───────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["DOWN", "OFFLINE", "UNREACHABLE", "DISCONNECTED",
"NOT LISTENING", "LINK DOWN", "NO CARRIER"],
colors.network_down,
);
// ── 15. Timeout ────────────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["TIMEOUT", "TIMED OUT", "TIMEOUTS", "ETIMEDOUT", "SLOW", "LATENCY"],
colors.timeout,
);
// ── 16. Refused / Denied ───────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["REFUSED", "REJECTED", "DENIED", "PERMISSION DENIED",
"ACCESS DENIED", "FORBIDDEN", "BLOCKED", "DROP"],
colors.refused,
);
// ── 17. Security / Protocol ────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["SSH", "SSHD", "SSL", "TLS", "HTTPS", "CERTIFICATE", "CERT",
"FIREWALL", "IPTABLES", "ACL", "WAF"],
colors.security,
);
// ── 18. Authentication ─────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["AUTHENTICATED", "ACCEPTED", "AUTHORIZED", "LOGIN", "LOGOUT",
"LOGGED IN", "LOGGED OUT", "SESSION"],
colors.auth,
);
// ── 19. Danger (root/sudo/secrets) ─────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["ROOT", "SUDO", "UID=0", "PASSWORD", "SECRET", "TOKEN",
"API_KEY", "APIKEY", "PRIVATE KEY", "CREDENTIALS"],
colors.danger,
);
// ── 20. Operations: Start ──────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["STARTED", "START", "STARTING", "BOOT", "BOOTING", "LAUNCHED", "LAUNCH"],
colors.started,
);
// ── 21. Operations: Stop ───────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["STOPPED", "STOP", "STOPPING", "SHUTDOWN", "SHUTTING DOWN", "TERMINATED"],
colors.stopped_op,
);
// ── 22. Operations: Restart ────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["RESTARTED", "RESTART", "RESTARTING", "RELOAD", "RELOADED", "RELOADING"],
colors.restart,
);
// ── 23. Operations: Deploy ─────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["DEPLOYED", "DEPLOYMENT", "DEPLOYING", "DEPLOY",
"ROLLBACK", "ROLLED BACK", "ROLLING BACK", "UPGRADE", "UPGRADED"],
colors.deploy,
);
// ── 24. Operations: Crash ──────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["CRASH", "CRASHED", "CRASHING", "SIGSEGV", "SIGABRT", "SIGKILL",
"DIED", "EXITED", "EXIT CODE", "CORE DUMP"],
colors.crashed,
);
// ── 25. Resources: Memory ──────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["MEMORY", "RAM", "HEAP", "STACK", "SWAP", "MEM"],
colors.memory,
);
// ── 26. Resources: CPU ─────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["CPU", "PROCESSOR", "CORE", "CORES", "THREAD", "THREADS", "LOAD"],
colors.cpu,
);
// ── 27. Resources: Disk ────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["DISK", "STORAGE", "PARTITION", "MOUNT", "FILESYSTEM",
"INODE", "IOPS", "READ", "WRITE"],
colors.disk,
);
// ── 28. Dev: Exceptions ────────────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["EXCEPTION", "TRACEBACK", "THROW", "THROWN", "STACKTRACE",
"TYPEERROR", "VALUEERROR", "KEYERROR", "ATTRIBUTEERROR",
"INDEXERROR", "RUNTIMEERROR", "IOERROR", "OSERROR",
"NULLPOINTER", "NPE", "SEGFAULT"],
colors.exception,
);
// ── 29. Dev: Deprecated / TODO ─────────────────────────
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
&["DEPRECATED", "TODO", "FIXME", "HACK", "XXX", "WORKAROUND", "TEMPORARY"],
colors.deprecated,
);
// ── 30. HTTP status codes ──────────────────────────────
highlight_http_codes(&mut map, text, &byte_to_col, row_i32, &colors);
// ── 31. IP addresses ───────────────────────────────────
for m in find_ip_addresses(text) {
let ip_len = find_ip_len(&text[m..]);
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + find_ip_len(&text[m..])).min(byte_to_col.len() - 1)];
let end_col = byte_to_col[(m + ip_len - 1).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.network);
}
}
// ── 8. URLs ───────────────────────────────────────
// ── 32. URLs ───────────────────────────────────────────
for m in find_urls(text) {
let url_len = find_url_len(&text[m..]);
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + url_len).min(byte_to_col.len() - 1)];
let end_col = byte_to_col[(m + url_len - 1).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.url);
}
}
// ── 9. Port numbers ─────────────────────────────────────
// ── 33. Port numbers ───────────────────────────────────
for m in find_ports(text) {
let port_len = find_port_len(&text[m..]);
let start_col = byte_to_col[m];
let end_col = byte_to_col[(m + port_len).min(byte_to_col.len() - 1)];
let end_col = byte_to_col[(m + port_len - 1).min(byte_to_col.len() - 1)];
for c in start_col..=end_col {
map.entry((row_i32, c)).or_insert(colors.port);
}
@@ -211,26 +501,6 @@ pub fn highlight_cells(
map
}
fn find_keyword(text: &str, keyword: &str) -> Vec<usize> {
let mut positions = Vec::new();
let mut start = 0;
while let Some(pos) = text[start..].find(keyword) {
let abs = start + pos;
let before_ok = abs == 0
|| text.as_bytes()[abs - 1] == b' '
|| is_boundary(text.as_bytes()[abs - 1] as char);
let after_pos = abs + keyword.len();
let after_ok = after_pos >= text.len()
|| text.as_bytes()[after_pos] == b' '
|| is_boundary(text.as_bytes()[after_pos] as char);
if before_ok && after_ok {
positions.push(abs);
}
start = abs + keyword.len();
}
positions
}
fn find_ip_len(text: &str) -> usize {
let bytes = text.as_bytes();
let mut dots = 0u8;
@@ -255,7 +525,7 @@ fn find_ip_len(text: &str) -> usize {
}
digits = 0;
}
_ => break,
_ => break, // Stop at first non-digit/non-dot (including '/')
}
len += 1;
}
@@ -292,6 +562,10 @@ fn find_ip_addresses(text: &str) -> Vec<usize> {
positions
}
fn is_boundary(c: char) -> bool {
!c.is_ascii_alphanumeric() && c != '_'
}
fn find_urls(text: &str) -> Vec<usize> {
let mut positions = Vec::new();
let mut start = 0;
+28 -8
View File
@@ -112,6 +112,29 @@ impl Ashell {
}
}
// If the active tab is disconnected and user presses Enter, reconnect
if event.keystroke.key == "enter"
&& !event.keystroke.modifiers.shift
&& !event.keystroke.modifiers.control
&& !event.keystroke.modifiers.alt
&& !event.keystroke.modifiers.platform
{
let active_id = self.active_tab.clone();
if let Some(active_id) = active_id {
let is_disconnected = self
.tabs
.iter()
.find(|t| t.id == active_id)
.is_some_and(|tab| tab.disconnected_reason.is_some());
if is_disconnected {
self.retry_disconnected_tab(&active_id, cx);
window.prevent_default();
cx.stop_propagation();
return;
}
}
}
if event.prefer_character_input {
if let Some(text) = event.keystroke.key_char.as_deref() {
if !text.is_empty()
@@ -138,7 +161,7 @@ impl Ashell {
tab.clear_selection();
if let Some(bytes) = encode_key(&event.keystroke, tab.app_cursor_mode(), false) {
tab.backend.send(BackendCommand::Input(bytes));
tab.send_backend(BackendCommand::Input(bytes));
window.prevent_default();
cx.stop_propagation();
cx.notify();
@@ -176,7 +199,7 @@ impl Ashell {
}
tab.clear_selection();
tab.backend.send(BackendCommand::Input(bytes));
tab.send_backend(BackendCommand::Input(bytes));
window.prevent_default();
cx.stop_propagation();
cx.notify();
@@ -258,8 +281,7 @@ impl Ashell {
}
tab.clear_selection();
self.terminal_marked_text = None;
tab.backend
.send(BackendCommand::Input(text.as_bytes().to_vec()));
tab.send_backend(BackendCommand::Input(text.as_bytes().to_vec()));
window.invalidate_character_coordinates();
cx.notify();
}
@@ -517,8 +539,7 @@ impl Ashell {
}
}
if !bytes.is_empty() {
tab.backend
.send(crate::terminal::BackendCommand::Input(bytes));
tab.send_backend(crate::terminal::BackendCommand::Input(bytes));
}
}
window.prevent_default();
@@ -532,8 +553,7 @@ impl Ashell {
bytes.extend_from_slice(&[b'\x1b', b'O', code]);
}
if !bytes.is_empty() {
tab.backend
.send(crate::terminal::BackendCommand::Input(bytes));
tab.send_backend(crate::terminal::BackendCommand::Input(bytes));
}
window.prevent_default();
cx.stop_propagation();
+46 -16
View File
@@ -138,12 +138,20 @@ pub struct TerminalTab {
pub kind: TabKind,
pub status: String,
pub connected: bool,
pub disconnected_reason: Option<String>,
/// Incremented each time the tab is reconnected. Used to ignore stale
/// `BackendEvent::Closed` from the previous backend after a retry.
pub backend_generation: u32,
/// Set to `true` when the current backend sends its first `Output` or
/// `Connected` event. Used to skip stale `Closed` events that arrive
/// before the new backend has started producing output.
pub backend_initialized: bool,
pub session: Option<Session>,
processor: Processor,
term: Term<TerminalListener>,
cols: u16,
rows: u16,
pub backend: BackendTx,
pub cols: u16,
pub rows: u16,
pub backend: std::sync::Arc<std::sync::Mutex<BackendTx>>,
pub scroll_pixel_y: f32,
pub(crate) highlight_cache: std::cell::RefCell<Option<(Vec<RenderCell>, std::collections::HashMap<(i32, i32), gpui::Hsla>)>>,
}
@@ -241,18 +249,22 @@ impl TerminalTab {
backend: BackendTx,
events: std::sync::mpsc::Sender<BackendEvent>,
) -> Self {
let shared_backend = std::sync::Arc::new(std::sync::Mutex::new(backend));
Self {
id: id.clone(),
title,
kind,
status,
connected: matches!(kind, TabKind::Local),
disconnected_reason: None,
backend_generation: 0,
backend_initialized: true,
session: None,
processor: Processor::new(),
term: new_term(100, 30, backend.clone(), id, events),
term: new_term(100, 30, shared_backend.clone(), id, events),
cols: 100,
rows: 30,
backend,
backend: shared_backend,
scroll_pixel_y: 0.0,
highlight_cache: std::cell::RefCell::new(None),
}
@@ -262,6 +274,22 @@ impl TerminalTab {
self.processor.advance(&mut self.term, bytes);
}
/// Send a command to the backend. Thread-safe via the shared Arc<Mutex>.
pub fn send_backend(&self, command: BackendCommand) {
if let Ok(backend) = self.backend.lock() {
backend.send(command);
}
}
/// Replace the backend with a new one. The `Term`'s internal listener
/// shares the same `Arc`, so user input is automatically routed to the
/// new backend. The old backend must be closed by the caller.
pub fn set_backend(&self, new_backend: BackendTx) {
if let Ok(mut backend) = self.backend.lock() {
*backend = new_backend;
}
}
pub fn resize(&mut self, cols: u16, rows: u16) {
let new_cols = cols.max(1);
let new_rows = rows.max(1);
@@ -274,7 +302,7 @@ impl TerminalTab {
self.rows
);
self.term.resize(TerminalSize::new(self.cols, self.rows));
self.backend.send(BackendCommand::Resize { cols, rows });
self.send_backend(BackendCommand::Resize { cols, rows });
}
}
@@ -339,7 +367,7 @@ impl TerminalTab {
let highlights = if is_enabled {
let mut cache = self.highlight_cache.borrow_mut();
let cache_valid = cache.as_ref().map_or(false, |(cached_cells, _)| {
let cache_valid = cache.as_ref().is_some_and(|(cached_cells, _)| {
cached_cells == &cells
});
if cache_valid {
@@ -467,8 +495,7 @@ impl TerminalTab {
.replace("\r\n", "\r")
.replace('\n', "\r");
self.backend
.send(BackendCommand::Input(paste_text.into_bytes()));
self.send_backend(BackendCommand::Input(paste_text.into_bytes()));
}
}
@@ -518,16 +545,18 @@ fn viewport_selection_from_range(
#[derive(Clone)]
struct TerminalListener {
tab_id: String,
backend: BackendTx,
backend: std::sync::Arc<std::sync::Mutex<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::PtyWrite(output) => {
if let Ok(backend) = self.backend.lock() {
backend.send(BackendCommand::Input(output.into_bytes()));
}
}
Event::TextAreaSizeRequest(format) => {
let size = alacritty_terminal::event::WindowSize {
num_lines: 30,
@@ -535,8 +564,9 @@ impl EventListener for TerminalListener {
cell_width: 8,
cell_height: 16,
};
self.backend
.send(BackendCommand::Input(format(size).into_bytes()));
if let Ok(backend) = self.backend.lock() {
backend.send(BackendCommand::Input(format(size).into_bytes()));
}
}
Event::Title(title) => {
let _ = self.events.send(BackendEvent::TerminalTitleChanged {
@@ -552,7 +582,7 @@ impl EventListener for TerminalListener {
fn new_term(
cols: u16,
rows: u16,
backend: BackendTx,
backend: std::sync::Arc<std::sync::Mutex<BackendTx>>,
tab_id: String,
events: std::sync::mpsc::Sender<BackendEvent>,
) -> Term<TerminalListener> {