mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1186e4c84 | ||
|
|
2595cc4853 | ||
|
|
2ae28df8f8 |
+2
-2
@@ -11,8 +11,8 @@ This project focuses on providing a high-performance and visually appealing shel
|
||||
## 🚀 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.
|
||||
- ✨ **Global Real-Time Font Settings** (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 in real-time with automatic persistence.
|
||||
- ✨ **Concurrent SFTP Transfers**: A dedicated Transfers dialog now supports highly optimized, concurrent file uploads and downloads with precise progress display.
|
||||
- ✨ **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.
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
## 🚀 v0.3 版本重磅更新!
|
||||
|
||||
全新发布的 v0.3 迎来了架构和体验的全面升级:
|
||||
- ✨ **全局实时字体设置与无级缩放**(感谢 [@ancion](https://github.com/ancion) 提供的 [PR #6](https://github.com/rust-kotlin/ashell/pull/6)):支持自定义全局 UI 字体和终端渲染字体,支持实时字号调节缩放并自动记忆。
|
||||
- ✨ **SFTP 多任务并发传输队列**:引入全新的独立传输对话框,支持多文件并发上传下载,提供精准的进度条、速率和剩余时间显示。
|
||||
- ✨ **全局实时字体设置与字号调节**(感谢 [@ancion](https://github.com/ancion) 提供的 [PR #6](https://github.com/rust-kotlin/ashell/pull/6)):支持自定义全局 UI 字体和终端渲染字体,支持实时字号调节并自动记忆。
|
||||
- ✨ **SFTP 多任务并发传输队列**:引入全新的独立传输对话框,支持多文件并发上传下载,提供精准的进度展示。
|
||||
- ✨ **持久化工作区记忆**:您对侧边栏的尺寸拖拽、终端主题、字体、隐藏文件开关等一切设置都会在下次启动时原样恢复!
|
||||
- ✨ **智能断线重连与状态感知**:全面增强的 SSH 网络异常处理机制,断线后直接提供美观的覆盖层用于一键重连或关闭。
|
||||
- ✨ **多语言热切换增强**:深度优化的 i18n 系统,包含中文与英文在内的所有界面文案现在均支持热切换。
|
||||
|
||||
+184
-103
@@ -165,6 +165,9 @@ struct Ashell {
|
||||
body_panels: Entity<ResizableState>,
|
||||
terminal_scrollbar: TerminalScrollbarHandle,
|
||||
remote_files_scroll_handle: UniformListScrollHandle,
|
||||
tabs_scroll_handle: gpui::ScrollHandle,
|
||||
selector_scroll_handle: gpui::ScrollHandle,
|
||||
saved_scroll_handle: gpui::ScrollHandle,
|
||||
connection_progress: Option<ConnectionProgress>,
|
||||
pending_sftp_path_sync: Option<String>,
|
||||
sftp_context_menu: Option<SftpContextMenuState>,
|
||||
@@ -181,6 +184,7 @@ struct Ashell {
|
||||
system: SystemSnapshot,
|
||||
last_system_sample: Instant,
|
||||
last_theme_sync: Instant,
|
||||
last_window_appearance: Option<gpui::WindowAppearance>,
|
||||
remote_sample_in_flight: bool,
|
||||
runtime: Runtime,
|
||||
events_rx: mpsc::Receiver<BackendEvent>,
|
||||
@@ -320,6 +324,9 @@ impl Ashell {
|
||||
body_panels,
|
||||
terminal_scrollbar: TerminalScrollbarHandle::default(),
|
||||
remote_files_scroll_handle: UniformListScrollHandle::new(),
|
||||
tabs_scroll_handle: gpui::ScrollHandle::new(),
|
||||
selector_scroll_handle: gpui::ScrollHandle::new(),
|
||||
saved_scroll_handle: gpui::ScrollHandle::new(),
|
||||
connection_progress: None,
|
||||
pending_sftp_path_sync: Some("/".into()),
|
||||
sftp_context_menu: None,
|
||||
@@ -336,6 +343,7 @@ impl Ashell {
|
||||
system,
|
||||
last_system_sample: Instant::now(),
|
||||
last_theme_sync: Instant::now(),
|
||||
last_window_appearance: None,
|
||||
remote_sample_in_flight: false,
|
||||
runtime: Runtime::new().expect("create tokio runtime"),
|
||||
events_rx,
|
||||
@@ -579,7 +587,11 @@ impl Ashell {
|
||||
|
||||
fn sync_theme_if_due(&mut self, cx: &mut Context<Self>) {
|
||||
if self.follow_system_theme && self.last_theme_sync.elapsed() >= Duration::from_secs(1) {
|
||||
Theme::sync_system_appearance(None, cx);
|
||||
let appearance = cx.window_appearance();
|
||||
if self.last_window_appearance != Some(appearance) {
|
||||
Theme::sync_system_appearance(None, cx);
|
||||
self.last_window_appearance = Some(appearance);
|
||||
}
|
||||
self.last_theme_sync = Instant::now();
|
||||
}
|
||||
}
|
||||
@@ -627,6 +639,7 @@ impl Ashell {
|
||||
tab.resize(DEFAULT_COLS, DEFAULT_ROWS);
|
||||
self.tabs.push(tab);
|
||||
self.active_tab = Some(id);
|
||||
self.tabs_scroll_handle.scroll_to_item(self.tabs.len() - 1);
|
||||
self.status = "local terminal opened".into();
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -948,7 +961,7 @@ impl Ashell {
|
||||
view.update(cx, |this, cx| {
|
||||
this.activate_selector_selection(window, cx);
|
||||
});
|
||||
true
|
||||
false
|
||||
}
|
||||
})
|
||||
.content({
|
||||
@@ -958,6 +971,7 @@ impl Ashell {
|
||||
let selector_focus_handle = selector_focus_handle.clone();
|
||||
move |content, window, _cx| {
|
||||
let selected_index = view.read(_cx).selector_selection;
|
||||
let scroll_handle = view.read(_cx).selector_scroll_handle.clone();
|
||||
content.child(
|
||||
v_flex()
|
||||
.track_focus(&selector_focus_handle)
|
||||
@@ -1053,19 +1067,26 @@ impl Ashell {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
div()
|
||||
.relative()
|
||||
.max_h(px(320.))
|
||||
.overflow_scrollbar()
|
||||
.gap_2()
|
||||
.children(sessions.clone().into_iter().enumerate().map(
|
||||
|(ix, session)| {
|
||||
let connect_id = session.id.clone();
|
||||
let is_selected = selected_index == ix + 2;
|
||||
let name = session.name.clone();
|
||||
let detail = format!(
|
||||
"{}@{}:{}",
|
||||
session.user, session.host, session.port
|
||||
);
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.id("selector-scroll-view")
|
||||
.track_scroll(&scroll_handle)
|
||||
.overflow_y_scroll()
|
||||
.gap_2()
|
||||
.children(sessions.clone().into_iter().enumerate().map(
|
||||
|(ix, session)| {
|
||||
let connect_id = session.id.clone();
|
||||
let is_selected = selected_index == ix + 2;
|
||||
let name = session.name.clone();
|
||||
let detail = format!(
|
||||
"{}@{}:{}",
|
||||
session.user, session.host, session.port
|
||||
);
|
||||
div()
|
||||
.id(("selector-open", ix))
|
||||
.w_full()
|
||||
@@ -1120,7 +1141,22 @@ impl Ashell {
|
||||
)
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.child(
|
||||
gpui_component::scroll::Scrollbar::new(&scroll_handle)
|
||||
.id("selector-scrollbar")
|
||||
.axis(gpui_component::scroll::ScrollbarAxis::Vertical)
|
||||
.scrollbar_show(gpui_component::scroll::ScrollbarShow::Always)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1806,6 +1842,9 @@ impl Ashell {
|
||||
let next = (current + delta).clamp(0, entries.len() as i32 - 1) as usize;
|
||||
if next != self.selector_selection {
|
||||
self.selector_selection = next;
|
||||
if next >= 2 {
|
||||
self.selector_scroll_handle.scroll_to_item(next - 2);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -1875,6 +1914,14 @@ impl Ashell {
|
||||
backend,
|
||||
self.events_tx.clone(),
|
||||
));
|
||||
self.active_tab = Some(id.clone());
|
||||
self.tabs_scroll_handle.scroll_to_item(self.tabs.len() - 1);
|
||||
if let Some(session_id) = self.active_session_id() {
|
||||
if let Some(index) = self.config.sessions().iter().position(|s| s.id == session_id) {
|
||||
self.saved_scroll_handle.scroll_to_item(index);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
let sftp_handle = sftp::spawn_sftp(
|
||||
self.runtime.handle(),
|
||||
id.clone(),
|
||||
@@ -1952,7 +1999,17 @@ impl Ashell {
|
||||
{
|
||||
self.pending_sftp_path_sync = Some(path);
|
||||
}
|
||||
self.active_tab = Some(id);
|
||||
self.active_tab = Some(id.clone());
|
||||
if let Some(index) = self.tabs.iter().position(|t| t.id == id) {
|
||||
self.tabs_scroll_handle.scroll_to_item(index);
|
||||
}
|
||||
if self.tabs.iter().any(|t| t.id == id) {
|
||||
if let Some(session_id) = self.active_session_id() {
|
||||
if let Some(index) = self.config.sessions().iter().position(|s| s.id == session_id) {
|
||||
self.saved_scroll_handle.scroll_to_item(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.remote_sample_in_flight = false;
|
||||
self.request_active_system_snapshot();
|
||||
self.focus_handle.focus(window, cx);
|
||||
@@ -3591,7 +3648,7 @@ impl Ashell {
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().sidebar_border)
|
||||
.bg(cx.theme().sidebar)
|
||||
.overflow_y_scrollbar()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
@@ -3715,6 +3772,8 @@ impl Ashell {
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_h(px(0.))
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
@@ -3724,91 +3783,114 @@ impl Ashell {
|
||||
.child(t!("saved")),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.max_h(px(220.))
|
||||
.overflow_scrollbar()
|
||||
.gap_2()
|
||||
.children(sessions.into_iter().enumerate().map(|(ix, session)| {
|
||||
let connect_id = session.id.clone();
|
||||
let edit_id = session.id.clone();
|
||||
let delete_id = session.id.clone();
|
||||
let is_active =
|
||||
active_session_id.as_deref() == Some(session.id.as_str());
|
||||
let name = session.name.clone();
|
||||
let detail = self.session_detail(&session);
|
||||
div()
|
||||
.id(("saved-connect", ix))
|
||||
.w_full()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(if is_active {
|
||||
cx.theme().primary
|
||||
} else {
|
||||
cx.theme().border
|
||||
})
|
||||
.bg(if is_active {
|
||||
cx.theme().tab_active
|
||||
} else {
|
||||
cx.theme().muted
|
||||
})
|
||||
.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().secondary))
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.connect_saved_session(connect_id.clone(), cx)
|
||||
}),
|
||||
)
|
||||
.context_menu({
|
||||
let view = cx.entity();
|
||||
move |menu, window, _| {
|
||||
let edit_value = edit_id.clone();
|
||||
let delete_value = delete_id.clone();
|
||||
menu.item(PopupMenuItem::new("Edit").on_click(
|
||||
window.listener_for(
|
||||
&view,
|
||||
move |this, _, window, cx| {
|
||||
this.edit_saved_session(
|
||||
edit_value.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
))
|
||||
.item(
|
||||
PopupMenuItem::new("Delete").on_click(
|
||||
window.listener_for(
|
||||
&view,
|
||||
move |this, _, _, cx| {
|
||||
this.remove_saved_session(
|
||||
delete_value.clone(),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
div()
|
||||
.relative()
|
||||
.flex_1()
|
||||
.min_h(px(0.))
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.id("saved-sessions-scroll")
|
||||
.track_scroll(&self.saved_scroll_handle)
|
||||
.overflow_y_scroll()
|
||||
.gap_2()
|
||||
.children(sessions.into_iter().enumerate().map(|(ix, session)| {
|
||||
let connect_id = session.id.clone();
|
||||
let edit_id = session.id.clone();
|
||||
let delete_id = session.id.clone();
|
||||
let is_active =
|
||||
active_session_id.as_deref() == Some(session.id.as_str());
|
||||
let name = session.name.clone();
|
||||
let detail = self.session_detail(&session);
|
||||
div()
|
||||
.id(("saved-connect", ix))
|
||||
.w_full()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(if is_active {
|
||||
cx.theme().primary
|
||||
} else {
|
||||
cx.theme().border
|
||||
})
|
||||
.bg(if is_active {
|
||||
cx.theme().tab_active
|
||||
} else {
|
||||
cx.theme().muted
|
||||
})
|
||||
.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().secondary))
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.connect_saved_session(connect_id.clone(), cx)
|
||||
}),
|
||||
)
|
||||
.context_menu({
|
||||
let view = cx.entity();
|
||||
move |menu, window, _| {
|
||||
let edit_value = edit_id.clone();
|
||||
let delete_value = delete_id.clone();
|
||||
menu.item(PopupMenuItem::new("Edit").on_click(
|
||||
window.listener_for(
|
||||
&view,
|
||||
move |this, _, window, cx| {
|
||||
this.edit_saved_session(
|
||||
edit_value.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
))
|
||||
.item(
|
||||
PopupMenuItem::new("Delete").on_click(
|
||||
window.listener_for(
|
||||
&view,
|
||||
move |this, _, _, cx| {
|
||||
this.remove_saved_session(
|
||||
delete_value.clone(),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_size(rems(1.0))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(rems(0.917))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(detail),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_size(rems(1.0))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(rems(0.917))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(detail),
|
||||
),
|
||||
gpui_component::scroll::Scrollbar::new(&self.saved_scroll_handle)
|
||||
.id("saved-scrollbar")
|
||||
.axis(gpui_component::scroll::ScrollbarAxis::Vertical)
|
||||
.scrollbar_show(gpui_component::scroll::ScrollbarShow::Always)
|
||||
)
|
||||
})),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -3881,15 +3963,13 @@ impl Render for Ashell {
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex()
|
||||
.h(px(60.))
|
||||
.h(px(40.))
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().tab_bar)
|
||||
.child(
|
||||
h_flex()
|
||||
.h(px(48.))
|
||||
.h_full()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
@@ -3899,9 +3979,10 @@ impl Render for Ashell {
|
||||
.flex_1()
|
||||
.min_w(px(0.))
|
||||
.h_full()
|
||||
.overflow_x_scrollbar()
|
||||
.overflow_x_hidden()
|
||||
.child(
|
||||
TabBar::new("ashell-tab-bar")
|
||||
.track_scroll(&self.tabs_scroll_handle)
|
||||
.selected_index(active_tab_index.unwrap_or(0))
|
||||
.children(self.tabs.iter().enumerate().map(|(ix, tab)| {
|
||||
let id = tab.id.clone();
|
||||
|
||||
Reference in New Issue
Block a user