diff --git a/src/config.rs b/src/config.rs index 2927f25..757dd43 100644 --- a/src/config.rs +++ b/src/config.rs @@ -214,6 +214,7 @@ impl ConfigStore { return Ok(()); } let raw = serde_json::to_string_pretty(&self.cache)?; - fs::write(&self.path, raw).with_context(|| format!("failed to write {}", self.path.display())) + fs::write(&self.path, raw) + .with_context(|| format!("failed to write {}", self.path.display())) } } diff --git a/src/local_terminal.rs b/src/local_terminal.rs index 13db406..587fbde 100644 --- a/src/local_terminal.rs +++ b/src/local_terminal.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::{Context, Result}; -use portable_pty::{native_pty_system, CommandBuilder, PtySize}; +use portable_pty::{CommandBuilder, PtySize, native_pty_system}; use crate::terminal::{BackendCommand, BackendEvent, BackendTx}; @@ -34,7 +34,10 @@ pub fn spawn_local_terminal( }); let mut cmd = CommandBuilder::new(&shell); - cmd.env("TERM", std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".into())); + cmd.env( + "TERM", + std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".into()), + ); cmd.env( "COLORTERM", std::env::var("COLORTERM").unwrap_or_else(|_| "truecolor".into()), diff --git a/src/main.rs b/src/main.rs index afe8b7d..d9b4bd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,6 @@ use std::{ use alacritty_terminal::index::Side; use alacritty_terminal::selection::SelectionType; use anyhow::{Context as _, Result}; -use rust_i18n::t; use gpui::{ Anchor, App, AppContext as _, Bounds, ClipboardItem, Context, ElementId, Entity, FocusHandle, Focusable as _, FontWeight, Hsla, InteractiveElement as _, IntoElement, KeyBinding, @@ -38,6 +37,7 @@ use gpui_component::{ v_flex, }; use gpui_component_assets::Assets; +use rust_i18n::t; use tokio::runtime::Runtime; use uuid::Uuid; @@ -204,8 +204,7 @@ struct SftpContextMenuState { impl Ashell { fn new(window: &mut Window, cx: &mut Context) -> Self { - let host_input = - cx.new(|cx| InputState::new(window, cx).placeholder(t!("host"))); + let host_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("host"))); let session_name_input = cx.new(|cx| InputState::new(window, cx).placeholder("name (optional)")); let port_input = cx.new(|cx| InputState::new(window, cx).default_value("22")); @@ -907,9 +906,7 @@ impl Ashell { div() .text_size(px(11.)) .text_color(_cx.theme().muted_foreground) - .child(t!( - "open_local_shell_tab" - )), + .child(t!("open_local_shell_tab")), ), ), ) @@ -951,9 +948,7 @@ impl Ashell { div() .text_size(px(11.)) .text_color(_cx.theme().muted_foreground) - .child(t!( - "create_or_edit_ssh_session" - )), + .child(t!("create_or_edit_ssh_session")), ), ), ) @@ -1051,27 +1046,25 @@ impl Ashell { h_flex() .items_center() .gap_3() - .child(div().w(px(180.)).child(t!("terminal_font_size").to_string())) - .child( - Button::new("font-size-down") - .label("-") - .on_click(window.listener_for(&view, |this, _, _, cx| { - this.change_terminal_font_size(-1.0, cx) - })), - ) .child( div() - .min_w(px(64.)) - .text_center() - .child(format!("{:.0}px", view.read(cx).terminal_font_size)), + .w(px(180.)) + .child(t!("terminal_font_size").to_string()), ) - .child( - Button::new("font-size-up") - .label("+") - .on_click(window.listener_for(&view, |this, _, _, cx| { - this.change_terminal_font_size(1.0, cx) - })), - ), + .child(Button::new("font-size-down").label("-").on_click( + window.listener_for(&view, |this, _, _, cx| { + this.change_terminal_font_size(-1.0, cx) + }), + )) + .child(div().min_w(px(64.)).text_center().child(format!( + "{:.0}px", + view.read(cx).terminal_font_size + ))) + .child(Button::new("font-size-up").label("+").on_click( + window.listener_for(&view, |this, _, _, cx| { + this.change_terminal_font_size(1.0, cx) + }), + )), ) .child( h_flex() @@ -1083,7 +1076,8 @@ impl Ashell { .small() .icon(IconName::Globe) .label({ - let current_locale = view.read(cx).config.locale().to_string(); + let current_locale = + view.read(cx).config.locale().to_string(); if current_locale == "en" { t!("english").to_string() } else if current_locale == "zh-CN" { @@ -1095,35 +1089,60 @@ impl Ashell { .dropdown_menu_with_anchor(Anchor::BottomRight, { let view = view.clone(); move |mut menu, window, cx| { - let current_locale = view.read(cx).config.locale().to_string(); + let current_locale = view + .read(cx) + .config + .locale() + .to_string(); menu = menu .min_w(160.) .item( - PopupMenuItem::new(t!("follow_system").to_string()) - .checked(current_locale == "system") - .on_click(window.listener_for(&view, |this, _, window, cx| { - this.set_display_language("system", window, cx) - })), + PopupMenuItem::new( + t!("follow_system").to_string(), + ) + .checked(current_locale == "system") + .on_click(window.listener_for( + &view, + |this, _, window, cx| { + this.set_display_language( + "system", window, cx, + ) + }, + )), ) .separator() .item( - PopupMenuItem::new(t!("english").to_string()) - .checked(current_locale == "en") - .on_click(window.listener_for(&view, |this, _, window, cx| { - this.set_display_language("en", window, cx) - })), + PopupMenuItem::new( + t!("english").to_string(), + ) + .checked(current_locale == "en") + .on_click(window.listener_for( + &view, + |this, _, window, cx| { + this.set_display_language( + "en", window, cx, + ) + }, + )), ) .item( - PopupMenuItem::new(t!("chinese").to_string()) - .checked(current_locale == "zh-CN") - .on_click(window.listener_for(&view, |this, _, window, cx| { - this.set_display_language("zh-CN", window, cx) - })), + PopupMenuItem::new( + t!("chinese").to_string(), + ) + .checked(current_locale == "zh-CN") + .on_click(window.listener_for( + &view, + |this, _, window, cx| { + this.set_display_language( + "zh-CN", window, cx, + ) + }, + )), ); menu } - }) - ) + }), + ), ) .child( div() @@ -2533,7 +2552,7 @@ impl Ashell { .border_color(cx.theme().border) .bg(cx.theme().background) .on_drop( - cx.listener(|this, paths: &gpui::ExternalPaths, window, cx| { + cx.listener(|this, paths: &gpui::ExternalPaths, _window, cx| { let paths_to_upload: Vec = paths .0 .iter() @@ -3600,8 +3619,7 @@ fn main() { .with_quit_mode(QuitMode::Explicit); #[cfg(not(target_os = "macos"))] - let app = gpui_platform::application() - .with_assets(Assets); + let app = gpui_platform::application().with_assets(Assets); app.on_reopen(|cx| { if cx.windows().is_empty() { open_main_window(cx); diff --git a/src/sftp.rs b/src/sftp.rs index c6e7415..db98faf 100644 --- a/src/sftp.rs +++ b/src/sftp.rs @@ -4,15 +4,15 @@ use std::{ sync::Arc, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; use async_trait::async_trait; use chrono::{DateTime, TimeZone, Utc}; use directories::BaseDirs; use flate2::read::GzDecoder; use russh::{ - client::{self, Handler}, - keys::{decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key, HashAlg, PrivateKey}, Disconnect, + client::{self, Handler}, + keys::{HashAlg, PrivateKey, decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key}, }; use russh_sftp::client::SftpSession; use tokio::{ @@ -38,6 +38,7 @@ pub struct RemoteEntry { pub modified: u32, } +#[allow(dead_code)] #[derive(Debug, Clone)] pub struct PreviewData { pub path: String, @@ -49,9 +50,16 @@ pub struct PreviewData { #[derive(Debug)] pub enum SftpCommand { ListDir(String), + #[allow(dead_code)] Preview(String), - Download { remote: String, local_dir: String }, - UploadPaths { locals: Vec, remote_dir: String }, + Download { + remote: String, + local_dir: String, + }, + UploadPaths { + locals: Vec, + remote_dir: String, + }, Close, } @@ -75,16 +83,21 @@ impl SftpHandle { let _ = self.commands.send(SftpCommand::ListDir(path)); } + #[allow(dead_code)] pub fn preview(&self, path: String) { let _ = self.commands.send(SftpCommand::Preview(path)); } pub fn download(&self, remote: String, local_dir: String) { - let _ = self.commands.send(SftpCommand::Download { remote, local_dir }); + let _ = self + .commands + .send(SftpCommand::Download { remote, local_dir }); } pub fn upload_paths(&self, locals: Vec, remote_dir: String) { - let _ = self.commands.send(SftpCommand::UploadPaths { locals, remote_dir }); + let _ = self + .commands + .send(SftpCommand::UploadPaths { locals, remote_dir }); } pub fn close(&self) { @@ -137,7 +150,10 @@ async fn run_sftp( .await .context("sftp handshake")?; - let home = sftp.canonicalize(".").await.unwrap_or_else(|_| "/".to_string()); + let home = sftp + .canonicalize(".") + .await + .unwrap_or_else(|_| "/".to_string()); emit_entries(&events, &tab_id, &sftp, &home).await?; while let Some(command) = commands.recv().await { @@ -218,7 +234,9 @@ async fn run_sftp( } } - let _ = handle.disconnect(Disconnect::ByApplication, "bye", "").await; + let _ = handle + .disconnect(Disconnect::ByApplication, "bye", "") + .await; Ok(()) } @@ -241,7 +259,9 @@ async fn emit_entries( Ok(()) } -async fn connect_and_authenticate(session: &Session) -> Result> { +async fn connect_and_authenticate( + session: &Session, +) -> Result> { let config = Arc::new(client::Config { inactivity_timeout: Some(std::time::Duration::from_secs(600)), ..Default::default() @@ -368,6 +388,7 @@ fn join_remote(parent: &str, child: &str) -> String { } } +#[allow(dead_code)] fn strip_archive_suffix(name: &str) -> &str { for suffix in [".tar.gz", ".tgz", ".zip", ".tar"] { if let Some(stripped) = name.strip_suffix(suffix) { @@ -437,7 +458,10 @@ async fn list_dir_impl(sftp: &SftpSession, path: &str) -> Result Result { - let metadata = sftp.metadata(path).await.with_context(|| format!("metadata {path}"))?; + let metadata = sftp + .metadata(path) + .await + .with_context(|| format!("metadata {path}"))?; let is_dir = metadata .permissions .map(|mode| (mode & 0o170_000) == 0o040_000) @@ -458,9 +482,15 @@ async fn preview_impl(sftp: &SftpSession, path: &str) -> Result { }); } - let mut remote_file = sftp.open(path).await.with_context(|| format!("open remote {path}"))?; + let mut remote_file = sftp + .open(path) + .await + .with_context(|| format!("open remote {path}"))?; let mut buffer = vec![0u8; 128 * 1024]; - let read = remote_file.read(&mut buffer).await.context("read preview bytes")?; + let read = remote_file + .read(&mut buffer) + .await + .context("read preview bytes")?; buffer.truncate(read); let nul_ratio = if buffer.is_empty() { @@ -496,7 +526,10 @@ async fn download_path_impl( .await .with_context(|| format!("create {}", local_dir.display()))?; - let metadata = sftp.metadata(remote).await.with_context(|| format!("metadata {remote}"))?; + let metadata = sftp + .metadata(remote) + .await + .with_context(|| format!("metadata {remote}"))?; let is_dir = metadata .permissions .map(|mode| (mode & 0o170_000) == 0o040_000) @@ -508,7 +541,8 @@ async fn download_path_impl( base_name(remote), Uuid::new_v4() )); - let extracted_to = download_remote_directory_archive(handle, sftp, remote, &local_archive).await?; + let extracted_to = + download_remote_directory_archive(handle, sftp, remote, &local_archive).await?; return Ok(format!("downloaded folder to {}", extracted_to.display())); } @@ -517,7 +551,12 @@ async fn download_path_impl( Ok(format!("downloaded file to {}", local_path.display())) } -async fn download_dir_recursive(sftp: &SftpSession, remote_dir: &str, local_dir: &Path) -> Result<()> { +#[allow(dead_code)] +async fn download_dir_recursive( + sftp: &SftpSession, + remote_dir: &str, + local_dir: &Path, +) -> Result<()> { tokio::fs::create_dir_all(local_dir) .await .with_context(|| format!("create {}", local_dir.display()))?; @@ -540,7 +579,11 @@ async fn download_remote_directory_archive( remote_dir: &str, local_archive: &Path, ) -> Result { - let remote_archive = format!("/tmp/ashell-{}-{}.tar.gz", base_name(remote_dir), Uuid::new_v4()); + let remote_archive = format!( + "/tmp/ashell-{}-{}.tar.gz", + base_name(remote_dir), + Uuid::new_v4() + ); create_remote_archive(handle, remote_dir, &remote_archive).await?; let local_extract_root = local_archive .parent() @@ -549,7 +592,11 @@ async fn download_remote_directory_archive( let archive_download = async { download_file_impl(sftp, &remote_archive, local_archive).await?; - extract_archive_to(local_archive, local_archive.parent().unwrap_or_else(|| Path::new("."))).await?; + extract_archive_to( + local_archive, + local_archive.parent().unwrap_or_else(|| Path::new(".")), + ) + .await?; tokio::fs::remove_file(local_archive) .await .with_context(|| format!("remove {}", local_archive.display()))?; @@ -568,14 +615,20 @@ async fn download_remote_directory_archive( } async fn download_file_impl(sftp: &SftpSession, remote: &str, local: &Path) -> Result<()> { - let mut remote_file = sftp.open(remote).await.with_context(|| format!("open remote {remote}"))?; + let mut remote_file = sftp + .open(remote) + .await + .with_context(|| format!("open remote {remote}"))?; let mut local_file = tokio::fs::File::create(local) .await .with_context(|| format!("create local {}", local.display()))?; let mut buffer = vec![0u8; 64 * 1024]; loop { - let read = remote_file.read(&mut buffer).await.context("read remote file")?; + let read = remote_file + .read(&mut buffer) + .await + .context("read remote file")?; if read == 0 { break; } @@ -588,7 +641,11 @@ async fn download_file_impl(sftp: &SftpSession, remote: &str, local: &Path) -> R Ok(()) } -async fn upload_paths_impl(sftp: &SftpSession, locals: &[String], remote_dir: &str) -> Result { +async fn upload_paths_impl( + sftp: &SftpSession, + locals: &[String], + remote_dir: &str, +) -> Result { create_remote_dir_all(sftp, remote_dir).await?; let mut file_count = 0usize; let mut folder_count = 0usize; @@ -612,7 +669,11 @@ async fn upload_paths_impl(sftp: &SftpSession, locals: &[String], remote_dir: &s Ok(summary) } -async fn upload_directory_impl(sftp: &SftpSession, local_dir: &Path, remote_parent: &str) -> Result<()> { +async fn upload_directory_impl( + sftp: &SftpSession, + local_dir: &Path, + remote_parent: &str, +) -> Result<()> { let root_name = local_dir .file_name() .and_then(|name| name.to_str()) @@ -651,7 +712,11 @@ async fn upload_directory_impl(sftp: &SftpSession, local_dir: &Path, remote_pare Ok(()) } -async fn upload_file_to_dir_impl(sftp: &SftpSession, local_file: &Path, remote_dir: &str) -> Result<()> { +async fn upload_file_to_dir_impl( + sftp: &SftpSession, + local_file: &Path, + remote_dir: &str, +) -> Result<()> { let file_name = local_file .file_name() .and_then(|name| name.to_str()) @@ -787,8 +852,13 @@ fn shell_quote(value: &str) -> String { format!("'{}'", value.replace('\'', "'\"'\"'")) } +#[allow(dead_code)] async fn maybe_extract_archive(path: &Path) -> Result> { - let Some(file_name) = path.file_name().and_then(|name| name.to_str()).map(|name| name.to_string()) else { + let Some(file_name) = path + .file_name() + .and_then(|name| name.to_str()) + .map(|name| name.to_string()) + else { return Ok(None); }; let is_archive = [".zip", ".tar", ".tar.gz", ".tgz"] @@ -834,7 +904,9 @@ async fn maybe_extract_archive(path: &Path) -> Result> { .with_context(|| format!("open {}", archive_path.display()))?; let decoder = GzDecoder::new(file); let mut archive = tar::Archive::new(decoder); - archive.unpack(&target_dir).context("unpack tar.gz archive")?; + archive + .unpack(&target_dir) + .context("unpack tar.gz archive")?; } else if file_name.ends_with(".tar") { let file = fs::File::open(&archive_path) .with_context(|| format!("open {}", archive_path.display()))?; @@ -851,7 +923,11 @@ async fn maybe_extract_archive(path: &Path) -> Result> { } async fn extract_archive_to(path: &Path, target_dir: &Path) -> Result<()> { - let Some(file_name) = path.file_name().and_then(|name| name.to_str()).map(|name| name.to_string()) else { + let Some(file_name) = path + .file_name() + .and_then(|name| name.to_str()) + .map(|name| name.to_string()) + else { return Ok(()); }; let archive_path = path.to_path_buf(); @@ -886,7 +962,9 @@ async fn extract_archive_to(path: &Path, target_dir: &Path) -> Result<()> { .with_context(|| format!("open {}", archive_path.display()))?; let decoder = GzDecoder::new(file); let mut archive = tar::Archive::new(decoder); - archive.unpack(&target_dir).context("unpack tar.gz archive")?; + archive + .unpack(&target_dir) + .context("unpack tar.gz archive")?; } else if file_name.ends_with(".tar") { let file = fs::File::open(&archive_path) .with_context(|| format!("open {}", archive_path.display()))?; diff --git a/src/ssh_terminal.rs b/src/ssh_terminal.rs index ab90eb5..721d279 100644 --- a/src/ssh_terminal.rs +++ b/src/ssh_terminal.rs @@ -3,19 +3,19 @@ use std::{ sync::Arc, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; use async_trait::async_trait; use directories::BaseDirs; use russh::{ - client::{self, Handler}, - keys::{decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key, HashAlg, PrivateKey}, ChannelMsg, Disconnect, + client::{self, Handler}, + keys::{HashAlg, PrivateKey, decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key}, }; use tokio::sync::mpsc; use crate::{ config::{AuthMethod, Session}, - system::{remote_snapshot_from_kv, SystemSnapshot}, + system::{SystemSnapshot, remote_snapshot_from_kv}, terminal::{BackendCommand, BackendEvent, BackendTx}, }; diff --git a/src/system.rs b/src/system.rs index 93bb668..0f0d0fd 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,6 +1,9 @@ -use std::{collections::BTreeMap, time::{Duration, Instant}}; +use std::{ + collections::BTreeMap, + time::{Duration, Instant}, +}; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use sysinfo::{Disks, Networks, System}; #[derive(Debug, Clone, Default)] @@ -69,7 +72,10 @@ impl SystemSampler { let rx_total: u64 = self.nets.iter().map(|(_, d)| d.total_received()).sum(); let tx_total: u64 = self.nets.iter().map(|(_, d)| d.total_transmitted()).sum(); let now = Instant::now(); - let elapsed = now.duration_since(self.last_instant).as_secs_f64().max(0.001); + let elapsed = now + .duration_since(self.last_instant) + .as_secs_f64() + .max(0.001); let rx_rate = (rx_total.saturating_sub(self.last_rx_total) as f64 / elapsed) as u64; let tx_rate = (tx_total.saturating_sub(self.last_tx_total) as f64 / elapsed) as u64; self.last_rx_total = rx_total; @@ -92,16 +98,8 @@ impl SystemSampler { cpu_percent, mem_percent: ratio(mem_used, mem_total), swap_percent: ratio(swap_used, swap_total), - mem_detail: format!( - "{}/{}", - format_bytes(mem_used), - format_bytes(mem_total) - ), - swap_detail: format!( - "{}/{}", - format_bytes(swap_used), - format_bytes(swap_total) - ), + mem_detail: format!("{}/{}", format_bytes(mem_used), format_bytes(mem_total)), + swap_detail: format!("{}/{}", format_bytes(swap_used), format_bytes(swap_total)), net_rx: format!("{}/s", format_bytes(rx_rate)), net_tx: format!("{}/s", format_bytes(tx_rate)), disks, diff --git a/src/terminal.rs b/src/terminal.rs index e922acb..24948d0 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -5,10 +5,7 @@ use alacritty_terminal::{ grid::{Dimensions, Scroll}, index::{Column, Point, Side}, selection::{Selection, SelectionRange, SelectionType}, - term::{ - cell::Cell, - point_to_viewport, viewport_to_point, Config, Term, TermMode, - }, + term::{Config, Term, TermMode, cell::Cell, point_to_viewport, viewport_to_point}, vte::ansi::{CursorShape, Processor}, }; use gpui::Keystroke; @@ -32,16 +29,46 @@ pub enum BackendCommand { #[derive(Debug, Clone)] pub enum BackendEvent { - Output { tab_id: String, bytes: Vec }, - Status { tab_id: String, text: String }, - Connected { tab_id: String }, - SftpEntries { tab_id: String, path: String, entries: Vec }, - SftpPreview { tab_id: String, preview: PreviewData }, - SftpStatus { tab_id: String, text: String }, - RemoteSystem { tab_id: String, snapshot: SystemSnapshot }, - RemoteSystemUnavailable { tab_id: String, reason: String }, - Closed { tab_id: String, reason: String }, - TerminalTitleChanged { tab_id: String, title: String }, + Output { + tab_id: String, + bytes: Vec, + }, + Status { + tab_id: String, + text: String, + }, + Connected { + tab_id: String, + }, + SftpEntries { + tab_id: String, + path: String, + entries: Vec, + }, + SftpPreview { + tab_id: String, + preview: PreviewData, + }, + SftpStatus { + tab_id: String, + text: String, + }, + RemoteSystem { + tab_id: String, + snapshot: SystemSnapshot, + }, + RemoteSystemUnavailable { + tab_id: String, + reason: String, + }, + Closed { + tab_id: String, + reason: String, + }, + TerminalTitleChanged { + tab_id: String, + title: String, + }, } #[derive(Clone)] @@ -123,16 +150,36 @@ pub struct SftpUiState { } impl TerminalTab { - pub fn new_local(id: String, title: String, backend: BackendTx, events: std::sync::mpsc::Sender) -> Self { - Self::new(id, title, TabKind::Local, "local shell".into(), backend, events) + pub fn new_local( + id: String, + title: String, + backend: BackendTx, + events: std::sync::mpsc::Sender, + ) -> Self { + Self::new( + id, + title, + TabKind::Local, + "local shell".into(), + backend, + events, + ) } - pub fn new_ssh(id: String, session: &Session, backend: BackendTx, events: std::sync::mpsc::Sender) -> Self { + pub fn new_ssh( + id: String, + session: &Session, + backend: BackendTx, + events: std::sync::mpsc::Sender, + ) -> Self { let mut tab = Self::new( id, session.name.clone(), TabKind::Ssh, - format!("connecting {}@{}:{}", session.user, session.host, session.port), + format!( + "connecting {}@{}:{}", + session.user, session.host, session.port + ), backend, events, ); @@ -149,7 +196,14 @@ impl TerminalTab { tab } - fn new(id: String, title: String, kind: TabKind, status: String, backend: BackendTx, events: std::sync::mpsc::Sender) -> Self { + fn new( + id: String, + title: String, + kind: TabKind, + status: String, + backend: BackendTx, + events: std::sync::mpsc::Sender, + ) -> Self { Self { id: id.clone(), title, @@ -264,8 +318,11 @@ impl TerminalTab { self.term.scroll_display(Scroll::Bottom); } + #[allow(dead_code)] pub fn has_selection(&self) -> bool { - self.term.selection_to_string().is_some_and(|text| !text.is_empty()) + self.term + .selection_to_string() + .is_some_and(|text| !text.is_empty()) } pub fn clear_selection(&mut self) { @@ -273,7 +330,9 @@ impl TerminalTab { } pub fn selection_text(&self) -> Option { - self.term.selection_to_string().filter(|text| !text.is_empty()) + self.term + .selection_to_string() + .filter(|text| !text.is_empty()) } pub fn begin_selection( @@ -301,19 +360,25 @@ impl TerminalTab { } pub fn paste_text(&mut self, text: &str) { - let paste_text = text.replace('\x1b', "").replace("\r\n", "\r").replace('\n', "\r"); + let paste_text = text + .replace('\x1b', "") + .replace("\r\n", "\r") + .replace('\n', "\r"); self.backend .send(BackendCommand::Input(paste_text.into_bytes())); } - } fn viewport_selection_from_range( display_offset: usize, selection: &Option, ) -> Option { - let SelectionRange { start, end, is_block } = selection.as_ref().copied()?; + let SelectionRange { + start, + end, + is_block, + } = selection.as_ref().copied()?; let start = point_to_viewport(display_offset, start)?; let end = point_to_viewport(display_offset, end)?; @@ -336,7 +401,9 @@ struct TerminalListener { impl EventListener for TerminalListener { fn send_event(&self, event: Event) { match event { - Event::PtyWrite(output) => self.backend.send(BackendCommand::Input(output.into_bytes())), + Event::PtyWrite(output) => self + .backend + .send(BackendCommand::Input(output.into_bytes())), Event::TextAreaSizeRequest(format) => { let size = alacritty_terminal::event::WindowSize { num_lines: 30, @@ -358,14 +425,24 @@ impl EventListener for TerminalListener { } } -fn new_term(cols: u16, rows: u16, backend: BackendTx, tab_id: String, events: std::sync::mpsc::Sender) -> Term { +fn new_term( + cols: u16, + rows: u16, + backend: BackendTx, + tab_id: String, + events: std::sync::mpsc::Sender, +) -> Term { Term::new( Config { scrolling_history: 2000, ..Config::default() }, &TerminalSize::new(cols, rows), - TerminalListener { tab_id, backend, events }, + TerminalListener { + tab_id, + backend, + events, + }, ) } @@ -397,7 +474,11 @@ impl Dimensions for TerminalSize { } } -pub fn encode_key(keystroke: &Keystroke, app_cursor_mode: bool, option_as_meta: bool) -> Option> { +pub fn encode_key( + keystroke: &Keystroke, + app_cursor_mode: bool, + option_as_meta: bool, +) -> Option> { zed_like_to_esc_str(keystroke, app_cursor_mode, option_as_meta) .map(|text| text.into_owned().into_bytes()) } diff --git a/src/terminal_element.rs b/src/terminal_element.rs index 66e4a7d..dfd2ea5 100644 --- a/src/terminal_element.rs +++ b/src/terminal_element.rs @@ -3,9 +3,10 @@ use alacritty_terminal::{ vte::ansi::{Color as AnsiColor, CursorShape, NamedColor}, }; use gpui::{ - App, Bounds, Element, ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, Hsla, InputHandler, IntoElement, - LayoutId, Pixels, Point, Rgba, StrikethroughStyle, TextRun, TextStyle, UTF16Selection, UnderlineStyle, Window, fill, point, px, - relative, rgb, + App, Bounds, Element, ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight, + GlobalElementId, Hsla, InputHandler, IntoElement, LayoutId, Pixels, Point, Rgba, + StrikethroughStyle, TextRun, TextStyle, UTF16Selection, UnderlineStyle, Window, fill, point, + px, relative, rgb, }; use gpui_component::ActiveTheme as _; @@ -81,7 +82,13 @@ impl BatchedTextRun { } } - fn paint(&self, origin: Point, metrics: TerminalMetrics, window: &mut Window, cx: &mut App) { + fn paint( + &self, + origin: Point, + metrics: TerminalMetrics, + window: &mut Window, + cx: &mut App, + ) { let pos = point( origin.x + metrics.cell_width * self.col as f32, origin.y + metrics.line_height * self.row as f32, @@ -146,10 +153,13 @@ impl InputHandler for TerminalInputHandler { _window: &mut Window, cx: &mut App, ) -> Option { - self.view.read(cx).terminal_accepts_text_input().then_some(UTF16Selection { - range: 0..0, - reversed: false, - }) + self.view + .read(cx) + .terminal_accepts_text_input() + .then_some(UTF16Selection { + range: 0..0, + reversed: false, + }) } fn marked_text_range( @@ -277,15 +287,21 @@ impl TerminalElement { fg.a *= 0.7; } - let underline = cell.flags.intersects(Flags::ALL_UNDERLINES).then(|| UnderlineStyle { - color: Some(fg), - thickness: px(1.0), - wavy: cell.flags.contains(Flags::UNDERCURL), - }); - let strikethrough = cell.flags.contains(Flags::STRIKEOUT).then(|| StrikethroughStyle { - color: Some(fg), - thickness: px(1.0), - }); + let underline = cell + .flags + .intersects(Flags::ALL_UNDERLINES) + .then(|| UnderlineStyle { + color: Some(fg), + thickness: px(1.0), + wavy: cell.flags.contains(Flags::UNDERCURL), + }); + let strikethrough = cell + .flags + .contains(Flags::STRIKEOUT) + .then(|| StrikethroughStyle { + color: Some(fg), + thickness: px(1.0), + }); let weight = if cell.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { FontWeight::BOLD @@ -326,10 +342,9 @@ impl TerminalElement { continue; } - let selected = self - .snapshot - .selection - .is_some_and(|selection| selection_contains(selection, render_cell.row, render_cell.col)); + let selected = self.snapshot.selection.is_some_and(|selection| { + selection_contains(selection, render_cell.row, render_cell.col) + }); let bg = color_to_hsla(cell.bg, false, cx); if selected || !is_default_bg(cell.bg) { rects.push(LayoutRect { @@ -525,7 +540,11 @@ impl Element for TerminalElement { } if let Some(cursor) = prepaint.cursor { - if self.marked_text.as_ref().is_some_and(|text| !text.is_empty()) { + if self + .marked_text + .as_ref() + .is_some_and(|text| !text.is_empty()) + { return; } let x = prepaint.bounds.origin.x + prepaint.metrics.cell_width * cursor.col as f32; @@ -534,7 +553,10 @@ impl Element for TerminalElement { CursorShape::Hidden => {} CursorShape::Beam => { window.paint_quad(fill( - Bounds::new(point(x, y), gpui::size(px(2.), prepaint.metrics.line_height)), + Bounds::new( + point(x, y), + gpui::size(px(2.), prepaint.metrics.line_height), + ), cursor.color, )); } @@ -572,7 +594,10 @@ fn merge_rects(mut rects: Vec) -> Vec { for rect in rects { if let Some(last) = merged.last_mut() { - if last.row == rect.row && last.color == rect.color && last.col + last.cells as i32 == rect.col { + if last.row == rect.row + && last.color == rect.color + && last.col + last.cells as i32 == rect.col + { last.cells += rect.cells; continue; } @@ -601,7 +626,11 @@ fn selection_contains(selection: ViewportSelection, row: i32, col: i32) -> bool } fn is_blank(cell: &alacritty_terminal::term::cell::Cell) -> bool { - cell.c == ' ' && cell.zerowidth().is_none() && !cell.flags.intersects(Flags::ALL_UNDERLINES | Flags::STRIKEOUT) + cell.c == ' ' + && cell.zerowidth().is_none() + && !cell + .flags + .intersects(Flags::ALL_UNDERLINES | Flags::STRIKEOUT) } fn is_default_bg(color: AnsiColor) -> bool { @@ -623,8 +652,8 @@ fn color_to_hsla(color: AnsiColor, foreground: bool, cx: &App) -> Hsla { fn ansi_index_color(index: u8, _cx: &App) -> Hsla { const ANSI_16: [u32; 16] = [ - 0x1f2430, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xf1f1f0, - 0x686868, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xffffff, + 0x1f2430, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xf1f1f0, 0x686868, + 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xffffff, ]; if (index as usize) < ANSI_16.len() {