feat(ui): 重构工作区交互与系统菜单 (#111)

This commit is contained in:
Realm
2026-08-27 00:01:03 +08:00
committed by Tom Cruise
parent b4551ee1cc
commit ee98a933e6
18 changed files with 1329 additions and 798 deletions
-1
View File
@@ -1,7 +1,6 @@
pub(crate) const DEFAULT_COLS: u16 = 100;
pub(crate) const DEFAULT_ROWS: u16 = 30;
pub(crate) const SIDEBAR_WIDTH: f32 = 306.0;
pub(crate) const COLLAPSED_SIDEBAR_WIDTH: f32 = 52.0;
pub(crate) const TERMINAL_SCROLLBAR_GUTTER: f32 = 16.0;
#[allow(dead_code)]
+127 -1
View File
@@ -1750,7 +1750,6 @@ impl Ashell {
if self.active_dialog.is_some() {
return;
}
self.show_collapsed_connections = false;
let editing_group = group.clone();
let is_editing = editing_group.is_some();
let existing_groups = self.config.connection_groups();
@@ -3229,6 +3228,99 @@ impl Ashell {
});
}
pub(crate) fn show_about_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.active_dialog.is_some() {
return;
}
self.active_dialog = Some(crate::app::DialogKind::About);
let view = cx.entity();
let version = env!("CARGO_PKG_VERSION");
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
dialog
.title(t!("menu_about_ashell").to_string())
.w(px(440.))
.keyboard(false)
.on_close({
let view = view.clone();
move |_, _, cx| {
view.update(cx, |this, cx| {
if this.active_dialog == Some(crate::app::DialogKind::About) {
this.active_dialog = None;
}
cx.notify();
});
}
})
.content(move |content, _window, cx| {
content.child(
v_flex()
.w_full()
.items_center()
.gap_3()
.py_4()
.child(
div()
.text_size(rems(1.5))
.font_weight(FontWeight::BOLD)
.child("Ashell"),
)
.child(div().text_size(rems(0.9)).child(format!(
"{} {}",
t!("version"),
version
)))
.child(
div()
.text_size(rems(0.9))
.text_color(cx.theme().muted_foreground)
.text_center()
.child(t!("about_description")),
)
.child(
div()
.text_size(rems(0.9))
.text_color(cx.theme().muted_foreground)
.text_center()
.child(t!("about_feedback_hint")),
)
.child(
pointer_button("about-project-link")
.label("https://github.com/rust-kotlin/ashell")
.ghost()
.on_click(|_, _, _| {
if let Err(error) =
open::that("https://github.com/rust-kotlin/ashell")
{
tracing::warn!(
"failed to open Ashell project website: {error}"
);
}
}),
),
)
})
.footer({
let view = view.clone();
h_flex().w_full().justify_end().child(
pointer_button("about-close")
.primary()
.label(t!("close").to_string())
.on_click(move |_, window, cx| {
view.update(cx, |this, cx| {
if this.active_dialog == Some(crate::app::DialogKind::About) {
this.active_dialog = None;
}
cx.notify();
});
window.close_dialog(cx);
}),
)
})
});
}
pub(crate) fn show_settings_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.active_dialog.is_some() {
return;
@@ -3259,6 +3351,7 @@ impl Ashell {
cx,
&this.config,
);
crate::app::system_menu::set_app_menus(cx);
cx.notify();
});
}
@@ -3702,6 +3795,39 @@ impl Ashell {
})
)
)
.item(
SettingItem::new(
t!("local_terminal_encoding").to_string(),
SettingField::render({
let view = view_clone_for_general.clone();
move |_, _window, cx| {
let current = view.read(cx).config.local_terminal_encoding();
pointer_button("local-terminal-encoding-dropdown")
.small()
.icon(IconName::Globe)
.label(current.label())
.dropdown_menu_with_anchor(Anchor::BottomRight, {
let view = view.clone();
move |mut menu, window, cx| {
let current = view.read(cx).config.local_terminal_encoding();
menu = menu.min_w(0.);
for encoding in TERMINAL_ENCODINGS.iter().copied() {
menu = menu.item(
PopupMenuItem::new(encoding.label())
.checked(encoding == current)
.on_click(window.listener_for(&view, move |this, _, _, cx| {
this.change_local_terminal_encoding(encoding, cx);
})),
);
}
menu
}
})
.into_any_element()
}
})
)
)
.item(
SettingItem::new(
t!("cursor_style").to_string(),
+10 -8
View File
@@ -6,6 +6,7 @@ pub mod keybinding_recorder;
pub mod resizable;
pub mod search;
pub mod startup;
pub mod system_menu;
pub mod theme;
pub mod ui;
@@ -213,6 +214,7 @@ impl ScrollbarHandle for TerminalScrollbarHandle {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DialogKind {
About,
Settings,
SessionSelector,
ConnectionExport,
@@ -312,6 +314,8 @@ pub(crate) struct Ashell {
pub(crate) process_scroll_handle: gpui::ScrollHandle,
pub(crate) port_scroll_handle: UniformListScrollHandle,
pub(crate) tabs_scroll_handle: gpui::ScrollHandle,
pub(crate) tabs_viewport_bounds: Option<Bounds<Pixels>>,
pub(crate) tab_scroll_animation_id: u64,
pub(crate) selector_scroll_handle: gpui::ScrollHandle,
pub(crate) saved_scroll_handle: gpui::ScrollHandle,
pub(crate) command_history_scroll_handle: gpui::ScrollHandle,
@@ -331,7 +335,6 @@ pub(crate) struct Ashell {
pub(crate) transfers: Vec<crate::terminal::Transfer>,
pub(crate) show_transfers_dialog: bool,
pub(crate) show_command_history: bool,
pub(crate) show_collapsed_connections: bool,
pub(crate) ssh_command_buffers: HashMap<String, String>,
pub(crate) ssh_command_starts: HashMap<String, (usize, usize)>,
pub(crate) system_status: Option<SharedString>,
@@ -354,8 +357,6 @@ pub(crate) struct Ashell {
pub(crate) terminal_marked_text: Option<String>,
pub(crate) sftp_panel_minimized: bool,
pub(crate) sidebar_collapsed: bool,
pub(crate) collapsed_saved_scroll_handle: gpui::ScrollHandle,
pub(crate) collapsed_saved_sessions_overflowing: bool,
pub(crate) window_active: bool,
pub(crate) native_window_handle: Option<isize>,
pub(crate) unread_terminal_notifications: HashSet<String>,
@@ -395,7 +396,8 @@ pub(crate) struct Ashell {
pub(crate) events_tx: mpsc::Sender<BackendEvent>,
pub(crate) last_window_size: Option<gpui::Size<Pixels>>,
pub(crate) last_sidebar_width: Option<Pixels>,
pub(crate) should_move_window: bool,
pub(crate) pending_local_terminal_resizes: HashMap<String, (u16, u16)>,
pub(crate) local_terminal_resize_task: Option<gpui::Task<()>>,
pub(crate) hovered_url: Option<HoveredUrl>,
pub(crate) cmd_ctrl_pressed: bool,
pub(crate) _subscriptions: Vec<gpui::Subscription>,
@@ -826,6 +828,8 @@ impl Ashell {
process_scroll_handle: gpui::ScrollHandle::new(),
port_scroll_handle: UniformListScrollHandle::new(),
tabs_scroll_handle: gpui::ScrollHandle::new(),
tabs_viewport_bounds: None,
tab_scroll_animation_id: 0,
selector_scroll_handle: gpui::ScrollHandle::new(),
saved_scroll_handle: gpui::ScrollHandle::new(),
command_history_scroll_handle: gpui::ScrollHandle::new(),
@@ -858,7 +862,6 @@ impl Ashell {
},
show_transfers_dialog: false,
show_command_history: false,
show_collapsed_connections: false,
ssh_command_buffers: HashMap::new(),
ssh_command_starts: HashMap::new(),
system_status: None,
@@ -878,8 +881,6 @@ impl Ashell {
drag_split_origin: None,
sftp_panel_minimized: config.sftp_panel_minimized(),
sidebar_collapsed: config.sidebar_collapsed(),
collapsed_saved_scroll_handle: gpui::ScrollHandle::new(),
collapsed_saved_sessions_overflowing: false,
window_active: window.is_window_active(),
native_window_handle: crate::desktop_notification::native_window_handle(window),
unread_terminal_notifications: HashSet::new(),
@@ -916,7 +917,8 @@ impl Ashell {
events_tx,
last_window_size: None,
last_sidebar_width,
should_move_window: false,
pending_local_terminal_resizes: HashMap::new(),
local_terminal_resize_task: None,
hovered_url: None,
cmd_ctrl_pressed: false,
_subscriptions,
-1
View File
@@ -326,7 +326,6 @@ impl Ashell {
pointer_button("search-btn")
.ghost()
.small()
.rounded(px(999.))
.icon(IconName::Search)
.tooltip(t!("search").to_string())
.on_click(cx.listener(|this, _, window, cx| {
+1
View File
@@ -343,6 +343,7 @@ pub(crate) fn open_main_window(cx: &mut App) {
window.set_window_title("ashell");
gpui_component::Theme::sync_system_appearance(Some(window), cx);
let view = cx.new(|cx| Ashell::new(window, cx));
crate::app::system_menu::set_app_menus(cx);
tracing::info!("[ui] main application window opened");
let focus_handle = view.read(cx).focus_handle.clone();
+132
View File
@@ -0,0 +1,132 @@
#[cfg(target_os = "macos")]
use gpui::KeyBinding;
use gpui::{App, Menu, MenuItem};
use rust_i18n::t;
gpui::actions!(
ashell_system_menu,
[
AboutAshell,
CloseWindow,
HideAshell,
HideOtherApplications,
MinimizeWindow,
NewLocalTerminal,
OpenProjectWebsite,
ShowAllApplications,
ToggleFullScreen,
ZoomWindow
]
);
const PROJECT_URL: &str = "https://github.com/rust-kotlin/ashell";
pub(crate) fn init(cx: &mut App) {
#[cfg(target_os = "macos")]
{
cx.on_action(|_: &HideAshell, cx| cx.hide());
cx.on_action(|_: &HideOtherApplications, cx| cx.hide_other_apps());
cx.on_action(|_: &ShowAllApplications, cx| cx.unhide_other_apps());
cx.bind_keys([
KeyBinding::new("cmd-h", HideAshell, None),
KeyBinding::new("alt-cmd-h", HideOtherApplications, None),
KeyBinding::new("cmd-m", MinimizeWindow, None),
KeyBinding::new("ctrl-cmd-f", ToggleFullScreen, None),
]);
}
cx.on_action(|_: &OpenProjectWebsite, _| {
if let Err(error) = open::that(PROJECT_URL) {
tracing::warn!("failed to open Ashell project website: {error}");
}
});
}
pub(crate) fn set_app_menus(cx: &mut App) {
let mut application_items = vec![
MenuItem::action(t!("menu_about_ashell").to_string(), AboutAshell),
MenuItem::separator(),
MenuItem::action(t!("settings").to_string(), crate::OpenSettings),
];
#[cfg(target_os = "macos")]
application_items.extend([
MenuItem::separator(),
MenuItem::os_submenu(
t!("menu_services").to_string(),
gpui::SystemMenuType::Services,
),
MenuItem::separator(),
MenuItem::action(t!("menu_hide_ashell").to_string(), HideAshell),
MenuItem::action(t!("menu_hide_others").to_string(), HideOtherApplications),
MenuItem::action(t!("menu_show_all").to_string(), ShowAllApplications),
]);
application_items.extend([
MenuItem::separator(),
MenuItem::action(t!("menu_quit_ashell").to_string(), crate::QuitApplication),
]);
cx.set_menus([
Menu::new("Ashell").items(application_items),
Menu::new(t!("menu_file").to_string()).items([
MenuItem::action(t!("local_terminal").to_string(), NewLocalTerminal),
MenuItem::action(t!("new_connection").to_string(), crate::NewSsh),
MenuItem::separator(),
MenuItem::action(t!("menu_close_window").to_string(), CloseWindow),
]),
Menu::new(t!("menu_edit").to_string()).items([
MenuItem::os_action(
t!("menu_undo").to_string(),
gpui_component::input::Undo,
gpui::OsAction::Undo,
),
MenuItem::os_action(
t!("menu_redo").to_string(),
gpui_component::input::Redo,
gpui::OsAction::Redo,
),
MenuItem::separator(),
MenuItem::os_action(
t!("menu_cut").to_string(),
gpui_component::input::Cut,
gpui::OsAction::Cut,
),
MenuItem::os_action(
t!("menu_copy").to_string(),
crate::Copy,
gpui::OsAction::Copy,
),
MenuItem::os_action(
t!("menu_paste").to_string(),
crate::Paste,
gpui::OsAction::Paste,
),
MenuItem::separator(),
MenuItem::os_action(
t!("menu_select_all").to_string(),
gpui_component::input::SelectAll,
gpui::OsAction::SelectAll,
),
]),
Menu::new(t!("menu_view").to_string()).items([
MenuItem::action(t!("search").to_string(), crate::OpenSearch),
MenuItem::action(t!("menu_toggle_sidebar").to_string(), crate::ToggleSidebar),
MenuItem::action(
t!("settings_open_transfers").to_string(),
crate::OpenTransfers,
),
MenuItem::separator(),
MenuItem::action(t!("menu_toggle_full_screen").to_string(), ToggleFullScreen),
]),
// GPUI recognizes this exact title as the native macOS Window menu.
Menu::new("Window").items([
MenuItem::action(t!("menu_minimize").to_string(), MinimizeWindow),
MenuItem::action(t!("menu_zoom").to_string(), ZoomWindow),
]),
Menu::new(t!("menu_help").to_string()).items([MenuItem::action(
t!("menu_project_website").to_string(),
OpenProjectWebsite,
)]),
]);
}
+1
View File
@@ -127,6 +127,7 @@ impl Ashell {
}
rust_i18n::set_locale(&active_locale);
gpui_component::set_locale(&active_locale);
crate::app::system_menu::set_app_menus(cx);
self.connection_filter_input.update(cx, |input, cx| {
input.set_placeholder(t!("filter_connections").to_string(), window, cx);
});
+745 -725
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -22,6 +22,9 @@ pub(crate) use app::keybinding_recorder::{
OpenSession, OpenSettings, OpenTransfers, Paste, QuitApplication, SplitPaneDown, SplitPaneLeft,
SplitPaneRight, SplitPaneUp, ToggleSftpZoom, ToggleSidebar,
};
pub(crate) use app::system_menu::{
AboutAshell, CloseWindow, MinimizeWindow, NewLocalTerminal, ToggleFullScreen, ZoomWindow,
};
pub(crate) use app::{Ashell, PaneLayout, SelectorEntry, SftpContextMenuState, TabGroup};
@@ -45,6 +48,7 @@ fn main() {
});
app.run(move |cx| {
gpui_component::init(cx);
app::system_menu::init(cx);
cx.on_action(|_: &QuitApplication, cx| cx.quit());
cx.bind_keys([
+15
View File
@@ -271,6 +271,8 @@ pub struct ConfigFile {
pub locale: String,
#[serde(default = "default_terminal_font_size")]
pub terminal_font_size: f32,
#[serde(default)]
pub local_terminal_encoding: TextEncoding,
#[serde(default = "default_ui_font_size")]
pub ui_font_size: f32,
#[serde(default)]
@@ -446,6 +448,7 @@ impl Default for ConfigFile {
dark_theme_name: String::new(),
locale: default_locale(),
terminal_font_size: default_terminal_font_size(),
local_terminal_encoding: TextEncoding::Utf8,
ui_font_size: default_ui_font_size(),
right_click_copy_paste: false,
keyword_highlight: false,
@@ -1249,6 +1252,14 @@ impl ConfigStore {
self.cache.terminal_font_size = terminal_font_size.max(10.0);
}
pub fn local_terminal_encoding(&self) -> TextEncoding {
self.cache.local_terminal_encoding
}
pub fn set_local_terminal_encoding(&mut self, encoding: TextEncoding) {
self.cache.local_terminal_encoding = encoding;
}
pub fn ui_font_size(&self) -> f32 {
if self.cache.ui_font_size <= 0.0 {
default_ui_font_size()
@@ -1468,6 +1479,7 @@ impl ConfigStore {
disk_config.dark_theme_name = local_config.dark_theme_name;
disk_config.locale = local_config.locale;
disk_config.terminal_font_size = local_config.terminal_font_size;
disk_config.local_terminal_encoding = local_config.local_terminal_encoding;
disk_config.ui_font_size = local_config.ui_font_size;
disk_config.right_click_copy_paste = local_config.right_click_copy_paste;
disk_config.keyword_highlight = local_config.keyword_highlight;
@@ -1876,6 +1888,7 @@ mod tests {
assert!(config.sftp_file_columns.is_none());
assert!(!config.sftp_file_columns_customized);
assert!(config.connection_groups.is_empty());
assert_eq!(config.local_terminal_encoding, TextEncoding::Utf8);
assert!(config.collapsed_connection_groups.is_empty());
}
@@ -2020,6 +2033,7 @@ mod tests {
let mut local_config = ConfigFile {
ui_font_size: 18.0,
terminal_font_size: 20.0,
local_terminal_encoding: TextEncoding::Gbk,
show_hidden_files: true,
sftp_file_columns_customized: true,
remember_tabs: true,
@@ -2057,6 +2071,7 @@ mod tests {
assert_eq!(decrypted.ui_font_size, 18.0);
assert_eq!(decrypted.terminal_font_size, 20.0);
assert_eq!(decrypted.local_terminal_encoding, TextEncoding::Gbk);
assert!(decrypted.show_hidden_files);
assert!(decrypted.sftp_file_columns_customized);
assert!(decrypted.remember_tabs);
+127 -11
View File
@@ -9,6 +9,7 @@ use gpui::{
};
use gpui_component::{Theme, WindowExt as _, input::InputState};
use rust_i18n::t;
use std::time::Duration;
use uuid::Uuid;
use self::config::{
@@ -99,6 +100,7 @@ fn connecting_sftp_state() -> crate::terminal::SftpUiState {
loading_directories: std::collections::HashSet::new(),
directory_errors: std::collections::HashMap::new(),
selected_path: None,
selection_anchor: None,
preview: None,
selected_entries: std::collections::HashSet::new(),
home_dir: "/".into(),
@@ -455,6 +457,66 @@ impl Ashell {
cx.notify();
}
pub(crate) fn resize_terminal_for_layout(
&mut self,
tab_id: &str,
cols: u16,
rows: u16,
cx: &mut Context<Self>,
) -> Option<RenderSnapshot> {
let tab_index = self.tabs.iter().position(|tab| tab.id == tab_id)?;
let cols = cols.max(1);
let rows = rows.max(1);
if self.tabs[tab_index].kind != TabKind::Local {
let keyword_highlight = self.config.keyword_highlight();
if self.tabs[tab_index].resize(cols, rows) {
return Some(self.tabs[tab_index].render_snapshot(keyword_highlight));
}
return None;
}
if self.tabs[tab_index].cols == cols && self.tabs[tab_index].rows == rows {
self.pending_local_terminal_resizes.remove(tab_id);
if self.pending_local_terminal_resizes.is_empty() {
self.local_terminal_resize_task = None;
}
return None;
}
let requested_size = (cols, rows);
if self.pending_local_terminal_resizes.get(tab_id) == Some(&requested_size) {
return None;
}
self.pending_local_terminal_resizes
.insert(tab_id.to_string(), requested_size);
self.local_terminal_resize_task = Some(cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(80))
.await;
let _ = this.update(cx, |this, cx| {
let pending = std::mem::take(&mut this.pending_local_terminal_resizes);
this.local_terminal_resize_task = None;
let mut resized = false;
for (tab_id, (cols, rows)) in pending {
if let Some(tab) = this
.tabs
.iter_mut()
.find(|tab| tab.id == tab_id && tab.kind == TabKind::Local)
{
resized |= tab.resize(cols, rows);
}
}
if resized {
cx.notify();
}
});
}));
None
}
pub(crate) fn open_local(&mut self, cx: &mut Context<Self>) {
let previous_active_tab = self.active_tab.clone();
let id = Uuid::new_v4().to_string();
@@ -473,6 +535,7 @@ impl Ashell {
let mut tab =
TerminalTab::new_local(id.clone(), title.clone(), backend, backend_events);
tab.local_cwd = initial_directory;
tab.set_text_encoding(self.config.local_terminal_encoding());
tab.resize(DEFAULT_COLS, DEFAULT_ROWS);
self.tabs.push(tab);
self.active_tab = Some(id.clone());
@@ -789,7 +852,6 @@ impl Ashell {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.show_collapsed_connections = false;
let Some(session) = self.config.get(&session_id).cloned() else {
self.status = "saved session not found".into();
cx.notify();
@@ -805,7 +867,6 @@ impl Ashell {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.show_collapsed_connections = false;
let Some(session) = self.config.get(&session_id).cloned() else {
self.status = "saved session not found".into();
cx.notify();
@@ -960,6 +1021,36 @@ impl Ashell {
cx.notify();
}
pub(crate) fn change_local_terminal_encoding(
&mut self,
encoding: TextEncoding,
cx: &mut Context<Self>,
) {
if self.config.local_terminal_encoding() == encoding {
return;
}
self.config.set_local_terminal_encoding(encoding);
for tab in self
.tabs
.iter_mut()
.filter(|tab| tab.kind == TabKind::Local)
{
tab.set_text_encoding(encoding);
}
if self.config.remember_tabs() {
self.capture_tabs_state();
}
self.save_preferences_background();
self.status = t!(
"local_terminal_encoding_changed",
encoding = encoding.label()
)
.to_string()
.into();
cx.notify();
}
pub(crate) fn refresh_ssh_config(&mut self) {
self.ssh_config_entries =
crate::session::ssh_config::parse_ssh_config().unwrap_or_default();
@@ -1010,7 +1101,6 @@ impl Ashell {
}
pub(crate) fn connect_saved_session(&mut self, session_id: String, cx: &mut Context<Self>) {
self.show_collapsed_connections = false;
tracing::info!(
"[ui] user clicked to connect saved session '{}'",
session_id
@@ -1846,14 +1936,6 @@ impl Ashell {
.map(|tab| tab.kind)
}
pub(crate) fn active_title(&self) -> String {
self.active_tab
.as_ref()
.and_then(|id| self.tabs.iter().find(|t| &t.id == id))
.map(|t| t.title.clone())
.unwrap_or_else(|| t!("idle_no_session").into())
}
pub(crate) fn active_ssh_session(&self) -> Option<(String, Session)> {
let active_id = self.active_tab.as_ref()?;
let tab = self.tabs.iter().find(|tab| &tab.id == active_id)?;
@@ -2204,6 +2286,40 @@ impl Ashell {
cx.notify();
}
pub(crate) fn reorder_tab_groups(
&mut self,
dragged_group_id: &str,
target_group_id: &str,
cx: &mut Context<Self>,
) {
if dragged_group_id == target_group_id {
return;
}
let Some(from_index) = self
.tab_groups
.iter()
.position(|group| group.id == dragged_group_id)
else {
return;
};
let Some(mut target_index) = self
.tab_groups
.iter()
.position(|group| group.id == target_group_id)
else {
return;
};
let dragged_group = self.tab_groups.remove(from_index);
if from_index < target_index {
target_index -= 1;
}
self.tab_groups.insert(target_index, dragged_group);
self.save_tabs_state_background();
cx.notify();
}
pub(crate) fn sync_pane_root_to_group(&mut self) {
if let Some(group_id) = self.active_group.clone() {
if let Some(group) = self.tab_groups.iter_mut().find(|g| g.id == group_id) {
+41 -1
View File
@@ -46,6 +46,7 @@ impl Ashell {
if let Some(sftp) = self.active_sftp_mut() {
sftp.current_path = path.clone();
sftp.selected_path = None;
sftp.selection_anchor = None;
sftp.preview = None;
sftp.selected_entries.clear();
sftp.expand_to(&path);
@@ -134,13 +135,48 @@ impl Ashell {
}
}
pub(crate) fn select_sftp_entry(&mut self, entry: RemoteEntry, cx: &mut Context<Self>) {
pub(crate) fn select_sftp_entry(
&mut self,
entry: RemoteEntry,
visible_paths: &[String],
extend_range: bool,
cx: &mut Context<Self>,
) {
if extend_range {
if let Some(sftp) = self.active_sftp_mut() {
let target_path = entry.full_path.clone();
let target_index = visible_paths.iter().position(|path| path == &target_path);
let anchor_index = sftp
.selection_anchor
.as_ref()
.and_then(|anchor| visible_paths.iter().position(|path| path == anchor));
sftp.selected_entries.clear();
if let Some(target_index) = target_index {
let anchor_index = anchor_index.unwrap_or(target_index);
let start = anchor_index.min(target_index);
let end = anchor_index.max(target_index);
sftp.selected_entries
.extend(visible_paths[start..=end].iter().cloned());
} else {
sftp.selected_entries.insert(target_path.clone());
}
if sftp.selection_anchor.is_none() {
sftp.selection_anchor = Some(target_path.clone());
}
sftp.selected_path = Some(target_path);
}
cx.notify();
return;
}
if entry.is_dir {
self.navigate_sftp(entry.full_path, cx);
return;
}
self.mark_sftp_entry_selected(&entry.full_path, cx);
if let Some(sftp) = self.active_sftp_mut() {
sftp.selection_anchor = Some(entry.full_path.clone());
if !sftp.selected_entries.remove(&entry.full_path) {
sftp.selected_entries.insert(entry.full_path);
}
@@ -381,6 +417,8 @@ impl Ashell {
cx: &mut Context<Self>,
) {
if let Some(sftp) = self.active_sftp_mut() {
sftp.selected_path = Some(path.clone());
sftp.selection_anchor = Some(path.clone());
if checked {
sftp.selected_entries.insert(path);
} else {
@@ -403,6 +441,8 @@ impl Ashell {
}
} else {
sftp.selected_entries.clear();
sftp.selected_path = None;
sftp.selection_anchor = None;
}
cx.notify();
}
+16 -22
View File
@@ -614,32 +614,26 @@ impl Element for TerminalElement {
cx: &mut App,
) -> Self::PrepaintState {
let _ = self.base_text_style(cx);
let (rects, runs, custom_blocks, underlines) = self.layout_grid(cx);
// Save the precise GPUI-rendered bounds of this terminal element.
// This is 100% accurate because it is recorded during layout prepaint.
// Local window animations emit many intermediate sizes. The view
// coalesces those requests before resizing its terminal grid and PTY.
let view = self.view.clone();
let tab_id = self.tab_id.clone();
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();
let cell_width = this.terminal_cell_width();
let w = bounds.size.width.as_f32();
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();
}
let cols = (bounds.size.width.as_f32() / self.cell_width.as_f32())
.floor()
.max(1.0) as u16;
let rows = (bounds.size.height.as_f32() / self.line_height.as_f32())
.floor()
.max(1.0) as u16;
let resized_snapshot = view.update(cx, |this, cx| {
this.terminal_bounds.insert(tab_id.clone(), bounds);
this.resize_terminal_for_layout(&tab_id, cols, rows, cx)
});
if let Some(snapshot) = resized_snapshot {
self.snapshot = snapshot;
}
let (rects, runs, custom_blocks, underlines) = self.layout_grid(cx);
PrepaintState {
bounds,
+6 -6
View File
@@ -19,11 +19,7 @@ use alacritty_terminal::{
grid::{Dimensions, Scroll},
index::{Column, Line, Point, Side},
selection::{Selection, SelectionRange, SelectionType},
term::{
Config, Term, TermMode,
cell::{Cell, Flags},
point_to_viewport, viewport_to_point,
},
term::{Config, Term, TermMode, cell::Cell, point_to_viewport, viewport_to_point},
vte::ansi::{CursorShape, Processor},
};
use base64::{Engine as _, engine::general_purpose};
@@ -891,6 +887,7 @@ pub struct SftpUiState {
pub loading_directories: HashSet<String>,
pub directory_errors: HashMap<String, String>,
pub selected_path: Option<String>,
pub selection_anchor: Option<String>,
pub preview: Option<PreviewData>,
pub selected_entries: HashSet<String>,
pub home_dir: String,
@@ -1626,7 +1623,7 @@ impl TerminalTab {
next
}
pub fn resize(&mut self, cols: u16, rows: u16) {
pub fn resize(&mut self, cols: u16, rows: u16) -> bool {
let new_cols = cols.max(1);
let new_rows = rows.max(1);
if self.cols != new_cols || self.rows != new_rows {
@@ -1639,6 +1636,9 @@ impl TerminalTab {
);
self.term.resize(TerminalSize::new(self.cols, self.rows));
self.send_backend(BackendCommand::Resize { cols, rows });
true
} else {
false
}
}