mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 08:01:00 +00:00
feature:1、支持断开重连功能;2、支持更多高亮关键词 (#59)
This commit is contained in:
@@ -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}..."
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -335,5 +335,3 @@ pub fn paint_custom_block(
|
||||
|
||||
painted
|
||||
}
|
||||
|
||||
|
||||
|
||||
+383
-109
@@ -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
@@ -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
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user