mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84eccb5521 | ||
|
|
4da9949c84 | ||
|
|
85fb354f80 | ||
|
|
584f962298 | ||
|
|
a32fd9437d | ||
|
|
4c292c060c | ||
|
|
c0a954ea0c |
Generated
+1
-1
@@ -315,7 +315,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ashell"
|
||||
version = "0.4.10"
|
||||
version = "0.4.11"
|
||||
dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ashell"
|
||||
version = "0.4.10"
|
||||
version = "0.4.11"
|
||||
edition = "2024"
|
||||
authors = ["ashell contributors"]
|
||||
description = "A GPUI Component based SSH and local terminal client"
|
||||
|
||||
@@ -134,6 +134,8 @@ settings_copy: "Copy Selection"
|
||||
settings_paste: "Paste Clipboard"
|
||||
edit_file: "Edit File"
|
||||
edit_file_tooltip: "Download to temporary directory, open in default editor, and auto upload on save."
|
||||
sync_cwd_tooltip: "Sync path from active terminal"
|
||||
sync_cwd: "Sync Path"
|
||||
auto_saved_and_uploaded: "Auto-saved & uploaded: %{base}"
|
||||
scroll: "Scroll ↓"
|
||||
clone: "Clone"
|
||||
|
||||
@@ -135,6 +135,8 @@ settings_copy: "复制选区"
|
||||
settings_paste: "粘贴剪贴板"
|
||||
edit_file: "编辑文件"
|
||||
edit_file_tooltip: "下载到临时目录,使用默认编辑器打开,并在保存后自动上传。"
|
||||
sync_cwd_tooltip: "从当前终端同步路径"
|
||||
sync_cwd: "同步路径"
|
||||
auto_saved_and_uploaded: "已自动保存并上传: %{base}"
|
||||
scroll: "滑动查看 ↓"
|
||||
clone: "克隆"
|
||||
|
||||
@@ -128,12 +128,20 @@ pub(crate) const WORKSPACE_ACTIONS: &[WorkspaceAction] = &[
|
||||
WorkspaceAction {
|
||||
id: "Copy",
|
||||
label_key: "settings_copy",
|
||||
default_suffix: if cfg!(target_os = "macos") { "c" } else { "shift-c" },
|
||||
default_suffix: if cfg!(target_os = "macos") {
|
||||
"c"
|
||||
} else {
|
||||
"shift-c"
|
||||
},
|
||||
},
|
||||
WorkspaceAction {
|
||||
id: "Paste",
|
||||
label_key: "settings_paste",
|
||||
default_suffix: if cfg!(target_os = "macos") { "v" } else { "shift-v" },
|
||||
default_suffix: if cfg!(target_os = "macos") {
|
||||
"v"
|
||||
} else {
|
||||
"shift-v"
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -305,7 +313,15 @@ impl KeybindingsPage {
|
||||
let groups = [
|
||||
(
|
||||
"settings_group_keybind_general",
|
||||
vec!["OpenSettings", "OpenSession", "OpenTransfers", "NewSsh", "OpenSearch", "Copy", "Paste"],
|
||||
vec![
|
||||
"OpenSettings",
|
||||
"OpenSession",
|
||||
"OpenTransfers",
|
||||
"NewSsh",
|
||||
"OpenSearch",
|
||||
"Copy",
|
||||
"Paste",
|
||||
],
|
||||
),
|
||||
(
|
||||
"settings_group_keybind_zoom",
|
||||
|
||||
+68
-1
@@ -1010,7 +1010,7 @@ impl Ashell {
|
||||
}
|
||||
BackendEvent::TerminalTitleChanged { tab_id, title } => {
|
||||
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||
tab.title = title.clone();
|
||||
tab.dynamic_title = title;
|
||||
}
|
||||
}
|
||||
BackendEvent::SyncFinished(result) => {
|
||||
@@ -1228,6 +1228,73 @@ impl Ashell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn sync_cwd_from_terminal(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
let active_id = self.active_tab.clone();
|
||||
let Some(active_id) = active_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(tab) = self.tabs.iter().find(|t| t.id == active_id) {
|
||||
let home_dir = if let Some(group) = self
|
||||
.tab_groups
|
||||
.iter()
|
||||
.find(|g| g.pane_root.contains(&tab.id))
|
||||
{
|
||||
group
|
||||
.sftp
|
||||
.as_ref()
|
||||
.map(|s| s.home_dir.as_str())
|
||||
.unwrap_or("/")
|
||||
} else {
|
||||
"/"
|
||||
};
|
||||
|
||||
let parsed = Self::parse_path_from_title(&tab.dynamic_title, home_dir);
|
||||
|
||||
if let Some(path) = parsed {
|
||||
if let Some(group) = self
|
||||
.tab_groups
|
||||
.iter_mut()
|
||||
.find(|g| g.pane_root.contains(&active_id))
|
||||
{
|
||||
if let Some(sftp) = group.sftp.as_mut() {
|
||||
sftp.current_path = path.clone();
|
||||
self.pending_sftp_path_sync = Some(path.clone());
|
||||
if let Some(handle) = self.sftp_handles.get(&group.id) {
|
||||
let _ = handle
|
||||
.commands
|
||||
.send(crate::sftp::SftpCommand::ListDir(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_path_from_title(title: &str, home_dir: &str) -> Option<String> {
|
||||
let title = title.strip_prefix("ASHELL_CWD:").unwrap_or(title);
|
||||
let path_part = if let Some(pos) = title.find(':') {
|
||||
title[pos + 1..].trim()
|
||||
} else {
|
||||
title.trim()
|
||||
};
|
||||
|
||||
if path_part.starts_with('/') {
|
||||
Some(path_part.to_string())
|
||||
} else if path_part == "~" {
|
||||
Some(home_dir.to_string())
|
||||
} else if let Some(rest) = path_part.strip_prefix("~/") {
|
||||
let home = home_dir.trim_end_matches('/');
|
||||
Some(format!("{}/{}", home, rest))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn save_layout_state(&self, window: &mut gpui::Window, cx: &gpui::App) {
|
||||
if self.is_layout_reset {
|
||||
tracing::info!("[ui] layout was reset, skipping save layout state.");
|
||||
|
||||
+13
-18
@@ -4,17 +4,15 @@ use std::{
|
||||
};
|
||||
|
||||
use gpui::{
|
||||
Along, AnyElement, App, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
|
||||
EventEmitter, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent,
|
||||
ParentElement, Pixels, Render, RenderOnce, Style, StyleRefinement, Styled, Window, div,
|
||||
prelude::FluentBuilder, AppContext as _, IsZero as _,
|
||||
Along, AnyElement, App, AppContext as _, Axis, Bounds, Context, Element, ElementId, Empty,
|
||||
Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, StyleRefinement, Styled,
|
||||
Window, div, prelude::FluentBuilder,
|
||||
};
|
||||
|
||||
use gpui_component::{
|
||||
AxisExt, ElementExt, h_flex, v_flex, StyledExt,
|
||||
};
|
||||
use gpui_component::{AxisExt, ElementExt, StyledExt, h_flex, v_flex};
|
||||
|
||||
use super::{ResizableState, resizable_panel, resize_handle, PANEL_MIN_SIZE, ResizablePanelEvent};
|
||||
use super::{PANEL_MIN_SIZE, ResizablePanelEvent, ResizableState, resizable_panel, resize_handle};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DragPanel;
|
||||
@@ -305,16 +303,13 @@ impl RenderOnce for ResizablePanel {
|
||||
let handle = if self.locked {
|
||||
handle
|
||||
} else {
|
||||
handle.on_drag(
|
||||
DragPanel,
|
||||
move |drag_panel, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
state.update(cx, |state, _| {
|
||||
state.resizing_panel_ix = Some(ix);
|
||||
});
|
||||
cx.new(|_| drag_panel.deref().clone())
|
||||
},
|
||||
)
|
||||
handle.on_drag(DragPanel, move |drag_panel, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
state.update(cx, |state, _| {
|
||||
state.resizing_panel_ix = Some(ix);
|
||||
});
|
||||
cx.new(|_| drag_panel.deref().clone())
|
||||
})
|
||||
};
|
||||
this.child(handle)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, AnyElement, App, Axis, Element, ElementId, Entity,
|
||||
GlobalElementId, InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent,
|
||||
ParentElement as _, Pixels, Point, Render, StatefulInteractiveElement, Styled as _, Window,
|
||||
AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
|
||||
IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
||||
StatefulInteractiveElement, Styled as _, Window, div, prelude::FluentBuilder as _, px,
|
||||
};
|
||||
|
||||
use gpui_component::{ActiveTheme as _, AxisExt as _, dock::DockPlacement};
|
||||
@@ -126,14 +126,13 @@ impl<T: 'static, E: 'static + Render> Element for ResizeHandle<T, E> {
|
||||
)
|
||||
})
|
||||
.map(|this| match self.placement {
|
||||
Some(DockPlacement::Left) => {
|
||||
this.when(has_drag, |this| this.cursor_col_resize())
|
||||
.top_0()
|
||||
.right(px(1.))
|
||||
.h_full()
|
||||
.w(HANDLE_SIZE)
|
||||
.pl(HANDLE_PADDING)
|
||||
}
|
||||
Some(DockPlacement::Left) => this
|
||||
.when(has_drag, |this| this.cursor_col_resize())
|
||||
.top_0()
|
||||
.right(px(1.))
|
||||
.h_full()
|
||||
.w(HANDLE_SIZE)
|
||||
.pl(HANDLE_PADDING),
|
||||
_ => this
|
||||
.when(axis.is_horizontal(), |this| {
|
||||
this.when(has_drag, |this| this.cursor_col_resize())
|
||||
|
||||
+33
-31
@@ -1,13 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use gpui::{
|
||||
Context, Focusable as _, Hsla, IntoElement, InteractiveElement as _, MouseButton,
|
||||
Context, Focusable as _, Hsla, InteractiveElement as _, IntoElement, MouseButton,
|
||||
ParentElement as _, Styled as _, Window, div, prelude::FluentBuilder as _, px, rems,
|
||||
};
|
||||
use gpui_component::{
|
||||
ActiveTheme as _, Disableable as _, ElementExt as _, IconName, Sizable as _,
|
||||
button::{Button, ButtonVariants as _},
|
||||
h_flex, input::Input,
|
||||
h_flex,
|
||||
input::Input,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
|
||||
@@ -214,7 +215,7 @@ impl Ashell {
|
||||
};
|
||||
|
||||
if let Some(tab) = tab {
|
||||
let snapshot = tab.render_snapshot();
|
||||
let snapshot = tab.render_snapshot(false);
|
||||
let display_offset = snapshot.display_offset as i32;
|
||||
let rows = snapshot.rows as i32;
|
||||
|
||||
@@ -274,7 +275,7 @@ impl Ashell {
|
||||
let Some(tab) = tab else {
|
||||
return None;
|
||||
};
|
||||
let snapshot = tab.render_snapshot();
|
||||
let snapshot = tab.render_snapshot(false);
|
||||
let display_offset = snapshot.display_offset as i32;
|
||||
let rows = snapshot.rows as i32;
|
||||
|
||||
@@ -323,23 +324,19 @@ impl Ashell {
|
||||
}
|
||||
|
||||
/// Render the search button (used in the tab bar).
|
||||
pub(crate) fn render_search_button(
|
||||
&self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
pub(crate) fn render_search_button(&self, cx: &mut Context<Self>) -> impl gpui::IntoElement {
|
||||
// Wrap in a div so .hover() doesn't conflict with Button's internal hover.
|
||||
div()
|
||||
.child(
|
||||
Button::new("search-btn")
|
||||
.ghost()
|
||||
.small()
|
||||
.rounded(px(999.))
|
||||
.icon(IconName::Search)
|
||||
.tooltip(t!("search").to_string())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_search(window, cx);
|
||||
})),
|
||||
)
|
||||
div().child(
|
||||
Button::new("search-btn")
|
||||
.ghost()
|
||||
.small()
|
||||
.rounded(px(999.))
|
||||
.icon(IconName::Search)
|
||||
.tooltip(t!("search").to_string())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_search(window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the expanded search bar overlay (when search is active).
|
||||
@@ -369,10 +366,13 @@ impl Ashell {
|
||||
this.search_bar_bounds = Some(bounds);
|
||||
});
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, cx.listener(|this, _, window, cx| {
|
||||
this.refocus_search_input(window, cx);
|
||||
cx.stop_propagation();
|
||||
}))
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.refocus_search_input(window, cx);
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -385,13 +385,15 @@ impl Ashell {
|
||||
.child(
|
||||
div()
|
||||
.w(px(200.))
|
||||
.on_key_down(cx.listener(|this, event: &gpui::KeyDownEvent, window, cx| {
|
||||
if event.keystroke.key.as_str() == "escape" {
|
||||
this.close_search(window, cx);
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}))
|
||||
.on_key_down(cx.listener(
|
||||
|this, event: &gpui::KeyDownEvent, window, cx| {
|
||||
if event.keystroke.key.as_str() == "escape" {
|
||||
this.close_search(window, cx);
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
},
|
||||
))
|
||||
.child(Input::new(&self.search_input).small()),
|
||||
)
|
||||
.when(!current_display.is_empty(), |this| {
|
||||
|
||||
+11
-4
@@ -179,7 +179,14 @@ pub(crate) fn sync_macos_launch_environment() {
|
||||
}
|
||||
|
||||
fn read_proxy_from_env() -> Option<(String, String, Option<u16>, String, String)> {
|
||||
let vars = ["ALL_PROXY", "all_proxy", "HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
||||
let vars = [
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
];
|
||||
for var in vars {
|
||||
if let Ok(val) = std::env::var(var) {
|
||||
if val.is_empty() {
|
||||
@@ -301,13 +308,13 @@ pub(crate) fn open_main_window(cx: &mut App) {
|
||||
let focus_handle = view.read(cx).focus_handle.clone();
|
||||
window.focus(&focus_handle, cx);
|
||||
|
||||
|
||||
|
||||
let view_clone = view.clone();
|
||||
window.on_window_should_close(cx, move |window: &mut gpui::Window, cx: &mut gpui::App| {
|
||||
let handle = window.window_handle();
|
||||
if !cx.windows().contains(&handle) {
|
||||
tracing::warn!("[ui] window not found in app during close, skipping save layout state.");
|
||||
tracing::warn!(
|
||||
"[ui] window not found in app during close, skipping save layout state."
|
||||
);
|
||||
return true;
|
||||
}
|
||||
view_clone.read(cx).save_layout_state(window, cx);
|
||||
|
||||
+4
-1
@@ -17,7 +17,10 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
pub(crate) static USING_SYSTEM_MAPLE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub(crate) fn load_fonts(cx: &mut App) -> Result<()> {
|
||||
let has_system_maple = cx.text_system().all_font_names().contains(&"Maple Mono NF CN".to_string());
|
||||
let has_system_maple = cx
|
||||
.text_system()
|
||||
.all_font_names()
|
||||
.contains(&"Maple Mono NF CN".to_string());
|
||||
if has_system_maple {
|
||||
USING_SYSTEM_MAPLE.store(true, Ordering::Relaxed);
|
||||
} else {
|
||||
|
||||
+85
-52
@@ -1,3 +1,4 @@
|
||||
use crate::app::resizable::{h_resizable, resizable_panel, v_resizable};
|
||||
use gpui::{
|
||||
Context, ElementId, Focusable as _, FontWeight, Hsla, InteractiveElement as _, IntoElement,
|
||||
MouseButton, MouseDownEvent, ParentElement as _, PathBuilder, Pixels, Render,
|
||||
@@ -17,7 +18,6 @@ use gpui_component::{
|
||||
tab::{Tab, TabBar},
|
||||
v_flex,
|
||||
};
|
||||
use crate::app::resizable::{h_resizable, resizable_panel, v_resizable};
|
||||
use rust_i18n::t;
|
||||
|
||||
use crate::{
|
||||
@@ -26,7 +26,7 @@ use crate::{
|
||||
sftp::format_mtime,
|
||||
sftp::ops::is_editable_text_file,
|
||||
system::format_bytes,
|
||||
terminal::{self, TabKind, TerminalTab},
|
||||
terminal::{self, TabKind},
|
||||
};
|
||||
|
||||
impl Ashell {
|
||||
@@ -197,6 +197,17 @@ impl Ashell {
|
||||
.when_some(active_sftp.clone(), |this, sftp| {
|
||||
let selected_entries = sftp.selected_entries.clone();
|
||||
this.child(
|
||||
Button::new("sftp-sync-cwd")
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::SquareTerminal)
|
||||
.label(t!("sync_cwd").to_string())
|
||||
.tooltip(t!("sync_cwd_tooltip").to_string())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.sync_cwd_from_terminal(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("sftp-refresh")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -1984,7 +1995,7 @@ impl Ashell {
|
||||
) -> impl IntoElement {
|
||||
let is_macos = cfg!(target_os = "macos");
|
||||
let is_fullscreen = window.is_fullscreen();
|
||||
|
||||
|
||||
let is_active = cx.active_window() == Some(window.window_handle());
|
||||
|
||||
h_flex()
|
||||
@@ -1999,9 +2010,17 @@ impl Ashell {
|
||||
.id("window-close")
|
||||
.size(px(12.))
|
||||
.rounded_full()
|
||||
.bg(if is_active { hsla(3.0 / 360.0, 1.0, 0.67, 1.0) } else { hsla(0.0, 0.0, 0.8, 1.0) }) // Red or Inactive Grey
|
||||
.group_hover("window-controls", |s| s.bg(hsla(3.0 / 360.0, 1.0, 0.67, 1.0)))
|
||||
.when(!is_macos, |this| this.window_control_area(gpui::WindowControlArea::Close))
|
||||
.bg(if is_active {
|
||||
hsla(3.0 / 360.0, 1.0, 0.67, 1.0)
|
||||
} else {
|
||||
hsla(0.0, 0.0, 0.8, 1.0)
|
||||
}) // Red or Inactive Grey
|
||||
.group_hover("window-controls", |s| {
|
||||
s.bg(hsla(3.0 / 360.0, 1.0, 0.67, 1.0))
|
||||
})
|
||||
.when(!is_macos, |this| {
|
||||
this.window_control_area(gpui::WindowControlArea::Close)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.save_layout_state(window, cx);
|
||||
window.remove_window();
|
||||
@@ -2020,7 +2039,7 @@ impl Ashell {
|
||||
.text_color(hsla(3.0 / 360.0, 1.0, 0.15, 0.7))
|
||||
.opacity(0.0)
|
||||
.group_hover("window-controls", |s| s.opacity(1.0))
|
||||
.child("✕")
|
||||
.child("✕"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -2028,9 +2047,17 @@ impl Ashell {
|
||||
.id("window-minimize")
|
||||
.size(px(12.))
|
||||
.rounded_full()
|
||||
.bg(if is_active { hsla(39.0 / 360.0, 1.0, 0.59, 1.0) } else { hsla(0.0, 0.0, 0.8, 1.0) }) // Yellow or Inactive Grey
|
||||
.group_hover("window-controls", |s| s.bg(hsla(39.0 / 360.0, 1.0, 0.59, 1.0)))
|
||||
.when(!is_macos, |this| this.window_control_area(gpui::WindowControlArea::Min))
|
||||
.bg(if is_active {
|
||||
hsla(39.0 / 360.0, 1.0, 0.59, 1.0)
|
||||
} else {
|
||||
hsla(0.0, 0.0, 0.8, 1.0)
|
||||
}) // Yellow or Inactive Grey
|
||||
.group_hover("window-controls", |s| {
|
||||
s.bg(hsla(39.0 / 360.0, 1.0, 0.59, 1.0))
|
||||
})
|
||||
.when(!is_macos, |this| {
|
||||
this.window_control_area(gpui::WindowControlArea::Min)
|
||||
})
|
||||
.on_click(|_, window, _| window.minimize_window())
|
||||
.hover(|s| s.bg(hsla(39.0 / 360.0, 1.0, 0.49, 1.0)))
|
||||
.active(|s| s.bg(hsla(39.0 / 360.0, 1.0, 0.39, 1.0)))
|
||||
@@ -2046,7 +2073,7 @@ impl Ashell {
|
||||
.text_color(hsla(39.0 / 360.0, 1.0, 0.15, 0.8))
|
||||
.opacity(0.0)
|
||||
.group_hover("window-controls", |s| s.opacity(1.0))
|
||||
.child("−")
|
||||
.child("−"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -2054,9 +2081,17 @@ impl Ashell {
|
||||
.id("window-maximize")
|
||||
.size(px(12.))
|
||||
.rounded_full()
|
||||
.bg(if is_active { hsla(127.0 / 360.0, 0.68, 0.47, 1.0) } else { hsla(0.0, 0.0, 0.8, 1.0) }) // Green or Inactive Grey
|
||||
.group_hover("window-controls", |s| s.bg(hsla(127.0 / 360.0, 0.68, 0.47, 1.0)))
|
||||
.when(!is_macos, |this| this.window_control_area(gpui::WindowControlArea::Max))
|
||||
.bg(if is_active {
|
||||
hsla(127.0 / 360.0, 0.68, 0.47, 1.0)
|
||||
} else {
|
||||
hsla(0.0, 0.0, 0.8, 1.0)
|
||||
}) // Green or Inactive Grey
|
||||
.group_hover("window-controls", |s| {
|
||||
s.bg(hsla(127.0 / 360.0, 0.68, 0.47, 1.0))
|
||||
})
|
||||
.when(!is_macos, |this| {
|
||||
this.window_control_area(gpui::WindowControlArea::Max)
|
||||
})
|
||||
.on_click(|_, window, _| {
|
||||
if window.is_fullscreen() {
|
||||
window.toggle_fullscreen();
|
||||
@@ -2081,7 +2116,7 @@ impl Ashell {
|
||||
.text_color(hsla(127.0 / 360.0, 1.0, 0.15, 0.8))
|
||||
.opacity(0.0)
|
||||
.group_hover("window-controls", |s| s.opacity(1.0))
|
||||
.child("+")
|
||||
.child("+"),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -2315,11 +2350,12 @@ impl Ashell {
|
||||
return this.render_home_page(cx).into_any_element();
|
||||
}
|
||||
let is_focused = path == this.focused_pane_path.as_slice();
|
||||
let keyword_highlight = this.config.keyword_highlight();
|
||||
let snapshot = this
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|t| &t.id == tab_id)
|
||||
.map(TerminalTab::render_snapshot);
|
||||
.map(|t| t.render_snapshot(keyword_highlight));
|
||||
let Some(snapshot) = snapshot else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
@@ -2334,7 +2370,10 @@ impl Ashell {
|
||||
let font_size = px(this.terminal_font_size);
|
||||
let line_height = px(this.terminal_line_height());
|
||||
let cell_width = px(this.terminal_cell_width());
|
||||
let is_url_hovered = this.hovered_url.as_ref().map_or(false, |hu| hu.tab_id == *tab_id);
|
||||
let is_url_hovered = this
|
||||
.hovered_url
|
||||
.as_ref()
|
||||
.map_or(false, |hu| hu.tab_id == *tab_id);
|
||||
let mut el = div()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
@@ -2375,44 +2414,38 @@ impl Ashell {
|
||||
.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()
|
||||
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(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.py_1()
|
||||
.bg(cx.theme().danger.opacity(0.15))
|
||||
div()
|
||||
.text_size(rems(0.85))
|
||||
.text_color(cx.theme().danger)
|
||||
.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);
|
||||
}),
|
||||
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
|
||||
@@ -2606,7 +2639,7 @@ impl Render for Ashell {
|
||||
if let Some(scrollbar) = self.terminal_scrollbars.get(&active_id) {
|
||||
if let Some(new_display_offset) = scrollbar.future_display_offset.take() {
|
||||
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == active_id) {
|
||||
let current = tab.render_snapshot().display_offset;
|
||||
let current = tab.render_snapshot(false).display_offset;
|
||||
match new_display_offset.cmp(¤t) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
tab.scroll_up_by(new_display_offset - current)
|
||||
|
||||
+5
-5
@@ -322,7 +322,9 @@ async fn connect_and_authenticate(
|
||||
break;
|
||||
}
|
||||
Ok(false) => {
|
||||
tracing::debug!("[ssh] public key auth failed with algorithm, trying next");
|
||||
tracing::debug!(
|
||||
"[ssh] public key auth failed with algorithm, trying next"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -344,8 +346,7 @@ async fn connect_and_authenticate(
|
||||
success
|
||||
} else {
|
||||
let success =
|
||||
authenticate_with_default_keys(&mut handle, &session.user, passphrase)
|
||||
.await?;
|
||||
authenticate_with_default_keys(&mut handle, &session.user, passphrase).await?;
|
||||
if !success {
|
||||
return Err(anyhow::anyhow!(
|
||||
"public key authentication failed for {}@{}:{} - no valid default key found in ~/.ssh/",
|
||||
@@ -417,8 +418,7 @@ async fn connect_and_authenticate(
|
||||
),
|
||||
});
|
||||
let success =
|
||||
authenticate_with_default_keys(&mut handle, &session.user, passphrase)
|
||||
.await?;
|
||||
authenticate_with_default_keys(&mut handle, &session.user, passphrase).await?;
|
||||
if !success {
|
||||
return Err(anyhow::anyhow!(
|
||||
"ssh-config authentication failed for {}@{}:{} - no valid default key found",
|
||||
|
||||
+4
-6
@@ -16,14 +16,12 @@ rust_i18n::i18n!("locales", fallback = "en");
|
||||
gpui::actions!(ashell_terminal, [TerminalTabKey, TerminalBacktabKey]);
|
||||
|
||||
pub(crate) use app::keybinding_recorder::{
|
||||
ClosePane, FocusPaneDown, FocusPaneLeft, FocusPaneRight, FocusPaneUp, NewSsh, OpenSearch,
|
||||
OpenSession, OpenSettings, OpenTransfers, SplitPaneDown, SplitPaneLeft, SplitPaneRight,
|
||||
SplitPaneUp, ToggleSftpZoom, ToggleSidebar, Copy, Paste,
|
||||
ClosePane, Copy, FocusPaneDown, FocusPaneLeft, FocusPaneRight, FocusPaneUp, NewSsh, OpenSearch,
|
||||
OpenSession, OpenSettings, OpenTransfers, Paste, SplitPaneDown, SplitPaneLeft, SplitPaneRight,
|
||||
SplitPaneUp, ToggleSftpZoom, ToggleSidebar,
|
||||
};
|
||||
|
||||
pub(crate) use app::{
|
||||
Ashell, PaneLayout, SelectorEntry, SftpContextMenuState, TabGroup,
|
||||
};
|
||||
pub(crate) use app::{Ashell, PaneLayout, SelectorEntry, SftpContextMenuState, TabGroup};
|
||||
|
||||
fn main() {
|
||||
app::startup::sync_macos_launch_environment();
|
||||
|
||||
+336
-127
@@ -1,7 +1,14 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use argon2::Argon2;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use chacha20poly1305::{
|
||||
XChaCha20Poly1305, XNonce,
|
||||
aead::{Aead, KeyInit},
|
||||
};
|
||||
use directories::BaseDirs;
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -344,26 +351,33 @@ impl ConfigStore {
|
||||
}
|
||||
|
||||
let mut cache = if path.exists() {
|
||||
let raw = fs::read_to_string(&path)
|
||||
.with_context(|| format!("failed to read {}", path.display()))?;
|
||||
match serde_json::from_str::<ConfigFile>(&raw) {
|
||||
let raw_bytes =
|
||||
fs::read(&path).with_context(|| format!("failed to read {}", path.display()))?;
|
||||
let hardware_uuid = get_hardware_uuid();
|
||||
match decrypt_config(&raw_bytes, &hardware_uuid) {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => {
|
||||
let backup_path = path.with_extension("json.bak");
|
||||
if let Err(backup_err) = fs::write(&backup_path, raw.as_bytes()) {
|
||||
tracing::warn!(
|
||||
"failed to parse config {}; backup to {} also failed: {backup_err:#}; parse error: {err:#}",
|
||||
path.display(),
|
||||
backup_path.display(),
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"failed to parse config {}; backed up the original to {} and loaded defaults: {err:#}",
|
||||
path.display(),
|
||||
backup_path.display(),
|
||||
);
|
||||
Err(decrypt_err) => {
|
||||
// Fallback to plain text JSON if decryption/parsing failed
|
||||
match serde_json::from_slice::<ConfigFile>(&raw_bytes) {
|
||||
Ok(cache) => cache,
|
||||
Err(json_err) => {
|
||||
let backup_path = path.with_extension("json.bak");
|
||||
if let Err(backup_err) = fs::write(&backup_path, &raw_bytes) {
|
||||
tracing::warn!(
|
||||
"failed to parse config {} (decrypt err: {decrypt_err:#}, json err: {json_err:#}); backup to {} also failed: {backup_err:#}",
|
||||
path.display(),
|
||||
backup_path.display(),
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"failed to parse config {} (decrypt err: {decrypt_err:#}, json err: {json_err:#}); backed up the original to {} and loaded defaults",
|
||||
path.display(),
|
||||
backup_path.display(),
|
||||
);
|
||||
}
|
||||
ConfigFile::default()
|
||||
}
|
||||
}
|
||||
ConfigFile::default()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -758,8 +772,9 @@ impl ConfigStore {
|
||||
if self.path.as_os_str().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let raw = serde_json::to_string_pretty(&self.cache)?;
|
||||
fs::write(&self.path, raw)
|
||||
let hardware_uuid = get_hardware_uuid();
|
||||
let encrypted_bytes = encrypt_config(&self.cache, &hardware_uuid)?;
|
||||
fs::write(&self.path, encrypted_bytes)
|
||||
.with_context(|| format!("failed to write {}", self.path.display()))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -798,118 +813,126 @@ pub struct EnvProxy {
|
||||
pub static ENV_PROXY: OnceLock<Option<EnvProxy>> = OnceLock::new();
|
||||
|
||||
pub async fn connect_proxy(session: &Session) -> Result<Box<dyn ProxyStream>> {
|
||||
let target_host = &session.host;
|
||||
let target_host = session.host.clone();
|
||||
let target_port = session.port;
|
||||
let session = session.clone();
|
||||
|
||||
let config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory());
|
||||
let (proxy_type, proxy_host, proxy_port, proxy_user, proxy_password) = {
|
||||
if !session.proxy_type.is_empty() && session.proxy_type != "none" {
|
||||
(
|
||||
session.proxy_type.clone(),
|
||||
session.proxy_host.clone(),
|
||||
session.proxy_port,
|
||||
session.proxy_user.clone(),
|
||||
session.proxy_password.clone(),
|
||||
)
|
||||
} else if config.cache.read_env_proxy
|
||||
&& ENV_PROXY.get().and_then(|opt| opt.as_ref()).is_some()
|
||||
{
|
||||
let env_p = ENV_PROXY.get().and_then(|opt| opt.as_ref()).unwrap();
|
||||
(
|
||||
env_p.proxy_type.clone(),
|
||||
env_p.host.clone(),
|
||||
env_p.port,
|
||||
env_p.user.clone(),
|
||||
env_p.pass.clone(),
|
||||
)
|
||||
} else if config.cache.use_proxy {
|
||||
(
|
||||
config.cache.global_proxy_type.clone(),
|
||||
config.cache.global_proxy_host.clone(),
|
||||
config.cache.global_proxy_port,
|
||||
config.cache.global_proxy_user.clone(),
|
||||
config.cache.global_proxy_password.clone(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"none".to_string(),
|
||||
String::new(),
|
||||
None,
|
||||
String::new(),
|
||||
String::new(),
|
||||
)
|
||||
let connect_fut = async move {
|
||||
let target_host = &target_host;
|
||||
let config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory());
|
||||
let (proxy_type, proxy_host, proxy_port, proxy_user, proxy_password) = {
|
||||
if !session.proxy_type.is_empty() && session.proxy_type != "none" {
|
||||
(
|
||||
session.proxy_type.clone(),
|
||||
session.proxy_host.clone(),
|
||||
session.proxy_port,
|
||||
session.proxy_user.clone(),
|
||||
session.proxy_password.clone(),
|
||||
)
|
||||
} else if config.cache.read_env_proxy
|
||||
&& ENV_PROXY.get().and_then(|opt| opt.as_ref()).is_some()
|
||||
{
|
||||
let env_p = ENV_PROXY.get().and_then(|opt| opt.as_ref()).unwrap();
|
||||
(
|
||||
env_p.proxy_type.clone(),
|
||||
env_p.host.clone(),
|
||||
env_p.port,
|
||||
env_p.user.clone(),
|
||||
env_p.pass.clone(),
|
||||
)
|
||||
} else if config.cache.use_proxy {
|
||||
(
|
||||
config.cache.global_proxy_type.clone(),
|
||||
config.cache.global_proxy_host.clone(),
|
||||
config.cache.global_proxy_port,
|
||||
config.cache.global_proxy_user.clone(),
|
||||
config.cache.global_proxy_password.clone(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"none".to_string(),
|
||||
String::new(),
|
||||
None,
|
||||
String::new(),
|
||||
String::new(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if proxy_type != "none" && (proxy_host.is_empty() || proxy_port.is_none()) {
|
||||
let addr = format!("{}:{}", target_host, target_port);
|
||||
let stream = tokio::net::TcpStream::connect(&addr).await?;
|
||||
return Ok(Box::new(stream) as Box<dyn ProxyStream>);
|
||||
}
|
||||
|
||||
match proxy_type.as_str() {
|
||||
"socks5" | "socks5h" => {
|
||||
let proxy_port = proxy_port.unwrap_or(1080);
|
||||
let proxy_addr = format!("{}:{}", proxy_host, proxy_port);
|
||||
|
||||
if !proxy_user.is_empty() {
|
||||
let stream = tokio_socks::tcp::Socks5Stream::connect_with_password(
|
||||
proxy_addr.as_str(),
|
||||
(target_host.as_str(), target_port),
|
||||
&proxy_user,
|
||||
&proxy_password,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("SOCKS5 proxy connection failed: {}", e))?;
|
||||
Ok(Box::new(stream) as Box<dyn ProxyStream>)
|
||||
} else {
|
||||
let stream = tokio_socks::tcp::Socks5Stream::connect(
|
||||
proxy_addr.as_str(),
|
||||
(target_host.as_str(), target_port),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("SOCKS5 proxy connection failed: {}", e))?;
|
||||
Ok(Box::new(stream) as Box<dyn ProxyStream>)
|
||||
}
|
||||
}
|
||||
"http" => {
|
||||
let proxy_port = proxy_port.unwrap_or(8080);
|
||||
let proxy_addr = format!("{}:{}", proxy_host, proxy_port);
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut stream = tokio::net::TcpStream::connect(&proxy_addr)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("HTTP proxy connection failed: {}", e))?;
|
||||
|
||||
let mut request = format!(
|
||||
"CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n",
|
||||
target_host, target_port, target_host, target_port
|
||||
);
|
||||
if !proxy_user.is_empty() {
|
||||
use base64::Engine as _;
|
||||
let auth = format!("{}:{}", proxy_user, proxy_password);
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(auth);
|
||||
request.push_str(&format!("Proxy-Authorization: Basic {}\r\n", encoded));
|
||||
}
|
||||
request.push_str("\r\n");
|
||||
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
|
||||
let mut response = [0u8; 1024];
|
||||
let n = tokio::io::AsyncReadExt::read(&mut stream, &mut response).await?;
|
||||
let resp_str = String::from_utf8_lossy(&response[..n]);
|
||||
if !resp_str.contains("200") && !resp_str.contains("established") {
|
||||
return Err(anyhow::anyhow!("HTTP proxy CONNECT failed: {}", resp_str));
|
||||
}
|
||||
|
||||
Ok(Box::new(stream) as Box<dyn ProxyStream>)
|
||||
}
|
||||
_ => {
|
||||
let addr = format!("{}:{}", target_host, target_port);
|
||||
let stream = tokio::net::TcpStream::connect(&addr).await?;
|
||||
Ok(Box::new(stream) as Box<dyn ProxyStream>)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if proxy_type != "none" && (proxy_host.is_empty() || proxy_port.is_none()) {
|
||||
let addr = format!("{}:{}", target_host, target_port);
|
||||
let stream = tokio::net::TcpStream::connect(&addr).await?;
|
||||
return Ok(Box::new(stream));
|
||||
}
|
||||
|
||||
match proxy_type.as_str() {
|
||||
"socks5" | "socks5h" => {
|
||||
let proxy_port = proxy_port.unwrap_or(1080);
|
||||
let proxy_addr = format!("{}:{}", proxy_host, proxy_port);
|
||||
|
||||
if !proxy_user.is_empty() {
|
||||
let stream = tokio_socks::tcp::Socks5Stream::connect_with_password(
|
||||
proxy_addr.as_str(),
|
||||
(target_host.as_str(), target_port),
|
||||
&proxy_user,
|
||||
&proxy_password,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("SOCKS5 proxy connection failed: {}", e))?;
|
||||
Ok(Box::new(stream))
|
||||
} else {
|
||||
let stream = tokio_socks::tcp::Socks5Stream::connect(
|
||||
proxy_addr.as_str(),
|
||||
(target_host.as_str(), target_port),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("SOCKS5 proxy connection failed: {}", e))?;
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
}
|
||||
"http" => {
|
||||
let proxy_port = proxy_port.unwrap_or(8080);
|
||||
let proxy_addr = format!("{}:{}", proxy_host, proxy_port);
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut stream = tokio::net::TcpStream::connect(&proxy_addr)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("HTTP proxy connection failed: {}", e))?;
|
||||
|
||||
let mut request = format!(
|
||||
"CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n",
|
||||
target_host, target_port, target_host, target_port
|
||||
);
|
||||
if !proxy_user.is_empty() {
|
||||
use base64::Engine as _;
|
||||
let auth = format!("{}:{}", proxy_user, proxy_password);
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(auth);
|
||||
request.push_str(&format!("Proxy-Authorization: Basic {}\r\n", encoded));
|
||||
}
|
||||
request.push_str("\r\n");
|
||||
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
|
||||
let mut response = [0u8; 1024];
|
||||
let n = tokio::io::AsyncReadExt::read(&mut stream, &mut response).await?;
|
||||
let resp_str = String::from_utf8_lossy(&response[..n]);
|
||||
if !resp_str.contains("200") && !resp_str.contains("established") {
|
||||
return Err(anyhow::anyhow!("HTTP proxy CONNECT failed: {}", resp_str));
|
||||
}
|
||||
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
_ => {
|
||||
let addr = format!("{}:{}", target_host, target_port);
|
||||
let stream = tokio::net::TcpStream::connect(&addr).await?;
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
}
|
||||
tokio::time::timeout(std::time::Duration::from_secs(16), connect_fut)
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("connection timed out after 16 seconds"))?
|
||||
}
|
||||
|
||||
pub fn active_proxy(session: &Session) -> Option<(String, String, Option<u16>)> {
|
||||
@@ -959,3 +982,189 @@ pub fn active_proxy(session: &Session) -> Option<(String, String, Option<u16>)>
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct EncryptedConfigEnvelope {
|
||||
format_version: u32,
|
||||
kdf: String,
|
||||
cipher: String,
|
||||
salt: String,
|
||||
nonce: String,
|
||||
payload: String,
|
||||
}
|
||||
|
||||
fn get_hardware_uuid() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(output) = std::process::Command::new("ioreg")
|
||||
.args(&["-rd1", "-c", "IOPlatformExpertDevice"])
|
||||
.output()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
if line.contains("IOPlatformUUID") {
|
||||
if let Some(uuid) = line.split('"').nth(3) {
|
||||
let uuid = uuid.trim().to_string();
|
||||
if !uuid.is_empty() {
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Ok(uuid) = std::fs::read_to_string("/sys/class/dmi/id/product_uuid") {
|
||||
let uuid = uuid.trim().to_string();
|
||||
if !uuid.is_empty() {
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
if let Ok(id) = std::fs::read_to_string("/etc/machine-id") {
|
||||
let id = id.trim().to_string();
|
||||
if !id.is_empty() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
if let Ok(id) = std::fs::read_to_string("/var/lib/dbus/machine-id") {
|
||||
let id = id.trim().to_string();
|
||||
if !id.is_empty() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(output) = std::process::Command::new("reg")
|
||||
.args(&[
|
||||
"query",
|
||||
"HKLM\\SOFTWARE\\Microsoft\\Cryptography",
|
||||
"/v",
|
||||
"MachineGuid",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
if line.contains("MachineGuid") {
|
||||
if let Some(guid) = line.split_whitespace().last() {
|
||||
let guid = guid.trim().to_string();
|
||||
if !guid.is_empty() {
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(output) = std::process::Command::new("wmic")
|
||||
.args(&["csproduct", "get", "uuid"])
|
||||
.output()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
if lines.len() >= 2 {
|
||||
let uuid = lines[1].trim().to_string();
|
||||
if !uuid.is_empty() {
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"ashell-default-hardware-uuid-fallback".to_string()
|
||||
}
|
||||
|
||||
fn encrypt_config(config: &ConfigFile, password: &str) -> Result<Vec<u8>> {
|
||||
let mut salt = [0u8; 16];
|
||||
let mut nonce = [0u8; 24];
|
||||
OsRng.fill_bytes(&mut salt);
|
||||
OsRng.fill_bytes(&mut nonce);
|
||||
|
||||
let mut key = [0u8; 32];
|
||||
Argon2::default()
|
||||
.hash_password_into(password.as_bytes(), &salt, &mut key)
|
||||
.map_err(|err| anyhow::anyhow!("derive encryption key: {err}"))?;
|
||||
|
||||
let plaintext = serde_json::to_vec(config).context("serialize config")?;
|
||||
let ciphertext = XChaCha20Poly1305::new((&key).into())
|
||||
.encrypt(XNonce::from_slice(&nonce), plaintext.as_ref())
|
||||
.map_err(|_| anyhow::anyhow!("encrypt config payload"))?;
|
||||
|
||||
serde_json::to_vec_pretty(&EncryptedConfigEnvelope {
|
||||
format_version: 1,
|
||||
kdf: "argon2id".to_string(),
|
||||
cipher: "xchacha20poly1305".to_string(),
|
||||
salt: STANDARD.encode(salt),
|
||||
nonce: STANDARD.encode(nonce),
|
||||
payload: STANDARD.encode(ciphertext),
|
||||
})
|
||||
.context("serialize encrypted config envelope")
|
||||
}
|
||||
|
||||
fn decrypt_config(raw: &[u8], password: &str) -> Result<ConfigFile> {
|
||||
let envelope: EncryptedConfigEnvelope =
|
||||
serde_json::from_slice(raw).context("parse encrypted config envelope")?;
|
||||
if envelope.format_version != 1
|
||||
|| envelope.kdf != "argon2id"
|
||||
|| envelope.cipher != "xchacha20poly1305"
|
||||
{
|
||||
return Err(anyhow::anyhow!("unsupported encrypted config format"));
|
||||
}
|
||||
let salt = STANDARD
|
||||
.decode(envelope.salt)
|
||||
.context("decode config salt")?;
|
||||
let nonce = STANDARD
|
||||
.decode(envelope.nonce)
|
||||
.context("decode config nonce")?;
|
||||
if nonce.len() != 24 {
|
||||
return Err(anyhow::anyhow!("invalid config nonce"));
|
||||
}
|
||||
let ciphertext = STANDARD
|
||||
.decode(envelope.payload)
|
||||
.context("decode encrypted config payload")?;
|
||||
|
||||
let mut key = [0u8; 32];
|
||||
Argon2::default()
|
||||
.hash_password_into(password.as_bytes(), &salt, &mut key)
|
||||
.map_err(|err| anyhow::anyhow!("derive encryption key: {err}"))?;
|
||||
|
||||
let plaintext = XChaCha20Poly1305::new((&key).into())
|
||||
.decrypt(XNonce::from_slice(&nonce), ciphertext.as_ref())
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!("cannot decrypt config; hardware UUID mismatch or corrupted data")
|
||||
})?;
|
||||
|
||||
serde_json::from_slice(&plaintext).context("parse decrypted config")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_hardware_uuid() {
|
||||
let uuid = get_hardware_uuid();
|
||||
assert!(!uuid.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_encryption_roundtrip() {
|
||||
let config = ConfigFile::default();
|
||||
let password = "test-password-123";
|
||||
let encrypted = encrypt_config(&config, password).unwrap();
|
||||
|
||||
// Ensure it doesn't contain plain text fields of default config
|
||||
let encrypted_str = String::from_utf8_lossy(&encrypted);
|
||||
assert!(!encrypted_str.contains("Maple Mono NF CN"));
|
||||
assert!(encrypted_str.contains("argon2id"));
|
||||
|
||||
let decrypted = decrypt_config(&encrypted, password).unwrap();
|
||||
assert_eq!(decrypted.terminal_font_family, config.terminal_font_family);
|
||||
|
||||
// Decrypt with wrong password should fail
|
||||
assert!(decrypt_config(&encrypted, "wrong-password").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -102,9 +102,7 @@ impl Ashell {
|
||||
|
||||
let mut session = match self.ssh_auth_method {
|
||||
AuthMethod::Password => Session::password(host, port, user, password),
|
||||
AuthMethod::Key => {
|
||||
Session::key(host, port, user, key_path, key_inline, passphrase)
|
||||
}
|
||||
AuthMethod::Key => Session::key(host, port, user, key_path, key_inline, passphrase),
|
||||
AuthMethod::Config => {
|
||||
// Force key_inline to empty — config mode never uses inline key content.
|
||||
// The backend will try default keys from ~/.ssh/ if no explicit key path is set.
|
||||
@@ -785,6 +783,13 @@ impl Ashell {
|
||||
}
|
||||
|
||||
pub(crate) fn handle_tab_close(&mut self, id: String) {
|
||||
if self
|
||||
.connection_progress
|
||||
.as_ref()
|
||||
.map_or(false, |p| p.tab_id == id)
|
||||
{
|
||||
self.connection_progress = None;
|
||||
}
|
||||
let group_ix = self
|
||||
.tab_groups
|
||||
.iter()
|
||||
@@ -1005,7 +1010,7 @@ impl Ashell {
|
||||
self.active_tab
|
||||
.as_ref()
|
||||
.and_then(|id| self.tabs.iter().find(|t| &t.id == id))
|
||||
.map(TerminalTab::render_snapshot)
|
||||
.map(|t| t.render_snapshot(self.config.keyword_highlight()))
|
||||
}
|
||||
|
||||
pub(crate) fn active_kind(&self) -> Option<TabKind> {
|
||||
|
||||
@@ -177,4 +177,3 @@ mod tests {
|
||||
assert_eq!(entries[1].hostname, "5.6.7.8");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -886,8 +886,7 @@ async fn connect_and_authenticate(
|
||||
let passphrase = session.passphrase.trim();
|
||||
let passphrase = (!passphrase.is_empty()).then_some(passphrase);
|
||||
let success =
|
||||
authenticate_with_default_keys(&mut handle, &session.user, passphrase)
|
||||
.await?;
|
||||
authenticate_with_default_keys(&mut handle, &session.user, passphrase).await?;
|
||||
if !success {
|
||||
return Err(anyhow!(
|
||||
"public key authentication failed for {}@{}:{} - no valid default key found in ~/.ssh/",
|
||||
@@ -940,8 +939,7 @@ async fn connect_and_authenticate(
|
||||
let passphrase = session.passphrase.trim();
|
||||
let passphrase = (!passphrase.is_empty()).then_some(passphrase);
|
||||
let success =
|
||||
authenticate_with_default_keys(&mut handle, &session.user, passphrase)
|
||||
.await?;
|
||||
authenticate_with_default_keys(&mut handle, &session.user, passphrase).await?;
|
||||
if !success {
|
||||
return Err(anyhow!(
|
||||
"ssh-config authentication failed for {}@{}:{} - no valid default key found",
|
||||
|
||||
+32
-17
@@ -375,7 +375,12 @@ impl TerminalElement {
|
||||
fn layout_grid(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> (Vec<LayoutRect>, Vec<BatchedTextRun>, Vec<LayoutCustomBlock>, Vec<LayoutUnderline>) {
|
||||
) -> (
|
||||
Vec<LayoutRect>,
|
||||
Vec<BatchedTextRun>,
|
||||
Vec<LayoutCustomBlock>,
|
||||
Vec<LayoutUnderline>,
|
||||
) {
|
||||
let view_read = self.view.read(cx);
|
||||
let hovered_url = view_read.hovered_url.clone();
|
||||
|
||||
@@ -439,12 +444,18 @@ impl TerminalElement {
|
||||
// Apply hover underline if mouse is hovering over this URL
|
||||
if let Some(hu) = &hovered_url {
|
||||
if hu.tab_id == self.tab_id
|
||||
&& hu.cells.contains(&(render_cell.row as usize, render_cell.col as usize))
|
||||
&& hu
|
||||
.cells
|
||||
.contains(&(render_cell.row as usize, render_cell.col as usize))
|
||||
{
|
||||
underlines.push(LayoutUnderline {
|
||||
row: render_cell.row,
|
||||
col: render_cell.col,
|
||||
cells: if cell.flags.contains(Flags::WIDE_CHAR) { 2 } else { 1 },
|
||||
cells: if cell.flags.contains(Flags::WIDE_CHAR) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
},
|
||||
color: style.color,
|
||||
});
|
||||
}
|
||||
@@ -515,7 +526,9 @@ impl TerminalElement {
|
||||
let cursor_style = self.view.read(cx).cursor_style;
|
||||
let show_cursor = match cursor_style {
|
||||
CursorStyle::Blink | CursorStyle::BeamBlink => {
|
||||
if let Ok(duration) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||
if let Ok(duration) =
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
|
||||
{
|
||||
(duration.as_millis() / 600) % 2 == 0
|
||||
} else {
|
||||
true
|
||||
@@ -595,7 +608,7 @@ impl Element for TerminalElement {
|
||||
let tab_id = self.tab_id.clone();
|
||||
let _ = view.update(cx, |this, cx| {
|
||||
let old_bounds = this.terminal_bounds.insert(tab_id.clone(), bounds);
|
||||
|
||||
|
||||
// Sync PTY size unconditionally on every prepaint layout pass to ensure
|
||||
// absolute synchronization with GPUI layout regardless of intermediate events.
|
||||
let line_height = this.terminal_line_height();
|
||||
@@ -604,11 +617,11 @@ impl Element for TerminalElement {
|
||||
let h = bounds.size.height.as_f32();
|
||||
let cols = (w / cell_width).floor().max(1.0) as u16;
|
||||
let rows = (h / line_height).floor().max(1.0) as u16;
|
||||
|
||||
|
||||
if let Some(tab) = this.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||
tab.resize(cols, rows);
|
||||
}
|
||||
|
||||
|
||||
if old_bounds != Some(bounds) {
|
||||
cx.notify();
|
||||
}
|
||||
@@ -640,9 +653,14 @@ impl Element for TerminalElement {
|
||||
) {
|
||||
// Compute a vertical offset to center the text grid vertically,
|
||||
// distributing the leftover pixel remainder evenly to the top and bottom.
|
||||
let grid_height = prepaint.metrics.line_height * (prepaint.bounds.size.height.as_f32() / prepaint.metrics.line_height.as_f32()).floor();
|
||||
let grid_height = prepaint.metrics.line_height
|
||||
* (prepaint.bounds.size.height.as_f32() / prepaint.metrics.line_height.as_f32())
|
||||
.floor();
|
||||
let y_offset = ((prepaint.bounds.size.height - grid_height) / 2.0).max(px(0.0));
|
||||
let draw_origin = point(prepaint.bounds.origin.x, prepaint.bounds.origin.y + y_offset);
|
||||
let draw_origin = point(
|
||||
prepaint.bounds.origin.x,
|
||||
prepaint.bounds.origin.y + y_offset,
|
||||
);
|
||||
|
||||
for rect in &prepaint.rects {
|
||||
rect.paint(draw_origin, prepaint.metrics, window);
|
||||
@@ -657,10 +675,10 @@ impl Element for TerminalElement {
|
||||
}
|
||||
|
||||
for block in &prepaint.custom_blocks {
|
||||
let x = draw_origin.x.as_f32()
|
||||
+ block.col as f32 * prepaint.metrics.cell_width.as_f32();
|
||||
let y = draw_origin.y.as_f32()
|
||||
+ block.row as f32 * prepaint.metrics.line_height.as_f32();
|
||||
let x =
|
||||
draw_origin.x.as_f32() + block.col as f32 * prepaint.metrics.cell_width.as_f32();
|
||||
let y =
|
||||
draw_origin.y.as_f32() + block.row as f32 * prepaint.metrics.line_height.as_f32();
|
||||
paint_custom_block(
|
||||
window,
|
||||
block.c,
|
||||
@@ -801,10 +819,7 @@ fn merge_underlines(mut underlines: Vec<LayoutUnderline>) -> Vec<LayoutUnderline
|
||||
|
||||
for u in underlines {
|
||||
if let Some(last) = merged.last_mut() {
|
||||
if last.row == u.row
|
||||
&& last.color == u.color
|
||||
&& last.col + last.cells as i32 == u.col
|
||||
{
|
||||
if last.row == u.row && last.color == u.color && last.col + last.cells as i32 == u.col {
|
||||
last.cells += u.cells;
|
||||
continue;
|
||||
}
|
||||
|
||||
+467
-168
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use gpui::Hsla;
|
||||
use crate::terminal::RenderCell;
|
||||
use gpui::Hsla;
|
||||
use std::collections::HashMap;
|
||||
|
||||
trait HslaExt {
|
||||
fn into_rgba_like(self, r: u8, g: u8, b: u8) -> Self;
|
||||
@@ -15,10 +15,19 @@ impl HslaExt for Hsla {
|
||||
let min = rf.min(gf).min(bf);
|
||||
let l = (max + min) / 2.0;
|
||||
if max == min {
|
||||
return Hsla { h: 0.0, s: 0.0, l, a: 1.0 };
|
||||
return Hsla {
|
||||
h: 0.0,
|
||||
s: 0.0,
|
||||
l,
|
||||
a: 1.0,
|
||||
};
|
||||
}
|
||||
let d = max - min;
|
||||
let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
|
||||
let s = if l > 0.5 {
|
||||
d / (2.0 - max - min)
|
||||
} else {
|
||||
d / (max + min)
|
||||
};
|
||||
let h = if max == rf {
|
||||
((gf - bf) / d + if gf < bf { 6.0 } else { 0.0 }) / 6.0
|
||||
} else if max == gf {
|
||||
@@ -33,58 +42,58 @@ impl HslaExt for Hsla {
|
||||
#[derive(Debug, Clone)]
|
||||
struct HighlightColors {
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
network: Hsla, // IP addresses
|
||||
url: Hsla, // http://, https://
|
||||
port: Hsla, // :22, :443, etc.
|
||||
}
|
||||
|
||||
fn hsla(r: u8, g: u8, b: u8) -> Hsla {
|
||||
@@ -100,58 +109,58 @@ fn hsla(r: u8, g: u8, b: u8) -> Hsla {
|
||||
fn highlight_colors() -> HighlightColors {
|
||||
HighlightColors {
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
network: hsla(199, 146, 234), // #C792EA purple
|
||||
url: hsla(86, 212, 199), // #56D4C7 teal
|
||||
port: hsla(130, 170, 200), // #82AAC8 muted teal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,18 +211,40 @@ fn highlight_http_codes(
|
||||
// Specific HTTP codes to highlight
|
||||
const HTTP_CODES: &[(u16, bool)] = &[
|
||||
// 2xx
|
||||
(200, true), (201, true), (202, true), (204, true), (206, true),
|
||||
(200, true),
|
||||
(201, true),
|
||||
(202, true),
|
||||
(204, true),
|
||||
(206, true),
|
||||
// 3xx
|
||||
(301, true), (302, true), (304, true), (307, true), (308, true),
|
||||
(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),
|
||||
(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),
|
||||
(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() {
|
||||
if !bytes[i].is_ascii_digit()
|
||||
|| !bytes[i + 1].is_ascii_digit()
|
||||
|| !bytes[i + 2].is_ascii_digit()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -249,10 +280,7 @@ fn highlight_http_codes(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_cells(
|
||||
cells: &[RenderCell],
|
||||
rows: usize,
|
||||
) -> HashMap<(i32, i32), Hsla> {
|
||||
pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32), Hsla> {
|
||||
let colors = highlight_colors();
|
||||
|
||||
let mut row_chars: Vec<Vec<(i32, char)>> = vec![Vec::with_capacity(128); rows];
|
||||
@@ -289,178 +317,458 @@ pub fn highlight_cells(
|
||||
let text = chars_buf.as_str();
|
||||
|
||||
// ── 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"],
|
||||
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. Error keywords ──────────────────────────────────
|
||||
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
|
||||
&["ERROR", "ERR"], colors.error);
|
||||
highlight_keywords(
|
||||
&mut map,
|
||||
text,
|
||||
&byte_to_col,
|
||||
row_i32,
|
||||
&["ERROR", "ERR"],
|
||||
colors.error,
|
||||
);
|
||||
|
||||
// ── 3. Alert ───────────────────────────────────────────
|
||||
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
|
||||
&["ALERT"], colors.alert);
|
||||
highlight_keywords(
|
||||
&mut map,
|
||||
text,
|
||||
&byte_to_col,
|
||||
row_i32,
|
||||
&["ALERT"],
|
||||
colors.alert,
|
||||
);
|
||||
|
||||
// ── 4. Warning keywords ────────────────────────────────
|
||||
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
|
||||
&["WARNING", "WARN"], colors.warning);
|
||||
highlight_keywords(
|
||||
&mut map,
|
||||
text,
|
||||
&byte_to_col,
|
||||
row_i32,
|
||||
&["WARNING", "WARN"],
|
||||
colors.warning,
|
||||
);
|
||||
|
||||
// ── 5. Info keywords ───────────────────────────────────
|
||||
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
|
||||
&["INFO", "INFORMATION", "NOTICE"], colors.info);
|
||||
highlight_keywords(
|
||||
&mut map,
|
||||
text,
|
||||
&byte_to_col,
|
||||
row_i32,
|
||||
&["INFO", "INFORMATION", "NOTICE"],
|
||||
colors.info,
|
||||
);
|
||||
|
||||
// ── 6. Debug keywords ──────────────────────────────────
|
||||
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
|
||||
&["DEBUG", "DBG", "TRACE"], colors.debug);
|
||||
highlight_keywords(
|
||||
&mut map,
|
||||
text,
|
||||
&byte_to_col,
|
||||
row_i32,
|
||||
&["DEBUG", "DBG", "TRACE"],
|
||||
colors.debug,
|
||||
);
|
||||
|
||||
// ── 7. Success status ──────────────────────────────────
|
||||
highlight_keywords(&mut map, text, &byte_to_col, row_i32,
|
||||
&["SUCCESS", "SUCCEEDED", "SUCCESSFUL", "PASSED", "PASS",
|
||||
"OK", "DONE", "COMPLETED", "FINISHED", "COMPLETE"],
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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);
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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,
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
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"],
|
||||
highlight_keywords(
|
||||
&mut map,
|
||||
text,
|
||||
&byte_to_col,
|
||||
row_i32,
|
||||
&[
|
||||
"DEPRECATED",
|
||||
"TODO",
|
||||
"FIXME",
|
||||
"HACK",
|
||||
"XXX",
|
||||
"WORKAROUND",
|
||||
"TEMPORARY",
|
||||
],
|
||||
colors.deprecated,
|
||||
);
|
||||
|
||||
@@ -477,8 +785,6 @@ pub fn highlight_cells(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── 33. Port numbers ───────────────────────────────────
|
||||
for m in find_ports(text) {
|
||||
let port_len = find_port_len(&text[m..]);
|
||||
@@ -537,11 +843,7 @@ fn find_ip_len(text: &str) -> usize {
|
||||
len += 1;
|
||||
}
|
||||
|
||||
if dots == 3 && digits > 0 {
|
||||
len
|
||||
} else {
|
||||
0
|
||||
}
|
||||
if dots == 3 && digits > 0 { len } else { 0 }
|
||||
}
|
||||
|
||||
fn find_ip_addresses(text: &str) -> Vec<usize> {
|
||||
@@ -550,16 +852,12 @@ fn find_ip_addresses(text: &str) -> Vec<usize> {
|
||||
let len = bytes.len();
|
||||
|
||||
for i in 0..len {
|
||||
if bytes[i].is_ascii_digit()
|
||||
&& (i == 0 || is_boundary(bytes[i - 1] as char))
|
||||
{
|
||||
if bytes[i].is_ascii_digit() && (i == 0 || is_boundary(bytes[i - 1] as char)) {
|
||||
let remaining = &text[i..];
|
||||
let ip_len = find_ip_len(remaining);
|
||||
if ip_len > 0 {
|
||||
let ip_str = &remaining[..ip_len];
|
||||
let valid = ip_str
|
||||
.split('.')
|
||||
.all(|octet| octet.parse::<u8>().is_ok());
|
||||
let valid = ip_str.split('.').all(|octet| octet.parse::<u8>().is_ok());
|
||||
if valid {
|
||||
positions.push(i);
|
||||
}
|
||||
@@ -671,7 +969,9 @@ pub fn build_logical_lines<'a>(cells: &'a [RenderCell], rows: usize) -> Vec<Logi
|
||||
let wraps_from_prev = row_idx > 0 && {
|
||||
current_line.as_ref().map_or(false, |line| {
|
||||
line.row_cells.last().map_or(false, |rc| {
|
||||
rc.cell.flags.contains(alacritty_terminal::term::cell::Flags::WRAPLINE)
|
||||
rc.cell
|
||||
.flags
|
||||
.contains(alacritty_terminal::term::cell::Flags::WRAPLINE)
|
||||
})
|
||||
})
|
||||
};
|
||||
@@ -738,4 +1038,3 @@ pub fn find_url_at_cell(
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
+11
-6
@@ -165,7 +165,7 @@ impl Ashell {
|
||||
return;
|
||||
};
|
||||
|
||||
if tab.render_snapshot().display_offset > 0 {
|
||||
if tab.render_snapshot(false).display_offset > 0 {
|
||||
tab.scroll_to_bottom();
|
||||
}
|
||||
tab.clear_selection();
|
||||
@@ -204,7 +204,7 @@ impl Ashell {
|
||||
return;
|
||||
};
|
||||
|
||||
if tab.render_snapshot().display_offset > 0 {
|
||||
if tab.render_snapshot(false).display_offset > 0 {
|
||||
tab.scroll_to_bottom();
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ impl Ashell {
|
||||
return;
|
||||
};
|
||||
|
||||
if tab.render_snapshot().display_offset > 0 {
|
||||
if tab.render_snapshot(false).display_offset > 0 {
|
||||
tab.scroll_to_bottom();
|
||||
}
|
||||
tab.clear_selection();
|
||||
@@ -291,7 +291,7 @@ impl Ashell {
|
||||
return;
|
||||
};
|
||||
|
||||
if tab.render_snapshot().display_offset > 0 {
|
||||
if tab.render_snapshot(false).display_offset > 0 {
|
||||
tab.scroll_to_bottom();
|
||||
}
|
||||
tab.clear_selection();
|
||||
@@ -387,7 +387,12 @@ impl Ashell {
|
||||
if let Some((row, col, _side)) = self.terminal_grid_point_and_side(event.position) {
|
||||
if let Some(snapshot) = self.active_snapshot() {
|
||||
if let Some(active_id) = &self.active_tab {
|
||||
if let Some((url, url_cells)) = crate::terminal::highlight::find_url_at_cell(&snapshot.cells, snapshot.rows, row, col) {
|
||||
if let Some((url, url_cells)) = crate::terminal::highlight::find_url_at_cell(
|
||||
&snapshot.cells,
|
||||
snapshot.rows,
|
||||
row,
|
||||
col,
|
||||
) {
|
||||
hovered_url = Some(crate::app::HoveredUrl {
|
||||
url,
|
||||
tab_id: active_id.clone(),
|
||||
@@ -397,7 +402,7 @@ impl Ashell {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if self.hovered_url != hovered_url || self.cmd_ctrl_pressed != cmd_ctrl_pressed {
|
||||
self.hovered_url = hovered_url;
|
||||
self.cmd_ctrl_pressed = cmd_ctrl_pressed;
|
||||
|
||||
+24
-13
@@ -1,7 +1,7 @@
|
||||
pub mod custom_blocks;
|
||||
pub mod element;
|
||||
pub mod input;
|
||||
pub mod highlight;
|
||||
pub mod input;
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
@@ -116,6 +116,7 @@ impl BackendTx {
|
||||
pub struct TerminalTab {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub dynamic_title: String,
|
||||
pub kind: TabKind,
|
||||
pub status: String,
|
||||
pub connected: bool,
|
||||
@@ -134,7 +135,12 @@ pub struct TerminalTab {
|
||||
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>)>>,
|
||||
pub(crate) highlight_cache: std::cell::RefCell<
|
||||
Option<(
|
||||
Vec<RenderCell>,
|
||||
std::collections::HashMap<(i32, i32), gpui::Hsla>,
|
||||
)>,
|
||||
>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -233,7 +239,8 @@ impl TerminalTab {
|
||||
let shared_backend = std::sync::Arc::new(std::sync::Mutex::new(backend));
|
||||
Self {
|
||||
id: id.clone(),
|
||||
title,
|
||||
title: title.clone(),
|
||||
dynamic_title: title,
|
||||
kind,
|
||||
status,
|
||||
connected: matches!(kind, TabKind::Local),
|
||||
@@ -242,7 +249,7 @@ impl TerminalTab {
|
||||
backend_initialized: true,
|
||||
session: None,
|
||||
processor: Processor::new(),
|
||||
term: new_term(100, 30, shared_backend.clone(), id, events),
|
||||
term: new_term(100, 30, shared_backend.clone(), id, events.clone()),
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
backend: shared_backend,
|
||||
@@ -312,7 +319,7 @@ impl TerminalTab {
|
||||
self.term.mode().contains(TermMode::APP_CURSOR)
|
||||
}
|
||||
|
||||
pub fn render_snapshot(&self) -> RenderSnapshot {
|
||||
pub fn render_snapshot(&self, keyword_highlight: bool) -> RenderSnapshot {
|
||||
let rows = self.rows;
|
||||
let cols = self.cols;
|
||||
let content = self.term.renderable_content();
|
||||
@@ -342,15 +349,13 @@ impl TerminalTab {
|
||||
}
|
||||
|
||||
// Get highlights from cache or recompute, only if keyword_highlight is enabled.
|
||||
let is_enabled = crate::session::config::ConfigStore::load()
|
||||
.map(|c| c.keyword_highlight())
|
||||
.unwrap_or(false);
|
||||
let is_enabled = keyword_highlight;
|
||||
|
||||
let highlights = if is_enabled {
|
||||
let mut cache = self.highlight_cache.borrow_mut();
|
||||
let cache_valid = cache.as_ref().is_some_and(|(cached_cells, _)| {
|
||||
cached_cells == &cells
|
||||
});
|
||||
let cache_valid = cache
|
||||
.as_ref()
|
||||
.is_some_and(|(cached_cells, _)| cached_cells == &cells);
|
||||
if cache_valid {
|
||||
cache.as_ref().unwrap().1.clone()
|
||||
} else {
|
||||
@@ -503,7 +508,10 @@ fn viewport_selection_from_range(
|
||||
} = selection.as_ref().copied()?;
|
||||
|
||||
let top_point = viewport_to_point(display_offset, Point::new(0, Column(0)));
|
||||
let bottom_point = viewport_to_point(display_offset, Point::new(rows.saturating_sub(1), Column(0)));
|
||||
let bottom_point = viewport_to_point(
|
||||
display_offset,
|
||||
Point::new(rows.saturating_sub(1), Column(0)),
|
||||
);
|
||||
|
||||
let top_line = top_point.line;
|
||||
let bottom_line = bottom_point.line;
|
||||
@@ -521,7 +529,10 @@ fn viewport_selection_from_range(
|
||||
} else if end.line > bottom_line {
|
||||
Point::new(rows.saturating_sub(1), Column(cols.saturating_sub(1)))
|
||||
} else {
|
||||
point_to_viewport(display_offset, end).unwrap_or(Point::new(rows.saturating_sub(1), Column(cols.saturating_sub(1))))
|
||||
point_to_viewport(display_offset, end).unwrap_or(Point::new(
|
||||
rows.saturating_sub(1),
|
||||
Column(cols.saturating_sub(1)),
|
||||
))
|
||||
};
|
||||
|
||||
Some(ViewportSelection {
|
||||
|
||||
Reference in New Issue
Block a user