diff --git a/src/app/keybinding_recorder.rs b/src/app/keybinding_recorder.rs index d5bee35..2a20032 100644 --- a/src/app/keybinding_recorder.rs +++ b/src/app/keybinding_recorder.rs @@ -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", diff --git a/src/app/resizable/panel.rs b/src/app/resizable/panel.rs index 1d3dee9..90ba0ae 100644 --- a/src/app/resizable/panel.rs +++ b/src/app/resizable/panel.rs @@ -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) }) diff --git a/src/app/resizable/resize_handle.rs b/src/app/resizable/resize_handle.rs index 3244f06..1289f08 100644 --- a/src/app/resizable/resize_handle.rs +++ b/src/app/resizable/resize_handle.rs @@ -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 Element for ResizeHandle { ) }) .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()) diff --git a/src/app/search.rs b/src/app/search.rs index 70784ce..552965d 100644 --- a/src/app/search.rs +++ b/src/app/search.rs @@ -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; @@ -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, - ) -> impl gpui::IntoElement { + pub(crate) fn render_search_button(&self, cx: &mut Context) -> 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| { diff --git a/src/app/startup.rs b/src/app/startup.rs index 3e71162..e905898 100644 --- a/src/app/startup.rs +++ b/src/app/startup.rs @@ -179,7 +179,14 @@ pub(crate) fn sync_macos_launch_environment() { } fn read_proxy_from_env() -> Option<(String, String, Option, 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); diff --git a/src/app/theme.rs b/src/app/theme.rs index fd4da05..3d09263 100644 --- a/src/app/theme.rs +++ b/src/app/theme.rs @@ -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 { diff --git a/src/app/ui.rs b/src/app/ui.rs index 1f010f2..c778f4c 100644 --- a/src/app/ui.rs +++ b/src/app/ui.rs @@ -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::{ @@ -1984,7 +1984,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 +1999,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 +2028,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 +2036,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 +2062,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 +2070,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 +2105,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("+"), ), ) }) @@ -2334,7 +2358,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 +2402,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 diff --git a/src/backend/ssh.rs b/src/backend/ssh.rs index bafeb3d..ac3c288 100644 --- a/src/backend/ssh.rs +++ b/src/backend/ssh.rs @@ -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", diff --git a/src/main.rs b/src/main.rs index a7aa815..2fceb39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); diff --git a/src/session/config.rs b/src/session/config.rs index 39623a2..2514db6 100644 --- a/src/session/config.rs +++ b/src/session/config.rs @@ -351,8 +351,8 @@ impl ConfigStore { } let mut cache = if path.exists() { - let raw_bytes = fs::read(&path) - .with_context(|| format!("failed to read {}", path.display()))?; + 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, @@ -1031,7 +1031,12 @@ fn get_hardware_uuid() -> String { #[cfg(target_os = "windows")] { if let Ok(output) = std::process::Command::new("reg") - .args(&["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]) + .args(&[ + "query", + "HKLM\\SOFTWARE\\Microsoft\\Cryptography", + "/v", + "MachineGuid", + ]) .output() { let stdout = String::from_utf8_lossy(&output.stdout); @@ -1069,17 +1074,17 @@ fn encrypt_config(config: &ConfigFile, password: &str) -> Result> { 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(), @@ -1100,7 +1105,9 @@ fn decrypt_config(raw: &[u8], password: &str) -> Result { { return Err(anyhow::anyhow!("unsupported encrypted config format")); } - let salt = STANDARD.decode(envelope.salt).context("decode config salt")?; + let salt = STANDARD + .decode(envelope.salt) + .context("decode config salt")?; let nonce = STANDARD .decode(envelope.nonce) .context("decode config nonce")?; @@ -1110,16 +1117,18 @@ fn decrypt_config(raw: &[u8], password: &str) -> Result { 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"))?; - + .map_err(|_| { + anyhow::anyhow!("cannot decrypt config; hardware UUID mismatch or corrupted data") + })?; + serde_json::from_slice(&plaintext).context("parse decrypted config") } @@ -1138,17 +1147,16 @@ mod tests { 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()); } } - diff --git a/src/session/mod.rs b/src/session/mod.rs index eb569bb..1b659f6 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -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,7 +783,11 @@ 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) { + if self + .connection_progress + .as_ref() + .map_or(false, |p| p.tab_id == id) + { self.connection_progress = None; } let group_ix = self diff --git a/src/session/ssh_config.rs b/src/session/ssh_config.rs index 5ef1fce..96edd68 100644 --- a/src/session/ssh_config.rs +++ b/src/session/ssh_config.rs @@ -177,4 +177,3 @@ mod tests { assert_eq!(entries[1].hostname, "5.6.7.8"); } } - diff --git a/src/sftp/mod.rs b/src/sftp/mod.rs index ab4348a..3249260 100644 --- a/src/sftp/mod.rs +++ b/src/sftp/mod.rs @@ -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", diff --git a/src/terminal/element.rs b/src/terminal/element.rs index 09c6a39..524cc73 100644 --- a/src/terminal/element.rs +++ b/src/terminal/element.rs @@ -375,7 +375,12 @@ impl TerminalElement { fn layout_grid( &self, cx: &App, - ) -> (Vec, Vec, Vec, Vec) { + ) -> ( + Vec, + Vec, + Vec, + Vec, + ) { 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) -> Vec 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![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 { @@ -550,16 +852,12 @@ fn find_ip_addresses(text: &str) -> Vec { 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::().is_ok()); + let valid = ip_str.split('.').all(|octet| octet.parse::().is_ok()); if valid { positions.push(i); } @@ -671,7 +969,9 @@ pub fn build_logical_lines<'a>(cells: &'a [RenderCell], rows: usize) -> Vec 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 } - diff --git a/src/terminal/input.rs b/src/terminal/input.rs index 1ae2e78..eac0a5f 100644 --- a/src/terminal/input.rs +++ b/src/terminal/input.rs @@ -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; diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs index 3ef083d..4a551db 100644 --- a/src/terminal/mod.rs +++ b/src/terminal/mod.rs @@ -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; @@ -134,7 +134,12 @@ pub struct TerminalTab { pub rows: u16, pub backend: std::sync::Arc>, pub scroll_pixel_y: f32, - pub(crate) highlight_cache: std::cell::RefCell, std::collections::HashMap<(i32, i32), gpui::Hsla>)>>, + pub(crate) highlight_cache: std::cell::RefCell< + Option<( + Vec, + std::collections::HashMap<(i32, i32), gpui::Hsla>, + )>, + >, } #[derive(Clone, Copy)] @@ -348,9 +353,9 @@ impl TerminalTab { 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 {