mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-04 08:00:18 +00:00
feat(ui): 重构工作区交互与系统菜单 (#111)
This commit is contained in:
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
)]),
|
||||
]);
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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([
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user