mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58fd03386a | ||
|
|
890ce85cfc | ||
|
|
d5fb956161 | ||
|
|
4bad75e522 | ||
|
|
9e28e78319 | ||
|
|
d561232d19 | ||
|
|
107fe8942c | ||
|
|
06796031e6 | ||
|
|
0cfd3a2fc2 | ||
|
|
6dfa199dcb |
@@ -151,10 +151,20 @@ jobs:
|
||||
name: ${{ matrix.name }}
|
||||
path: ${{ env.ASSET }}
|
||||
|
||||
publish:
|
||||
name: Publish Release
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download all workflow artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: Attach to GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ${{ env.ASSET }}
|
||||
files: dist/**/*
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
Generated
+1
@@ -292,6 +292,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"directories",
|
||||
"flate2",
|
||||
"futures",
|
||||
"gpui",
|
||||
"gpui-component",
|
||||
"gpui-component-assets",
|
||||
|
||||
@@ -39,6 +39,7 @@ uuid = { version = "1", features = ["v4", "serde"] }
|
||||
walkdir = "2"
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
image = { version = "0.25.9", default-features = false, features = ["png"] }
|
||||
futures = "0.3.32"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
|
||||
This project focuses on providing a high-performance and visually appealing shell workspace by combining local and remote environments with a rich set of built-in features.
|
||||
|
||||
## 🚀 v0.3 Massive Update!
|
||||
|
||||
The brand-new v0.3 introduces comprehensive upgrades to both architecture and user experience:
|
||||
- ✨ **Global Real-Time Font Sizing** (Thanks to [@ancion](https://github.com/ancion) for [PR #6](https://github.com/rust-kotlin/ashell/pull/6)): Customize your global UI fonts and terminal rendering fonts independently. Adjust text size with real-time scaling and automatic persistence.
|
||||
- ✨ **Concurrent SFTP Transfers**: A dedicated Transfers dialog now supports highly optimized, concurrent file uploads and downloads with precise progress, speed, and ETA calculations.
|
||||
- ✨ **Persistent Workspace State**: Your sidebar width adjustments, terminal themes, font settings, and file visibility preferences are completely restored upon your next launch!
|
||||
- ✨ **Intelligent SSH Disconnect/Reconnect**: Greatly enhanced SSH connection resilience. Provides a beautiful and localized overlay for one-click re-connection on unexpected disconnections.
|
||||
- ✨ **Hot-Swappable i18n System**: Deeply optimized internationalization. Switch instantly between English and Simplified Chinese without restarting the application.
|
||||
- ✨ **Smart Right-Click Actions**: Added intelligent right-click copy and paste for the terminal, featuring a dedicated settings toggle and OS-native keyboard shortcut hints.
|
||||
|
||||
## Download
|
||||
|
||||
You can download the latest pre-compiled releases for macOS, Windows, and Linux from the [GitHub Releases page](https://github.com/rust-kotlin/ashell/releases/latest).
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
|
||||
该项目旨在通过结合本地和远程环境,并内置丰富的核心功能,提供一个高性能且美观的 Shell 工作区。
|
||||
|
||||
## 🚀 v0.3 版本重磅更新!
|
||||
|
||||
全新发布的 v0.3 迎来了架构和体验的全面升级:
|
||||
- ✨ **全局实时字体设置与无级缩放**(感谢 [@ancion](https://github.com/ancion) 提供的 [PR #6](https://github.com/rust-kotlin/ashell/pull/6)):支持自定义全局 UI 字体和终端渲染字体,支持实时字号调节缩放并自动记忆。
|
||||
- ✨ **SFTP 多任务并发传输队列**:引入全新的独立传输对话框,支持多文件并发上传下载,提供精准的进度条、速率和剩余时间显示。
|
||||
- ✨ **持久化工作区记忆**:您对侧边栏的尺寸拖拽、终端主题、字体、隐藏文件开关等一切设置都会在下次启动时原样恢复!
|
||||
- ✨ **智能断线重连与状态感知**:全面增强的 SSH 网络异常处理机制,断线后直接提供美观的覆盖层用于一键重连或关闭。
|
||||
- ✨ **多语言热切换增强**:深度优化的 i18n 系统,包含中文与英文在内的所有界面文案现在均支持热切换。
|
||||
- ✨ **智能右键快捷操作**:新增终端右键智能复制与粘贴功能,提供专属设置项自由控制开关,并支持根据不同操作系统自适应显示原生快捷键提示。
|
||||
|
||||
## 下载
|
||||
|
||||
您可以从 [GitHub Releases 页面](https://github.com/rust-kotlin/ashell/releases/latest) 下载 macOS、Windows 和 Linux 版本的最新预编译程序。
|
||||
|
||||
@@ -51,7 +51,11 @@ add_ssh: "+ ssh"
|
||||
open_local_or_ssh: "Open a local terminal or pick an SSH session."
|
||||
open_session: "Open Session"
|
||||
settings: "Settings"
|
||||
ui_font_size: "UI Font Size"
|
||||
terminal_font_size: "Terminal Font Size"
|
||||
system_default: "System Default"
|
||||
ui_font_family: "UI Font"
|
||||
terminal_font_family: "Terminal Font"
|
||||
theme_management_hint: "Theme mode and light/dark theme families are managed from the top-bar theme button."
|
||||
local_host: "local host"
|
||||
local_terminal_opened: "local terminal opened"
|
||||
@@ -71,6 +75,23 @@ uploaded_folder: "Uploaded folder"
|
||||
uploaded_n_files: "Uploaded %{files} files"
|
||||
uploaded_n_folders: "Uploaded %{folders} folders"
|
||||
uploaded_files_and_folders: "Uploaded %{files} files and %{folders} folders"
|
||||
n_files: "%{files} files"
|
||||
n_folders: "%{folders} folders"
|
||||
n_files_and_folders: "%{files} files and %{folders} folders"
|
||||
reset: "Reset"
|
||||
reset_layout: "Reset Default Layout"
|
||||
reset_layout_success: "Layout reset. Window size takes effect on restart."
|
||||
|
||||
transfers: "Transfers"
|
||||
no_transfers_yet: "No transfers yet"
|
||||
downloading: "Downloading"
|
||||
paused: "Paused"
|
||||
completed: "Completed"
|
||||
failed: "Failed"
|
||||
cancelled: "Cancelled"
|
||||
session: "Session"
|
||||
|
||||
software_builtin: "Built-in"
|
||||
retry: "Retry"
|
||||
right_click_copy_paste: "Right-click Copy/Paste"
|
||||
copy_paste_hint: "Tip: Regardless of this switch, you can always use %{key} + C/V to copy/paste."
|
||||
@@ -51,7 +51,11 @@ add_ssh: "+ SSH"
|
||||
open_local_or_ssh: "打开本地终端或选择一个 SSH 会话。"
|
||||
open_session: "打开会话"
|
||||
settings: "设置"
|
||||
ui_font_size: "界面字体大小"
|
||||
terminal_font_size: "终端字体大小"
|
||||
system_default: "系统默认"
|
||||
ui_font_family: "界面字体"
|
||||
terminal_font_family: "终端字体"
|
||||
theme_management_hint: "主题模式和亮/暗主题系列可通过顶栏的主题按钮管理。"
|
||||
local_host: "本地主机"
|
||||
local_terminal_opened: "已打开本地终端"
|
||||
@@ -71,6 +75,23 @@ uploaded_folder: "已上传文件夹"
|
||||
uploaded_n_files: "已上传 %{files} 个文件"
|
||||
uploaded_n_folders: "已上传 %{folders} 个文件夹"
|
||||
uploaded_files_and_folders: "已上传 %{files} 个文件及 %{folders} 个文件夹"
|
||||
n_files: "%{files} 个文件"
|
||||
n_folders: "%{folders} 个文件夹"
|
||||
n_files_and_folders: "%{files} 个文件及 %{folders} 个文件夹"
|
||||
reset: "重置"
|
||||
reset_layout: "恢复默认布局"
|
||||
reset_layout_success: "布局占比已恢复默认。窗口大小将在下次启动时生效。"
|
||||
|
||||
transfers: "传输记录"
|
||||
no_transfers_yet: "暂无传输记录"
|
||||
downloading: "正在下载"
|
||||
paused: "已暂停"
|
||||
completed: "已完成"
|
||||
failed: "失败"
|
||||
cancelled: "已取消"
|
||||
session: "会话"
|
||||
|
||||
software_builtin: "软件内置"
|
||||
retry: "重试"
|
||||
right_click_copy_paste: "右键复制/粘贴"
|
||||
copy_paste_hint: "提示:不管是否开启此开关,都可以使用 %{key} + C/V 快捷键进行复制和粘贴。"
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 279 KiB |
@@ -107,6 +107,14 @@ pub struct ConfigFile {
|
||||
pub locale: String,
|
||||
#[serde(default = "default_terminal_font_size")]
|
||||
pub terminal_font_size: f32,
|
||||
#[serde(default = "default_ui_font_size")]
|
||||
pub ui_font_size: f32,
|
||||
#[serde(default)]
|
||||
pub right_click_copy_paste: bool,
|
||||
#[serde(default = "default_ui_font_family")]
|
||||
pub ui_font_family: String,
|
||||
#[serde(default = "default_terminal_font_family")]
|
||||
pub terminal_font_family: String,
|
||||
#[serde(default)]
|
||||
pub sessions: Vec<Session>,
|
||||
#[serde(default)]
|
||||
@@ -115,6 +123,8 @@ pub struct ConfigFile {
|
||||
pub workspace_panels: Option<Vec<f32>>,
|
||||
#[serde(default)]
|
||||
pub body_panels: Option<Vec<f32>>,
|
||||
#[serde(default)]
|
||||
pub transfers: Vec<crate::terminal::Transfer>,
|
||||
}
|
||||
|
||||
fn default_locale() -> String {
|
||||
@@ -125,6 +135,20 @@ fn default_terminal_font_size() -> f32 {
|
||||
13.0
|
||||
}
|
||||
|
||||
fn default_ui_font_size() -> f32 {
|
||||
12.0
|
||||
}
|
||||
|
||||
pub fn default_ui_font_family() -> String {
|
||||
// ".SystemUIFont" is a GPUI sentinel that resolves to the platform system UI font.
|
||||
// This matches gpui-component's own Theme default.
|
||||
".SystemUIFont".to_string()
|
||||
}
|
||||
|
||||
fn default_terminal_font_family() -> String {
|
||||
"Maple Mono NF CN".to_string()
|
||||
}
|
||||
|
||||
pub struct ConfigStore {
|
||||
path: PathBuf,
|
||||
cache: ConfigFile,
|
||||
@@ -230,6 +254,17 @@ impl ConfigStore {
|
||||
self.cache.body_panels.as_ref()
|
||||
}
|
||||
|
||||
pub fn transfers(&self) -> Vec<crate::terminal::Transfer> {
|
||||
self.cache.transfers.clone()
|
||||
}
|
||||
|
||||
pub fn set_transfers(&mut self, transfers: Vec<crate::terminal::Transfer>) {
|
||||
self.cache.transfers = transfers;
|
||||
if let Err(err) = self.save() {
|
||||
tracing::error!("failed to save config: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_layout_state(
|
||||
&mut self,
|
||||
window_bounds: Option<SavedWindowBounds>,
|
||||
@@ -245,6 +280,50 @@ impl ConfigStore {
|
||||
self.cache.terminal_font_size = terminal_font_size.max(10.0);
|
||||
}
|
||||
|
||||
pub fn ui_font_size(&self) -> f32 {
|
||||
if self.cache.ui_font_size <= 0.0 {
|
||||
default_ui_font_size()
|
||||
} else {
|
||||
self.cache.ui_font_size
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ui_font_size(&mut self, ui_font_size: f32) {
|
||||
self.cache.ui_font_size = ui_font_size.max(8.0);
|
||||
}
|
||||
|
||||
pub fn ui_font_family(&self) -> &str {
|
||||
if self.cache.ui_font_family.is_empty() {
|
||||
".SystemUIFont"
|
||||
} else {
|
||||
&self.cache.ui_font_family
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ui_font_family(&mut self, family: &str) {
|
||||
self.cache.ui_font_family = family.to_string();
|
||||
}
|
||||
|
||||
pub fn right_click_copy_paste(&self) -> bool {
|
||||
self.cache.right_click_copy_paste
|
||||
}
|
||||
|
||||
pub fn set_right_click_copy_paste(&mut self, val: bool) {
|
||||
self.cache.right_click_copy_paste = val;
|
||||
}
|
||||
|
||||
pub fn terminal_font_family(&self) -> &str {
|
||||
if self.cache.terminal_font_family.is_empty() {
|
||||
"Maple Mono NF CN"
|
||||
} else {
|
||||
&self.cache.terminal_font_family
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_terminal_font_family(&mut self, family: &str) {
|
||||
self.cache.terminal_font_family = family.to_string();
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str) -> Option<&Session> {
|
||||
self.cache.sessions.iter().find(|s| s.id == id)
|
||||
}
|
||||
|
||||
+804
-155
File diff suppressed because it is too large
Load Diff
+393
-116
@@ -62,9 +62,68 @@ pub enum SftpCommand {
|
||||
locals: Vec<String>,
|
||||
remote_dir: String,
|
||||
},
|
||||
PauseTransfer(String),
|
||||
ResumeTransfer(String),
|
||||
CancelTransfer(String),
|
||||
TransferFinished(String),
|
||||
Close,
|
||||
}
|
||||
|
||||
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
|
||||
|
||||
pub struct TransferStateFlag(pub Arc<AtomicU8>);
|
||||
|
||||
impl TransferStateFlag {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(AtomicU8::new(0)))
|
||||
}
|
||||
|
||||
pub fn pause(&self) { self.0.store(1, Ordering::SeqCst); }
|
||||
pub fn resume(&self) { self.0.store(0, Ordering::SeqCst); }
|
||||
pub fn cancel(&self) { self.0.store(2, Ordering::SeqCst); }
|
||||
|
||||
pub async fn yield_if_paused(
|
||||
&self,
|
||||
events: &std::sync::mpsc::Sender<crate::terminal::BackendEvent>,
|
||||
tab_id: &str,
|
||||
id: &str,
|
||||
transferred: u64,
|
||||
total: Option<u64>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut was_paused = false;
|
||||
loop {
|
||||
let state = self.0.load(Ordering::SeqCst);
|
||||
if state == 2 {
|
||||
return Err(anyhow::anyhow!("transfer cancelled"));
|
||||
}
|
||||
if state == 1 {
|
||||
if !was_paused {
|
||||
let _ = events.send(crate::terminal::BackendEvent::TransferProgress {
|
||||
tab_id: tab_id.to_string(),
|
||||
id: id.to_string(),
|
||||
transferred,
|
||||
total,
|
||||
state: crate::terminal::TransferState::Paused,
|
||||
});
|
||||
was_paused = true;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
} else {
|
||||
if was_paused {
|
||||
let _ = events.send(crate::terminal::BackendEvent::TransferProgress {
|
||||
tab_id: tab_id.to_string(),
|
||||
id: id.to_string(),
|
||||
transferred,
|
||||
total,
|
||||
state: crate::terminal::TransferState::Running,
|
||||
});
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SftpHandle {
|
||||
pub commands: UnboundedSender<SftpCommand>,
|
||||
#[allow(dead_code)]
|
||||
@@ -105,6 +164,18 @@ impl SftpHandle {
|
||||
pub fn close(&self) {
|
||||
let _ = self.commands.send(SftpCommand::Close);
|
||||
}
|
||||
|
||||
pub fn pause_transfer(&self, id: String) {
|
||||
let _ = self.commands.send(SftpCommand::PauseTransfer(id));
|
||||
}
|
||||
|
||||
pub fn resume_transfer(&self, id: String) {
|
||||
let _ = self.commands.send(SftpCommand::ResumeTransfer(id));
|
||||
}
|
||||
|
||||
pub fn cancel_transfer(&self, id: String) {
|
||||
let _ = self.commands.send(SftpCommand::CancelTransfer(id));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_sftp(
|
||||
@@ -114,8 +185,9 @@ pub fn spawn_sftp(
|
||||
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||
) -> SftpHandle {
|
||||
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
||||
let cmd_tx_clone = cmd_tx.clone();
|
||||
let join = runtime.spawn(async move {
|
||||
if let Err(err) = run_sftp(tab_id.clone(), session, cmd_rx, events.clone()).await {
|
||||
if let Err(err) = run_sftp(tab_id.clone(), session, cmd_rx, cmd_tx_clone, events.clone()).await {
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
tab_id,
|
||||
text: format!("sftp error: {err:#}"),
|
||||
@@ -132,11 +204,12 @@ async fn run_sftp(
|
||||
tab_id: String,
|
||||
session: Session,
|
||||
mut commands: UnboundedReceiver<SftpCommand>,
|
||||
commands_tx: UnboundedSender<SftpCommand>,
|
||||
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||
) -> Result<()> {
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id.clone(),
|
||||
text: "sftp connecting...".into(),
|
||||
text: t!("sftp_connecting").to_string(),
|
||||
});
|
||||
|
||||
let handle = connect_and_authenticate(&session).await?;
|
||||
@@ -158,9 +231,29 @@ async fn run_sftp(
|
||||
.unwrap_or_else(|_| "/".to_string());
|
||||
emit_entries(&events, &tab_id, &sftp, &home).await?;
|
||||
|
||||
let mut active_transfers: std::collections::HashMap<String, TransferStateFlag> = std::collections::HashMap::new();
|
||||
|
||||
while let Some(command) = commands.recv().await {
|
||||
match command {
|
||||
SftpCommand::Close => break,
|
||||
SftpCommand::PauseTransfer(id) => {
|
||||
if let Some(flag) = active_transfers.get(&id) {
|
||||
flag.pause();
|
||||
}
|
||||
}
|
||||
SftpCommand::ResumeTransfer(id) => {
|
||||
if let Some(flag) = active_transfers.get(&id) {
|
||||
flag.resume();
|
||||
}
|
||||
}
|
||||
SftpCommand::CancelTransfer(id) => {
|
||||
if let Some(flag) = active_transfers.remove(&id) {
|
||||
flag.cancel();
|
||||
}
|
||||
}
|
||||
SftpCommand::TransferFinished(id) => {
|
||||
active_transfers.remove(&id);
|
||||
}
|
||||
SftpCommand::ListDir(path) => {
|
||||
let actual_path = if path == "~" {
|
||||
home.clone()
|
||||
@@ -192,46 +285,142 @@ async fn run_sftp(
|
||||
}
|
||||
},
|
||||
SftpCommand::Download { remote, local_dir } => {
|
||||
let base = base_name(&remote);
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let flag = TransferStateFlag::new();
|
||||
active_transfers.insert(id.clone(), TransferStateFlag(flag.0.clone()));
|
||||
|
||||
let info = crate::terminal::TransferInfo {
|
||||
id: id.clone(),
|
||||
name: base_name(&remote).to_string(),
|
||||
source: remote.clone(),
|
||||
target: local_dir.clone(),
|
||||
kind: crate::terminal::TransferType::Download,
|
||||
total_bytes: None,
|
||||
};
|
||||
let _ = events.send(BackendEvent::TransferStarted {
|
||||
tab_id: tab_id.clone(),
|
||||
text: t!("downloading_file", base = base).into(),
|
||||
info,
|
||||
});
|
||||
match download_path_impl(&handle, &sftp, &remote, Path::new(&local_dir)).await {
|
||||
Ok(summary) => {
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id.clone(),
|
||||
text: summary.into(),
|
||||
});
|
||||
|
||||
let handle_clone = handle.clone();
|
||||
let events_clone = events.clone();
|
||||
let tab_id_clone = tab_id.clone();
|
||||
let commands_tx_clone = commands_tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let Ok(channel) = handle_clone.channel_open_session().await else { return };
|
||||
let Ok(_) = channel.request_subsystem(true, "sftp").await else { return };
|
||||
let Ok(sftp_session) = SftpSession::new(channel.into_stream()).await else { return };
|
||||
|
||||
let _ = events_clone.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id_clone.clone(),
|
||||
text: t!("downloading_file", base = base_name(&remote)).to_string(),
|
||||
});
|
||||
|
||||
match download_path_impl(&handle_clone, &sftp_session, &remote, Path::new(&local_dir), flag, &events_clone, &tab_id_clone, &id).await {
|
||||
Ok(summary) => {
|
||||
let _ = events_clone.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id_clone,
|
||||
text: summary,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
let err_msg = format!("{err:#}");
|
||||
let _ = events_clone.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id_clone.clone(),
|
||||
text: t!("download_failed", err = err_msg.clone()).to_string(),
|
||||
});
|
||||
let _ = events_clone.send(BackendEvent::TransferProgress {
|
||||
tab_id: tab_id_clone,
|
||||
id: id.clone(),
|
||||
transferred: 0,
|
||||
total: None,
|
||||
state: crate::terminal::TransferState::Failed(err_msg),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id.clone(),
|
||||
text: t!("download_failed", err = format!("{err:#}")).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let _ = commands_tx_clone.send(SftpCommand::TransferFinished(id));
|
||||
});
|
||||
}
|
||||
SftpCommand::UploadPaths { locals, remote_dir } => {
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let flag = TransferStateFlag::new();
|
||||
active_transfers.insert(id.clone(), TransferStateFlag(flag.0.clone()));
|
||||
|
||||
let name = if locals.len() == 1 {
|
||||
base_name(&locals[0]).to_string()
|
||||
} else {
|
||||
let mut file_count = 0;
|
||||
let mut folder_count = 0;
|
||||
for local in &locals {
|
||||
if std::path::Path::new(local).is_dir() {
|
||||
folder_count += 1;
|
||||
} else {
|
||||
file_count += 1;
|
||||
}
|
||||
}
|
||||
if file_count > 0 && folder_count == 0 {
|
||||
t!("n_files", files = file_count).to_string()
|
||||
} else if file_count == 0 && folder_count > 0 {
|
||||
t!("n_folders", folders = folder_count).to_string()
|
||||
} else {
|
||||
t!("n_files_and_folders", files = file_count, folders = folder_count).to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let info = crate::terminal::TransferInfo {
|
||||
id: id.clone(),
|
||||
name,
|
||||
source: "local".to_string(),
|
||||
target: remote_dir.clone(),
|
||||
kind: crate::terminal::TransferType::Upload,
|
||||
total_bytes: None,
|
||||
};
|
||||
let _ = events.send(BackendEvent::TransferStarted {
|
||||
tab_id: tab_id.clone(),
|
||||
text: t!("uploading").into(),
|
||||
info,
|
||||
});
|
||||
match upload_paths_impl(&sftp, &locals, &remote_dir).await {
|
||||
Ok(summary) => {
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id.clone(),
|
||||
text: summary.into(),
|
||||
});
|
||||
let _ = emit_entries(&events, &tab_id, &sftp, &remote_dir).await;
|
||||
|
||||
let handle_clone = handle.clone();
|
||||
let events_clone = events.clone();
|
||||
let tab_id_clone = tab_id.clone();
|
||||
let commands_tx_clone = commands_tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let Ok(channel) = handle_clone.channel_open_session().await else { return };
|
||||
let Ok(_) = channel.request_subsystem(true, "sftp").await else { return };
|
||||
let Ok(sftp_session) = SftpSession::new(channel.into_stream()).await else { return };
|
||||
|
||||
let _ = events_clone.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id_clone.clone(),
|
||||
text: t!("uploading").to_string(),
|
||||
});
|
||||
|
||||
match upload_paths_impl(&sftp_session, &locals, &remote_dir, flag, &events_clone, &tab_id_clone, &id).await {
|
||||
Ok(summary) => {
|
||||
let _ = events_clone.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id_clone.clone(),
|
||||
text: summary,
|
||||
});
|
||||
let _ = commands_tx_clone.send(SftpCommand::ListDir(remote_dir));
|
||||
}
|
||||
Err(err) => {
|
||||
let err_msg = format!("{err:#}");
|
||||
let _ = events_clone.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id_clone.clone(),
|
||||
text: t!("upload_failed", err = err_msg.clone()).to_string(),
|
||||
});
|
||||
let _ = events_clone.send(BackendEvent::TransferProgress {
|
||||
tab_id: tab_id_clone,
|
||||
id: id.clone(),
|
||||
transferred: 0,
|
||||
total: None,
|
||||
state: crate::terminal::TransferState::Failed(err_msg),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id.clone(),
|
||||
text: t!("upload_failed", err = format!("{err:#}")).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let _ = commands_tx_clone.send(SftpCommand::TransferFinished(id));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,7 +452,7 @@ async fn emit_entries(
|
||||
|
||||
async fn connect_and_authenticate(
|
||||
session: &Session,
|
||||
) -> Result<russh::client::Handle<SftpClientHandler>> {
|
||||
) -> Result<Arc<russh::client::Handle<SftpClientHandler>>> {
|
||||
let config = Arc::new(client::Config {
|
||||
inactivity_timeout: Some(std::time::Duration::from_secs(600)),
|
||||
..Default::default()
|
||||
@@ -304,7 +493,7 @@ async fn connect_and_authenticate(
|
||||
));
|
||||
}
|
||||
|
||||
Ok(handle)
|
||||
Ok(Arc::new(handle))
|
||||
}
|
||||
|
||||
fn load_session_private_key(session: &Session) -> Result<PrivateKey> {
|
||||
@@ -523,6 +712,10 @@ async fn download_path_impl(
|
||||
sftp: &SftpSession,
|
||||
remote: &str,
|
||||
local_dir: &Path,
|
||||
flag: TransferStateFlag,
|
||||
events: &std::sync::mpsc::Sender<BackendEvent>,
|
||||
tab_id: &str,
|
||||
id: &str,
|
||||
) -> Result<String> {
|
||||
tokio::fs::create_dir_all(local_dir)
|
||||
.await
|
||||
@@ -544,12 +737,12 @@ async fn download_path_impl(
|
||||
Uuid::new_v4()
|
||||
));
|
||||
let extracted_to =
|
||||
download_remote_directory_archive(handle, sftp, remote, &local_archive).await?;
|
||||
download_remote_directory_archive(handle, sftp, remote, &local_archive, &flag, events, tab_id, id).await?;
|
||||
return Ok(t!("downloaded_folder", path = extracted_to.display()).to_string());
|
||||
}
|
||||
|
||||
let local_path = local_dir.join(base_name(remote));
|
||||
download_file_impl(sftp, remote, &local_path).await?;
|
||||
download_file_impl(sftp, remote, &local_path, &flag, events, tab_id, id).await?;
|
||||
Ok(t!("downloaded_file", path = local_path.display()).to_string())
|
||||
}
|
||||
|
||||
@@ -558,6 +751,10 @@ async fn download_dir_recursive(
|
||||
sftp: &SftpSession,
|
||||
remote_dir: &str,
|
||||
local_dir: &Path,
|
||||
flag: &TransferStateFlag,
|
||||
events: &std::sync::mpsc::Sender<BackendEvent>,
|
||||
tab_id: &str,
|
||||
id: &str,
|
||||
) -> Result<()> {
|
||||
tokio::fs::create_dir_all(local_dir)
|
||||
.await
|
||||
@@ -566,9 +763,9 @@ async fn download_dir_recursive(
|
||||
for entry in entries {
|
||||
let local_path = local_dir.join(&entry.name);
|
||||
if entry.is_dir {
|
||||
Box::pin(download_dir_recursive(sftp, &entry.full_path, &local_path)).await?;
|
||||
Box::pin(download_dir_recursive(sftp, &entry.full_path, &local_path, flag, events, tab_id, id)).await?;
|
||||
} else {
|
||||
download_file_impl(sftp, &entry.full_path, &local_path).await?;
|
||||
download_file_impl(sftp, &entry.full_path, &local_path, flag, events, tab_id, id).await?;
|
||||
let _ = maybe_extract_archive(&local_path).await;
|
||||
}
|
||||
}
|
||||
@@ -580,6 +777,10 @@ async fn download_remote_directory_archive(
|
||||
sftp: &SftpSession,
|
||||
remote_dir: &str,
|
||||
local_archive: &Path,
|
||||
flag: &TransferStateFlag,
|
||||
events: &std::sync::mpsc::Sender<BackendEvent>,
|
||||
tab_id: &str,
|
||||
id: &str,
|
||||
) -> Result<PathBuf> {
|
||||
let remote_archive = format!(
|
||||
"/tmp/ashell-{}-{}.tar.gz",
|
||||
@@ -593,7 +794,7 @@ async fn download_remote_directory_archive(
|
||||
.join(base_name(remote_dir));
|
||||
|
||||
let archive_download = async {
|
||||
download_file_impl(sftp, &remote_archive, local_archive).await?;
|
||||
download_file_impl(sftp, &remote_archive, local_archive, flag, events, tab_id, id).await?;
|
||||
extract_archive_to(
|
||||
local_archive,
|
||||
local_archive.parent().unwrap_or_else(|| Path::new(".")),
|
||||
@@ -616,7 +817,15 @@ async fn download_remote_directory_archive(
|
||||
Ok(extracted_to)
|
||||
}
|
||||
|
||||
async fn download_file_impl(sftp: &SftpSession, remote: &str, local: &Path) -> Result<()> {
|
||||
async fn download_file_impl(
|
||||
sftp: &SftpSession,
|
||||
remote: &str,
|
||||
local: &Path,
|
||||
flag: &TransferStateFlag,
|
||||
events: &std::sync::mpsc::Sender<BackendEvent>,
|
||||
tab_id: &str,
|
||||
id: &str,
|
||||
) -> Result<()> {
|
||||
let mut remote_file = sftp
|
||||
.open(remote)
|
||||
.await
|
||||
@@ -625,8 +834,12 @@ async fn download_file_impl(sftp: &SftpSession, remote: &str, local: &Path) -> R
|
||||
.await
|
||||
.with_context(|| format!("create local {}", local.display()))?;
|
||||
|
||||
let mut buffer = vec![0u8; 64 * 1024];
|
||||
let total = sftp.metadata(remote).await.ok().and_then(|m| m.size);
|
||||
let mut transferred = 0u64;
|
||||
|
||||
let mut buffer = vec![0u8; 128 * 1024];
|
||||
loop {
|
||||
flag.yield_if_paused(events, tab_id, id, transferred, total).await?;
|
||||
let read = remote_file
|
||||
.read(&mut buffer)
|
||||
.await
|
||||
@@ -638,8 +851,26 @@ async fn download_file_impl(sftp: &SftpSession, remote: &str, local: &Path) -> R
|
||||
.write_all(&buffer[..read])
|
||||
.await
|
||||
.with_context(|| format!("write {}", local.display()))?;
|
||||
|
||||
transferred += read as u64;
|
||||
let _ = events.send(BackendEvent::TransferProgress {
|
||||
tab_id: tab_id.to_string(),
|
||||
id: id.to_string(),
|
||||
transferred,
|
||||
total,
|
||||
state: crate::terminal::TransferState::Running,
|
||||
});
|
||||
}
|
||||
local_file.flush().await.context("flush local file")?;
|
||||
|
||||
let _ = events.send(BackendEvent::TransferProgress {
|
||||
tab_id: tab_id.to_string(),
|
||||
id: id.to_string(),
|
||||
transferred,
|
||||
total,
|
||||
state: crate::terminal::TransferState::Completed,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -647,92 +878,126 @@ async fn upload_paths_impl(
|
||||
sftp: &SftpSession,
|
||||
locals: &[String],
|
||||
remote_dir: &str,
|
||||
flag: TransferStateFlag,
|
||||
events: &std::sync::mpsc::Sender<BackendEvent>,
|
||||
tab_id: &str,
|
||||
id: &str,
|
||||
) -> Result<String> {
|
||||
create_remote_dir_all(sftp, remote_dir).await?;
|
||||
let mut file_count = 0usize;
|
||||
let mut folder_count = 0usize;
|
||||
|
||||
let mut total_bytes = 0u64;
|
||||
let mut files_to_upload = Vec::new();
|
||||
let mut dirs_to_create = Vec::new();
|
||||
|
||||
for local in locals {
|
||||
let path = PathBuf::from(local);
|
||||
if path.is_dir() {
|
||||
upload_directory_impl(sftp, &path, remote_dir).await?;
|
||||
let p = PathBuf::from(local);
|
||||
if p.is_dir() {
|
||||
folder_count += 1;
|
||||
} else {
|
||||
upload_file_to_dir_impl(sftp, &path, remote_dir).await?;
|
||||
let root_name = p.file_name().and_then(|n| n.to_str()).unwrap_or("folder");
|
||||
let remote_root = join_remote(remote_dir, root_name);
|
||||
dirs_to_create.push(remote_root.clone());
|
||||
|
||||
for entry in WalkDir::new(&p) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path == p { continue; }
|
||||
|
||||
if let Ok(meta) = tokio::fs::metadata(&path).await {
|
||||
let relative = path.strip_prefix(&p)?;
|
||||
let remote_path = if relative.as_os_str().is_empty() {
|
||||
remote_root.clone()
|
||||
} else {
|
||||
let rel = relative.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::<Vec<_>>().join("/");
|
||||
join_remote(&remote_root, &rel)
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
dirs_to_create.push(remote_path);
|
||||
} else {
|
||||
total_bytes += meta.len();
|
||||
files_to_upload.push((path.to_path_buf(), remote_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Ok(meta) = tokio::fs::metadata(&p).await {
|
||||
total_bytes += meta.len();
|
||||
let file_name = p.file_name().and_then(|n| n.to_str()).unwrap_or("file");
|
||||
files_to_upload.push((p.clone(), join_remote(remote_dir, file_name)));
|
||||
file_count += 1;
|
||||
}
|
||||
}
|
||||
let summary = match (file_count, folder_count) {
|
||||
(1, 0) => t!("uploaded_file").to_string(),
|
||||
(0, 1) => t!("uploaded_folder").to_string(),
|
||||
(files, 0) => t!("uploaded_n_files", files = files).to_string(),
|
||||
(0, folders) => t!("uploaded_n_folders", folders = folders).to_string(),
|
||||
(files, folders) => t!(
|
||||
"uploaded_files_and_folders",
|
||||
files = files,
|
||||
folders = folders
|
||||
)
|
||||
.to_string(),
|
||||
|
||||
// Create directories sequentially first
|
||||
for dir in dirs_to_create {
|
||||
create_remote_dir_all(sftp, &dir).await?;
|
||||
}
|
||||
|
||||
let transferred = Arc::new(AtomicU64::new(0));
|
||||
let mut futures = Vec::new();
|
||||
|
||||
for (local_path, remote_path) in files_to_upload {
|
||||
let flag_clone = TransferStateFlag(Arc::clone(&flag.0));
|
||||
let events_clone = events.clone();
|
||||
let tab_id_clone = tab_id.to_string();
|
||||
let id_clone = id.to_string();
|
||||
let transferred_clone = Arc::clone(&transferred);
|
||||
|
||||
futures.push(async move {
|
||||
upload_file_impl(
|
||||
sftp,
|
||||
&local_path,
|
||||
&remote_path,
|
||||
&flag_clone,
|
||||
&events_clone,
|
||||
&tab_id_clone,
|
||||
&id_clone,
|
||||
transferred_clone,
|
||||
Some(total_bytes),
|
||||
).await
|
||||
});
|
||||
}
|
||||
|
||||
use futures::StreamExt as _;
|
||||
let mut stream = futures::stream::iter(futures).buffer_unordered(4);
|
||||
while let Some(res) = stream.next().await {
|
||||
res?;
|
||||
}
|
||||
|
||||
let _ = events.send(BackendEvent::TransferProgress {
|
||||
tab_id: tab_id.to_string(),
|
||||
id: id.to_string(),
|
||||
transferred: total_bytes,
|
||||
total: Some(total_bytes),
|
||||
state: crate::terminal::TransferState::Completed,
|
||||
});
|
||||
|
||||
let summary = if file_count == 1 && folder_count == 0 {
|
||||
t!("uploaded_file").to_string()
|
||||
} else if file_count == 0 && folder_count == 1 {
|
||||
t!("uploaded_folder").to_string()
|
||||
} else if file_count > 0 && folder_count == 0 {
|
||||
t!("uploaded_n_files", files = file_count).to_string()
|
||||
} else if file_count == 0 && folder_count > 0 {
|
||||
t!("uploaded_n_folders", folders = folder_count).to_string()
|
||||
} else {
|
||||
t!("uploaded_files_and_folders", files = file_count, folders = folder_count).to_string()
|
||||
};
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
async fn upload_directory_impl(
|
||||
sftp: &SftpSession,
|
||||
local_dir: &Path,
|
||||
remote_parent: &str,
|
||||
async fn upload_file_impl(
|
||||
sftp: &SftpSession,
|
||||
local_file: &Path,
|
||||
remote_path: &str,
|
||||
flag: &TransferStateFlag,
|
||||
events: &std::sync::mpsc::Sender<BackendEvent>,
|
||||
tab_id: &str,
|
||||
id: &str,
|
||||
transferred: Arc<AtomicU64>,
|
||||
total: Option<u64>,
|
||||
) -> Result<()> {
|
||||
let root_name = local_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or_else(|| anyhow!("invalid folder name: {}", local_dir.display()))?;
|
||||
let remote_root = join_remote(remote_parent, root_name);
|
||||
create_remote_dir_all(sftp, &remote_root).await?;
|
||||
|
||||
for entry in WalkDir::new(local_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path == local_dir {
|
||||
continue;
|
||||
}
|
||||
let relative = path.strip_prefix(local_dir)?;
|
||||
let remote_path = if relative.as_os_str().is_empty() {
|
||||
remote_root.clone()
|
||||
} else {
|
||||
let rel = relative
|
||||
.components()
|
||||
.map(|component| component.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
join_remote(&remote_root, &rel)
|
||||
};
|
||||
if path.is_dir() {
|
||||
create_remote_dir_all(sftp, &remote_path).await?;
|
||||
} else {
|
||||
if let Some(parent) = Path::new(&remote_path).parent() {
|
||||
let parent_remote = parent.to_string_lossy().replace('\\', "/");
|
||||
create_remote_dir_all(sftp, &parent_remote).await?;
|
||||
}
|
||||
upload_file_impl(sftp, path, &remote_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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())
|
||||
.ok_or_else(|| anyhow!("invalid file name: {}", local_file.display()))?;
|
||||
let remote_path = join_remote(remote_dir, file_name);
|
||||
upload_file_impl(sftp, local_file, &remote_path).await
|
||||
}
|
||||
|
||||
async fn upload_file_impl(sftp: &SftpSession, local_file: &Path, remote_path: &str) -> Result<()> {
|
||||
let mut local = tokio::fs::File::open(local_file)
|
||||
.await
|
||||
.with_context(|| format!("open local {}", local_file.display()))?;
|
||||
@@ -741,8 +1006,10 @@ async fn upload_file_impl(sftp: &SftpSession, local_file: &Path, remote_path: &s
|
||||
.await
|
||||
.with_context(|| format!("create remote {remote_path}"))?;
|
||||
|
||||
let mut buffer = vec![0u8; 64 * 1024];
|
||||
let mut buffer = vec![0u8; 128 * 1024];
|
||||
loop {
|
||||
let cur = transferred.load(Ordering::Relaxed);
|
||||
flag.yield_if_paused(events, tab_id, id, cur, total).await?;
|
||||
let read = local.read(&mut buffer).await.context("read local file")?;
|
||||
if read == 0 {
|
||||
break;
|
||||
@@ -751,6 +1018,15 @@ async fn upload_file_impl(sftp: &SftpSession, local_file: &Path, remote_path: &s
|
||||
.write_all(&buffer[..read])
|
||||
.await
|
||||
.with_context(|| format!("write remote {remote_path}"))?;
|
||||
|
||||
let new_cur = transferred.fetch_add(read as u64, Ordering::Relaxed) + read as u64;
|
||||
let _ = events.send(BackendEvent::TransferProgress {
|
||||
tab_id: tab_id.to_string(),
|
||||
id: id.to_string(),
|
||||
transferred: new_cur,
|
||||
total,
|
||||
state: crate::terminal::TransferState::Running,
|
||||
});
|
||||
}
|
||||
remote.flush().await.context("flush remote file")?;
|
||||
Ok(())
|
||||
@@ -987,6 +1263,7 @@ async fn extract_archive_to(path: &Path, target_dir: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SftpClientHandler;
|
||||
|
||||
#[async_trait]
|
||||
|
||||
+49
-1
@@ -61,6 +61,18 @@ pub enum BackendEvent {
|
||||
tab_id: String,
|
||||
reason: String,
|
||||
},
|
||||
TransferProgress {
|
||||
#[allow(dead_code)]
|
||||
tab_id: String,
|
||||
id: String,
|
||||
transferred: u64,
|
||||
total: Option<u64>,
|
||||
state: TransferState,
|
||||
},
|
||||
TransferStarted {
|
||||
tab_id: String,
|
||||
info: TransferInfo,
|
||||
},
|
||||
Closed {
|
||||
tab_id: String,
|
||||
reason: String,
|
||||
@@ -187,7 +199,7 @@ impl TerminalTab {
|
||||
tab.connected = false;
|
||||
tab.sftp = Some(SftpUiState {
|
||||
current_path: "/".into(),
|
||||
status: "sftp connecting...".into(),
|
||||
status: t!("sftp_connecting").to_string(),
|
||||
entries: Vec::new(),
|
||||
selected_path: None,
|
||||
preview: None,
|
||||
@@ -425,6 +437,7 @@ impl EventListener for TerminalListener {
|
||||
}
|
||||
}
|
||||
|
||||
use rust_i18n::t;
|
||||
fn new_term(
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
@@ -648,3 +661,38 @@ fn modifier_code(keystroke: &Keystroke) -> u32 {
|
||||
}
|
||||
modifier_code + 1
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransferType {
|
||||
Upload,
|
||||
Download,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransferState {
|
||||
Running,
|
||||
Paused,
|
||||
Completed,
|
||||
Failed(String),
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TransferInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub source: String,
|
||||
pub target: String,
|
||||
pub kind: TransferType,
|
||||
pub total_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Transfer {
|
||||
pub tab_id: String,
|
||||
pub tab_title: String,
|
||||
pub info: TransferInfo,
|
||||
pub transferred: u64,
|
||||
pub total: Option<u64>,
|
||||
pub state: TransferState,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use alacritty_terminal::{
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, Element, ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight,
|
||||
GlobalElementId, Hsla, InputHandler, IntoElement, LayoutId, Pixels, Point, Rgba,
|
||||
GlobalElementId, Hsla, InputHandler, IntoElement, LayoutId, Pixels, Point, Rgba, SharedString,
|
||||
StrikethroughStyle, TextRun, TextStyle, UTF16Selection, UnderlineStyle, Window, fill, point,
|
||||
px, relative, rgb,
|
||||
};
|
||||
@@ -127,7 +127,7 @@ pub struct TerminalElement {
|
||||
focus_handle: FocusHandle,
|
||||
snapshot: RenderSnapshot,
|
||||
marked_text: Option<String>,
|
||||
font_family: &'static str,
|
||||
font_family: SharedString,
|
||||
font_size: Pixels,
|
||||
line_height: Pixels,
|
||||
cell_width: Pixels,
|
||||
@@ -250,7 +250,7 @@ impl TerminalElement {
|
||||
focus_handle: FocusHandle,
|
||||
snapshot: RenderSnapshot,
|
||||
marked_text: Option<String>,
|
||||
font_family: &'static str,
|
||||
font_family: SharedString,
|
||||
font_size: Pixels,
|
||||
line_height: Pixels,
|
||||
cell_width: Pixels,
|
||||
@@ -270,7 +270,7 @@ impl TerminalElement {
|
||||
fn base_text_style(&self, cx: &App) -> TextStyle {
|
||||
TextStyle {
|
||||
color: cx.theme().foreground,
|
||||
font_family: self.font_family.into(),
|
||||
font_family: self.font_family.clone(),
|
||||
font_size: self.font_size.into(),
|
||||
line_height: self.line_height.into(),
|
||||
..Default::default()
|
||||
@@ -319,7 +319,7 @@ impl TerminalElement {
|
||||
color: fg,
|
||||
background_color: None,
|
||||
font: Font {
|
||||
family: self.font_family.into(),
|
||||
family: self.font_family.clone(),
|
||||
weight,
|
||||
style,
|
||||
..Font::default()
|
||||
@@ -514,7 +514,7 @@ impl Element for TerminalElement {
|
||||
&[TextRun {
|
||||
len: marked_text.len(),
|
||||
font: Font {
|
||||
family: self.font_family.into(),
|
||||
family: self.font_family.clone(),
|
||||
..Font::default()
|
||||
},
|
||||
color: base_style.color,
|
||||
|
||||
Reference in New Issue
Block a user