mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 16:00:59 +00:00
1661 lines
106 KiB
Rust
1661 lines
106 KiB
Rust
use gpui::{
|
|
Anchor, Context, Focusable as _, FontWeight, InteractiveElement as _, MouseButton,
|
|
ParentElement as _, SharedString, StatefulInteractiveElement as _, Styled as _, Window, div,
|
|
prelude::FluentBuilder as _, px, rems,
|
|
};
|
|
use gpui_component::{
|
|
ActiveTheme as _, Disableable as _, IconName, Sizable as _, WindowExt as _,
|
|
button::{Button, ButtonVariants as _},
|
|
dialog::Dialog,
|
|
h_flex,
|
|
input::Input,
|
|
menu::{DropdownMenu as _, PopupMenuItem},
|
|
progress::Progress,
|
|
scroll::{Scrollbar, ScrollbarShow},
|
|
switch::Switch,
|
|
v_flex,
|
|
};
|
|
use rust_i18n::t;
|
|
|
|
use crate::{Ashell, session::config::AuthMethod, system::format_bytes};
|
|
|
|
impl Ashell {
|
|
pub(crate) fn show_ssh_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.active_dialog.is_some() {
|
|
return;
|
|
}
|
|
self.active_dialog = Some(crate::app::DialogKind::NewSsh);
|
|
|
|
let view = cx.entity();
|
|
let session_name_input = self.session_name_input.clone();
|
|
let host_input = self.host_input.clone();
|
|
let focus_host_input = host_input.clone();
|
|
let port_input = self.port_input.clone();
|
|
let user_input = self.user_input.clone();
|
|
let password_input = self.password_input.clone();
|
|
let key_path_input = self.key_path_input.clone();
|
|
let key_inline_input = self.key_inline_input.clone();
|
|
let passphrase_input = self.passphrase_input.clone();
|
|
|
|
window.open_dialog(cx, move |dialog: Dialog, _window, _cx| {
|
|
dialog
|
|
.title(t!("new_ssh_connection"))
|
|
.w(px(520.))
|
|
.overlay_closable(true)
|
|
.on_close({
|
|
let view = view.clone();
|
|
move |_, _, cx| {
|
|
view.update(cx, |this, cx| {
|
|
this.active_dialog = None;
|
|
cx.notify();
|
|
});
|
|
}
|
|
})
|
|
.content({
|
|
let view = view.clone();
|
|
let session_name_input = session_name_input.clone();
|
|
let host_input = host_input.clone();
|
|
let port_input = port_input.clone();
|
|
let user_input = user_input.clone();
|
|
let password_input = password_input.clone();
|
|
let key_path_input = key_path_input.clone();
|
|
let key_inline_input = key_inline_input.clone();
|
|
let passphrase_input = passphrase_input.clone();
|
|
move |content, window, cx| {
|
|
let is_password = view.read(cx).ssh_auth_method == AuthMethod::Password;
|
|
let is_editing = view.read(cx).editing_session_id.is_some();
|
|
content.child(
|
|
v_flex()
|
|
.gap_3()
|
|
.child(Input::new(&session_name_input).tab_index(0))
|
|
.child(Input::new(&host_input).tab_index(1))
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(Input::new(&port_input).w(px(96.)).tab_index(2))
|
|
.child(Input::new(&user_input).flex_1().tab_index(3)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("ssh-auth-password")
|
|
.label(t!("password").to_string())
|
|
.when(is_password, |button| button.primary())
|
|
.on_click(window.listener_for(
|
|
&view,
|
|
|this, _, _, cx| {
|
|
this.set_ssh_auth_method(
|
|
AuthMethod::Password,
|
|
cx,
|
|
)
|
|
},
|
|
)),
|
|
)
|
|
.child(
|
|
Button::new("ssh-auth-key")
|
|
.label(t!("key").to_string())
|
|
.when(!is_password, |button| button.primary())
|
|
.on_click(window.listener_for(
|
|
&view,
|
|
|this, _, _, cx| {
|
|
this.set_ssh_auth_method(
|
|
AuthMethod::Key,
|
|
cx,
|
|
)
|
|
},
|
|
)),
|
|
),
|
|
)
|
|
.when(is_password, |this| {
|
|
this.child(
|
|
Input::new(&password_input).mask_toggle().tab_index(4),
|
|
)
|
|
})
|
|
.when(!is_password, |this| {
|
|
this.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.cursor_pointer()
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
window.listener_for(
|
|
&view,
|
|
|this, _, window, cx| {
|
|
this.pick_ssh_key_path(window, cx);
|
|
},
|
|
),
|
|
)
|
|
.child(
|
|
Input::new(&key_path_input).tab_index(4),
|
|
),
|
|
)
|
|
.child(
|
|
Button::new("clear-key-path")
|
|
.ghost()
|
|
.icon(IconName::Close)
|
|
.on_click(window.listener_for(
|
|
&view,
|
|
|this, _, window, cx| {
|
|
Self::set_input_value(
|
|
&this.key_path_input,
|
|
"",
|
|
window,
|
|
cx,
|
|
);
|
|
},
|
|
)),
|
|
),
|
|
)
|
|
.child(Input::new(&key_inline_input).h(px(128.)).tab_index(5))
|
|
.child(Input::new(&passphrase_input).mask_toggle().tab_index(6))
|
|
})
|
|
.child(
|
|
h_flex()
|
|
.justify_end()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("connect-ssh-cancel")
|
|
.label(t!("cancel").to_string())
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.active_dialog = None;
|
|
window.close_dialog(cx);
|
|
cx.notify();
|
|
})),
|
|
)
|
|
.child(
|
|
Button::new("connect-ssh-confirm")
|
|
.primary()
|
|
.label(if is_editing {
|
|
t!("save")
|
|
} else {
|
|
t!("connect")
|
|
})
|
|
.on_click(window.listener_for(
|
|
&view,
|
|
|this, _, window, cx| {
|
|
this.connect_ssh(window, cx)
|
|
},
|
|
)),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
})
|
|
});
|
|
window.defer(cx, move |window, cx| {
|
|
window.focus(&focus_host_input.read(cx).focus_handle(cx), cx);
|
|
});
|
|
}
|
|
pub(crate) fn show_selector_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.active_dialog.is_some() {
|
|
return;
|
|
}
|
|
self.active_dialog = Some(crate::app::DialogKind::SessionSelector);
|
|
|
|
let view = cx.entity();
|
|
let selector_focus_handle = self.selector_focus_handle.clone();
|
|
let deferred_selector_focus_handle = selector_focus_handle.clone();
|
|
let sessions = self.config.sessions().to_vec();
|
|
let active_session_id = self.active_session_id().map(ToOwned::to_owned);
|
|
self.selector_selection = self.default_selector_index();
|
|
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
|
|
dialog
|
|
.title(t!("open_session").to_string())
|
|
.w(px(520.))
|
|
.on_close({
|
|
let view = view.clone();
|
|
move |_, _, cx| {
|
|
view.update(cx, |this, cx| {
|
|
this.active_dialog = None;
|
|
cx.notify();
|
|
});
|
|
}
|
|
})
|
|
.on_ok({
|
|
let view = view.clone();
|
|
move |_, window, cx| {
|
|
view.update(cx, |this, cx| {
|
|
this.activate_selector_selection(window, cx);
|
|
});
|
|
false
|
|
}
|
|
})
|
|
.content({
|
|
let view = view.clone();
|
|
let sessions = sessions.clone();
|
|
let _active_session_id = active_session_id.clone();
|
|
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)
|
|
.on_key_down(window.listener_for(
|
|
&view,
|
|
|this, event, window, cx| {
|
|
this.on_selector_key_down(event, window, cx)
|
|
},
|
|
))
|
|
.gap_2()
|
|
.child(
|
|
div()
|
|
.w_full()
|
|
.p_2()
|
|
.rounded_md()
|
|
.border_1()
|
|
.border_color(if selected_index == 0 {
|
|
_cx.theme().primary
|
|
} else {
|
|
_cx.theme().border
|
|
})
|
|
.bg(if selected_index == 0 {
|
|
_cx.theme().tab_active
|
|
} else {
|
|
_cx.theme().muted
|
|
})
|
|
.cursor_pointer()
|
|
.hover(|this| this.bg(_cx.theme().secondary))
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
window.listener_for(&view, |this, _, window, cx| {
|
|
this.active_dialog = None;
|
|
this.open_local(cx);
|
|
window.close_dialog(cx);
|
|
cx.notify();
|
|
}),
|
|
)
|
|
.child(
|
|
v_flex()
|
|
.gap_1()
|
|
.child(
|
|
div()
|
|
.text_size(rems(1.0))
|
|
.font_weight(FontWeight::SEMIBOLD)
|
|
.child(t!("local_terminal")),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(rems(0.917))
|
|
.text_color(_cx.theme().muted_foreground)
|
|
.child(t!("open_local_shell_tab")),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
div()
|
|
.w_full()
|
|
.p_2()
|
|
.rounded_md()
|
|
.border_1()
|
|
.border_color(if selected_index == 1 {
|
|
_cx.theme().primary
|
|
} else {
|
|
_cx.theme().border
|
|
})
|
|
.bg(if selected_index == 1 {
|
|
_cx.theme().tab_active
|
|
} else {
|
|
_cx.theme().muted
|
|
})
|
|
.cursor_pointer()
|
|
.hover(|this| this.bg(_cx.theme().secondary))
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
window.listener_for(&view, |this, _, window, cx| {
|
|
this.active_dialog = None;
|
|
window.close_dialog(cx);
|
|
this.open_new_ssh_dialog(window, cx);
|
|
cx.notify();
|
|
}),
|
|
)
|
|
.child(
|
|
v_flex()
|
|
.gap_1()
|
|
.child(
|
|
div()
|
|
.text_size(rems(1.0))
|
|
.font_weight(FontWeight::SEMIBOLD)
|
|
.child(t!("new_ssh_connection")),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(rems(0.917))
|
|
.text_color(_cx.theme().muted_foreground)
|
|
.child(t!("create_or_edit_ssh_session")),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
div()
|
|
.relative()
|
|
.max_h(px(320.))
|
|
.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()
|
|
.p_2()
|
|
.rounded_md()
|
|
.border_1()
|
|
.border_color(if is_selected {
|
|
_cx.theme().primary
|
|
} else {
|
|
_cx.theme().border
|
|
})
|
|
.bg(if is_selected {
|
|
_cx.theme().tab_active
|
|
} else {
|
|
_cx.theme().muted
|
|
})
|
|
.cursor_pointer()
|
|
.hover(|this| this.bg(_cx.theme().secondary))
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
window.listener_for(
|
|
&view,
|
|
move |this, _, window, cx| {
|
|
this.active_dialog = None;
|
|
this.connect_saved_session(
|
|
connect_id.clone(),
|
|
cx,
|
|
);
|
|
window.close_dialog(cx);
|
|
cx.notify();
|
|
},
|
|
),
|
|
)
|
|
.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(
|
|
gpui_component::scroll::Scrollbar::new(
|
|
&scroll_handle,
|
|
)
|
|
.id("selector-scrollbar")
|
|
.axis(
|
|
gpui_component::scroll::ScrollbarAxis::Vertical,
|
|
)
|
|
.scrollbar_show(
|
|
gpui_component::scroll::ScrollbarShow::Always,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
})
|
|
});
|
|
window.defer(cx, move |window, cx| {
|
|
window.focus(&deferred_selector_focus_handle, cx);
|
|
});
|
|
}
|
|
pub(crate) fn show_transfers_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.active_dialog.is_some() {
|
|
return;
|
|
}
|
|
self.active_dialog = Some(crate::app::DialogKind::Transfers);
|
|
|
|
let view = cx.entity();
|
|
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
|
|
dialog
|
|
.w(px(600.))
|
|
.close_button(false)
|
|
.on_close({
|
|
let view = view.clone();
|
|
move |_, _, cx| {
|
|
view.update(cx, |this, cx| {
|
|
this.active_dialog = None;
|
|
cx.notify();
|
|
});
|
|
}
|
|
})
|
|
.content({
|
|
let view = view.clone();
|
|
move |content, window, cx| {
|
|
let can_clear = view.read(cx).transfers.iter().any(|t| {
|
|
!matches!(
|
|
t.state,
|
|
crate::terminal::TransferState::Running
|
|
| crate::terminal::TransferState::Paused
|
|
)
|
|
});
|
|
|
|
let clear_btn = if can_clear {
|
|
Some(
|
|
Button::new("clear_transfers_btn")
|
|
.small()
|
|
.ghost()
|
|
.icon(IconName::Delete)
|
|
.label(t!("clear_transfers").to_string())
|
|
.on_click(window.listener_for(&view, |this, _, _, cx| {
|
|
this.transfers.retain(|t| {
|
|
matches!(
|
|
t.state,
|
|
crate::terminal::TransferState::Running
|
|
| crate::terminal::TransferState::Paused
|
|
)
|
|
});
|
|
this.config.set_transfers(this.transfers.clone());
|
|
cx.notify();
|
|
})),
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let header = h_flex()
|
|
.w_full()
|
|
.justify_between()
|
|
.items_center()
|
|
.child(
|
|
h_flex()
|
|
.items_baseline()
|
|
.child(
|
|
div()
|
|
.text_lg()
|
|
.font_weight(FontWeight::SEMIBOLD)
|
|
.child(t!("transfers").to_string()),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_sm()
|
|
.text_color(cx.theme().muted_foreground)
|
|
.ml_2()
|
|
.child(t!("transfers_limit").to_string()),
|
|
),
|
|
)
|
|
.child(
|
|
h_flex().gap_2().children(clear_btn).child(
|
|
Button::new("close_dialog")
|
|
.small()
|
|
.ghost()
|
|
.icon(IconName::Close)
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.active_dialog = None;
|
|
window.close_dialog(cx);
|
|
cx.notify();
|
|
})),
|
|
),
|
|
);
|
|
|
|
let mut transfers = view.read(cx).transfers.clone();
|
|
transfers.sort_by_key(|t| match t.state {
|
|
crate::terminal::TransferState::Running
|
|
| crate::terminal::TransferState::Paused => 0,
|
|
_ => 1,
|
|
});
|
|
|
|
if transfers.is_empty() {
|
|
return content.child(
|
|
v_flex().gap_2().child(header).child(
|
|
div()
|
|
.p_4()
|
|
.text_center()
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child(t!("no_transfers_yet").to_string()),
|
|
),
|
|
);
|
|
}
|
|
let list = v_flex().gap_2().children(transfers.into_iter().map(|t| {
|
|
let (icon, _color) = match t.info.kind {
|
|
crate::terminal::TransferType::Upload => {
|
|
(IconName::ArrowUp, cx.theme().primary)
|
|
}
|
|
crate::terminal::TransferType::Download => {
|
|
(IconName::ArrowDown, cx.theme().success)
|
|
}
|
|
};
|
|
|
|
let (status_text, actions) = match t.state {
|
|
crate::terminal::TransferState::Running => {
|
|
let percent = t
|
|
.total
|
|
.map(|tot| {
|
|
(t.transferred as f64 / tot as f64 * 100.0)
|
|
.clamp(0.0, 100.0)
|
|
})
|
|
.unwrap_or(0.0);
|
|
let txt = if let Some(tot) = t.total {
|
|
format!(
|
|
"{:.1}% ({}/{})",
|
|
percent,
|
|
format_bytes(t.transferred),
|
|
format_bytes(tot)
|
|
)
|
|
} else {
|
|
match t.info.kind {
|
|
crate::terminal::TransferType::Upload => {
|
|
format!("{}...", t!("uploading"))
|
|
}
|
|
crate::terminal::TransferType::Download => {
|
|
format!("{}...", t!("downloading"))
|
|
}
|
|
}
|
|
};
|
|
let btn_pause =
|
|
Button::new(SharedString::from(format!("pause-{}", t.info.id)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Pause)
|
|
.on_click(window.listener_for(&view, {
|
|
let id = t.info.id.clone();
|
|
move |this, _, _, _| {
|
|
if let Some(handle) = this.active_sftp_handle() {
|
|
handle.pause_transfer(id.clone());
|
|
}
|
|
}
|
|
}));
|
|
let btn_cancel = Button::new(SharedString::from(format!(
|
|
"cancel-{}",
|
|
t.info.id
|
|
)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Close)
|
|
.on_click(window.listener_for(&view, {
|
|
let id = t.info.id.clone();
|
|
move |this, _, _, _| {
|
|
if let Some(handle) = this.active_sftp_handle() {
|
|
handle.cancel_transfer(id.clone());
|
|
}
|
|
}
|
|
}));
|
|
(txt, h_flex().gap_1().child(btn_pause).child(btn_cancel))
|
|
}
|
|
crate::terminal::TransferState::Paused => {
|
|
let txt = t!("paused").to_string();
|
|
let btn_resume = Button::new(SharedString::from(format!(
|
|
"resume-{}",
|
|
t.info.id
|
|
)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Play)
|
|
.on_click(window.listener_for(&view, {
|
|
let id = t.info.id.clone();
|
|
move |this, _, _, _| {
|
|
if let Some(handle) = this.active_sftp_handle() {
|
|
handle.resume_transfer(id.clone());
|
|
}
|
|
}
|
|
}));
|
|
let btn_cancel = Button::new(SharedString::from(format!(
|
|
"cancel-{}",
|
|
t.info.id
|
|
)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Close)
|
|
.on_click(window.listener_for(&view, {
|
|
let id = t.info.id.clone();
|
|
move |this, _, _, _| {
|
|
if let Some(handle) = this.active_sftp_handle() {
|
|
handle.cancel_transfer(id.clone());
|
|
}
|
|
}
|
|
}));
|
|
(txt, h_flex().gap_1().child(btn_resume).child(btn_cancel))
|
|
}
|
|
crate::terminal::TransferState::Interrupted(ref reason) => {
|
|
let txt = format!("{}: {}", t!("interrupted"), reason);
|
|
let btn_remove = Button::new(SharedString::from(format!(
|
|
"remove-{}",
|
|
t.info.id
|
|
)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Close)
|
|
.on_click(window.listener_for(&view, {
|
|
let id = t.info.id.clone();
|
|
move |this, _, _, cx| {
|
|
this.remove_transfer(&id, cx);
|
|
}
|
|
}));
|
|
(txt, h_flex().gap_1().child(btn_remove))
|
|
}
|
|
crate::terminal::TransferState::Completed => {
|
|
let txt = t!("completed").to_string();
|
|
let mut actions = h_flex().gap_1();
|
|
if matches!(t.info.kind, crate::terminal::TransferType::Download) {
|
|
let btn_folder = Button::new(SharedString::from(format!(
|
|
"folder-{}",
|
|
t.info.id
|
|
)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Folder)
|
|
.on_click({
|
|
let target = t.info.target.clone();
|
|
move |_, _, _| {
|
|
let _ = std::process::Command::new("open")
|
|
.arg(&target)
|
|
.spawn();
|
|
}
|
|
});
|
|
actions = actions.child(btn_folder);
|
|
}
|
|
let btn_remove = Button::new(SharedString::from(format!(
|
|
"remove-{}",
|
|
t.info.id
|
|
)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Close)
|
|
.on_click(window.listener_for(&view, {
|
|
let id = t.info.id.clone();
|
|
move |this, _, _, cx| {
|
|
this.remove_transfer(&id, cx);
|
|
}
|
|
}));
|
|
actions = actions.child(btn_remove);
|
|
(txt, actions)
|
|
}
|
|
crate::terminal::TransferState::Failed(ref err) => {
|
|
let txt = format!("{}: {}", t!("failed"), err);
|
|
let btn_remove = Button::new(SharedString::from(format!(
|
|
"remove-{}",
|
|
t.info.id
|
|
)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Close)
|
|
.on_click(window.listener_for(&view, {
|
|
let id = t.info.id.clone();
|
|
move |this, _, _, cx| {
|
|
this.remove_transfer(&id, cx);
|
|
}
|
|
}));
|
|
(txt, h_flex().gap_1().child(btn_remove))
|
|
}
|
|
crate::terminal::TransferState::Zombie(ref reason) => {
|
|
let txt = format!("{}: {}", t!("zombie"), reason);
|
|
let btn_remove = Button::new(SharedString::from(format!(
|
|
"remove-{}",
|
|
t.info.id
|
|
)))
|
|
.ghost()
|
|
.small()
|
|
.icon(IconName::Close)
|
|
.on_click(window.listener_for(&view, {
|
|
let id = t.info.id.clone();
|
|
move |this, _, _, cx| {
|
|
this.remove_transfer(&id, cx);
|
|
}
|
|
}));
|
|
(txt, h_flex().gap_1().child(btn_remove))
|
|
}
|
|
};
|
|
|
|
let percent = match t.state {
|
|
crate::terminal::TransferState::Completed => 100.0,
|
|
_ => t
|
|
.total
|
|
.map(|tot| t.transferred as f64 / tot as f64 * 100.0)
|
|
.unwrap_or(0.0),
|
|
};
|
|
|
|
v_flex()
|
|
.gap_1()
|
|
.p_2()
|
|
.rounded_md()
|
|
.border_1()
|
|
.border_color(cx.theme().border)
|
|
.bg(cx.theme().muted)
|
|
.child(
|
|
h_flex()
|
|
.items_center()
|
|
.gap_2()
|
|
.child(
|
|
Button::new(SharedString::from(format!(
|
|
"icon-{}",
|
|
t.info.id
|
|
)))
|
|
.icon(icon)
|
|
.ghost()
|
|
.small()
|
|
.disabled(true),
|
|
)
|
|
.child(
|
|
v_flex()
|
|
.flex_1()
|
|
.min_w(px(0.))
|
|
.overflow_hidden()
|
|
.child(
|
|
div()
|
|
.text_size(px(12.))
|
|
.font_weight(FontWeight::SEMIBOLD)
|
|
.text_color(cx.theme().foreground)
|
|
.overflow_hidden()
|
|
.child(t.info.name.clone()),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(px(10.))
|
|
.text_color(cx.theme().muted_foreground)
|
|
.overflow_hidden()
|
|
.child(format!(
|
|
"{}: {}",
|
|
t!("session"),
|
|
t.tab_title
|
|
)),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(px(11.))
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child(status_text.clone()),
|
|
),
|
|
)
|
|
.child(actions),
|
|
)
|
|
.when(
|
|
matches!(
|
|
t.state,
|
|
crate::terminal::TransferState::Running
|
|
| crate::terminal::TransferState::Paused
|
|
),
|
|
|this| {
|
|
this.child(
|
|
Progress::new(format!("progress-{}", t.info.id))
|
|
.with_size(px(4.))
|
|
.value(percent as f32)
|
|
.color(cx.theme().primary)
|
|
.w_full(),
|
|
)
|
|
},
|
|
)
|
|
}));
|
|
|
|
let scroll_handle = window
|
|
.use_keyed_state("transfers-scroll", cx, |_, _| {
|
|
gpui::ScrollHandle::default()
|
|
})
|
|
.read(cx)
|
|
.clone();
|
|
|
|
content.child(
|
|
v_flex().gap_2().child(header).child(
|
|
div()
|
|
.w_full()
|
|
.relative()
|
|
.child(
|
|
div()
|
|
.w_full()
|
|
.max_h(px(400.))
|
|
.flex_col()
|
|
.id("transfers-scroll-view")
|
|
.track_scroll(&scroll_handle)
|
|
.overflow_y_scroll()
|
|
.pr(px(14.))
|
|
.child(list),
|
|
)
|
|
.child(
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.right_0()
|
|
.bottom_0()
|
|
.w(px(16.))
|
|
.child(
|
|
Scrollbar::vertical(&scroll_handle)
|
|
.scrollbar_show(ScrollbarShow::Always),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
})
|
|
});
|
|
}
|
|
pub(crate) fn show_delete_confirm_dialog(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let view = cx.entity();
|
|
let selected_entries = self
|
|
.active_sftp()
|
|
.map(|s| s.selected_entries.clone())
|
|
.unwrap_or_default();
|
|
if selected_entries.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let has_system_path = selected_entries.iter().any(|path| {
|
|
let p = path.as_str();
|
|
p.starts_with("/bin/")
|
|
|| p == "/bin"
|
|
|| p.starts_with("/etc/")
|
|
|| p == "/etc"
|
|
|| p.starts_with("/usr/")
|
|
|| p == "/usr"
|
|
|| p.starts_with("/var/")
|
|
|| p == "/var"
|
|
|| p.starts_with("/sys/")
|
|
|| p == "/sys"
|
|
|| p.starts_with("/dev/")
|
|
|| p == "/dev"
|
|
|| p.starts_with("/boot/")
|
|
|| p == "/boot"
|
|
|| p.starts_with("/lib/")
|
|
|| p == "/lib"
|
|
|| p.starts_with("/opt/")
|
|
|| p == "/opt"
|
|
|| p.starts_with("/run/")
|
|
|| p == "/run"
|
|
|| p.starts_with("/sbin/")
|
|
|| p == "/sbin"
|
|
});
|
|
|
|
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
|
|
dialog
|
|
.title(t!("confirm_delete").to_string())
|
|
.w(px(500.))
|
|
.keyboard(false)
|
|
.on_ok({
|
|
let view = view.clone();
|
|
let paths_to_delete: Vec<String> =
|
|
selected_entries.clone().into_iter().collect();
|
|
move |_, window, cx| {
|
|
view.update(cx, |this, cx| {
|
|
if let Some(handle) = this.active_sftp_handle() {
|
|
let _ = handle.commands.send(
|
|
crate::sftp::SftpCommand::DeletePaths(paths_to_delete.clone()),
|
|
);
|
|
}
|
|
if let Some(sftp) = this.active_sftp_mut() {
|
|
sftp.selected_entries.clear();
|
|
}
|
|
cx.notify();
|
|
});
|
|
window.close_dialog(cx);
|
|
true
|
|
}
|
|
})
|
|
.content({
|
|
let view = view.clone();
|
|
move |content, _window, cx| {
|
|
let scroll_handle = view.read(cx).sftp_delete_scroll_handle.clone();
|
|
let selected_paths: Vec<String> = view
|
|
.read(cx)
|
|
.active_sftp()
|
|
.map(|s| s.selected_entries.clone().into_iter().collect())
|
|
.unwrap_or_default();
|
|
|
|
let warning_block = if has_system_path {
|
|
Some(
|
|
div()
|
|
.w_full()
|
|
.p_3()
|
|
.mb_3()
|
|
.rounded_md()
|
|
.bg(gpui::rgba(0xff00001a))
|
|
.border_1()
|
|
.border_color(gpui::rgba(0xff000080))
|
|
.child(
|
|
div()
|
|
.text_color(gpui::rgba(0xff0000ff))
|
|
.font_weight(FontWeight::BOLD)
|
|
.child(t!("system_path_warning").to_string()),
|
|
),
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let paths_list = div()
|
|
.relative()
|
|
.max_h(px(200.))
|
|
.w_full()
|
|
.border_1()
|
|
.border_color(cx.theme().border)
|
|
.bg(cx.theme().background)
|
|
.rounded_md()
|
|
.child(
|
|
v_flex()
|
|
.id("delete-scroll-view")
|
|
.size_full()
|
|
.track_scroll(&scroll_handle)
|
|
.overflow_y_scroll()
|
|
.p_2()
|
|
.gap_1()
|
|
.children(selected_paths.into_iter().map(|path| {
|
|
div()
|
|
.text_size(rems(0.917))
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child(path)
|
|
})),
|
|
)
|
|
.child(
|
|
div().absolute().top_0().bottom_0().right_0().child(
|
|
gpui_component::scroll::Scrollbar::vertical(&scroll_handle)
|
|
.scrollbar_show(
|
|
gpui_component::scroll::ScrollbarShow::Always,
|
|
),
|
|
),
|
|
);
|
|
|
|
content.child(
|
|
v_flex()
|
|
.w_full()
|
|
.gap_2()
|
|
.children(warning_block)
|
|
.child(
|
|
div().text_size(rems(1.0)).mb_2().child(
|
|
t!(
|
|
"confirm_delete_desc",
|
|
count = view
|
|
.read(cx)
|
|
.active_sftp()
|
|
.map(|s| s.selected_entries.len())
|
|
.unwrap_or(0)
|
|
)
|
|
.to_string(),
|
|
),
|
|
)
|
|
.child(paths_list),
|
|
)
|
|
}
|
|
})
|
|
.footer({
|
|
let view = view.clone();
|
|
let paths_to_delete: Vec<String> =
|
|
selected_entries.clone().into_iter().collect();
|
|
h_flex()
|
|
.w_full()
|
|
.justify_end()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("cancel")
|
|
.ghost()
|
|
.label(t!("cancel").to_string())
|
|
.on_click(move |_, window, cx| {
|
|
window.close_dialog(cx);
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("confirm")
|
|
.danger()
|
|
.label(t!("confirm").to_string())
|
|
.on_click({
|
|
let view = view.clone();
|
|
move |_, window, cx| {
|
|
view.update(cx, |this, cx| {
|
|
if let Some(handle) = this.active_sftp_handle() {
|
|
let _ = handle.commands.send(
|
|
crate::sftp::SftpCommand::DeletePaths(
|
|
paths_to_delete.clone(),
|
|
),
|
|
);
|
|
}
|
|
if let Some(sftp) = this.active_sftp_mut() {
|
|
sftp.selected_entries.clear();
|
|
}
|
|
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;
|
|
}
|
|
self.active_dialog = Some(crate::app::DialogKind::Settings);
|
|
|
|
let view = cx.entity();
|
|
|
|
// Unbind all workspace keys so they don't interfere with keybinding recording
|
|
crate::app::keybinding_recorder::unbind_all_workspace_keys(cx, &self.config);
|
|
self.keybinds_suspended = true;
|
|
|
|
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
|
|
dialog
|
|
.title(t!("settings").to_string())
|
|
.w(px(840.))
|
|
.h(px(560.))
|
|
.on_close({
|
|
let view = view.clone();
|
|
move |_, _window, cx| {
|
|
// Re-register all workspace keys when closing settings
|
|
view.update(cx, |this, cx| {
|
|
this.active_dialog = None;
|
|
this.keybinds_suspended = false;
|
|
this.recording_action = None;
|
|
this.keybind_error = None;
|
|
crate::app::keybinding_recorder::bind_workspace_keys_from_config(
|
|
cx,
|
|
&this.config,
|
|
);
|
|
cx.notify();
|
|
});
|
|
}
|
|
})
|
|
.content({
|
|
let view = view.clone();
|
|
move |content, _window, cx| {
|
|
use gpui_component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField};
|
|
use gpui::IntoElement;
|
|
let version = env!("CARGO_PKG_VERSION");
|
|
let view_clone_for_general = view.clone();
|
|
|
|
let focus_handle = view.read(cx).focus_handle.clone();
|
|
|
|
content.child(
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.size_full()
|
|
.track_focus(&focus_handle)
|
|
.on_key_down({
|
|
let view = view.clone();
|
|
move |ev: &gpui::KeyDownEvent, window, cx| {
|
|
view.update(cx, |this, cx| {
|
|
let Some(action) = this.recording_action.clone() else {
|
|
return;
|
|
};
|
|
|
|
window.prevent_default();
|
|
cx.stop_propagation();
|
|
|
|
if ev.keystroke.key == "escape" {
|
|
this.recording_action = None;
|
|
cx.notify();
|
|
return;
|
|
}
|
|
|
|
let Some(new_key) = crate::app::keybinding_recorder::normalize_recorded_keystroke(ev) else {
|
|
return;
|
|
};
|
|
|
|
// Check for conflicts with other actions
|
|
if let Some((_conflict_id, conflict_label)) =
|
|
crate::app::keybinding_recorder::find_conflict(
|
|
&this.config,
|
|
&action,
|
|
&new_key,
|
|
)
|
|
{
|
|
let formatted = crate::app::keybinding_recorder::format_keystroke(&new_key);
|
|
this.recording_action = None;
|
|
this.keybind_error = Some((
|
|
action.clone(),
|
|
t!("keybind_conflict", key = formatted, action = conflict_label).to_string(),
|
|
));
|
|
cx.notify();
|
|
return;
|
|
}
|
|
|
|
this.recording_action = None;
|
|
this.keybind_error = None;
|
|
this.config.set_key_binding(&action, &new_key);
|
|
if let Err(err) = this.config.save() {
|
|
tracing::error!("failed to save key binding: {err:#}");
|
|
}
|
|
cx.notify();
|
|
});
|
|
}
|
|
})
|
|
.on_mouse_down_out({
|
|
let view = view.clone();
|
|
move |_, _window, cx| {
|
|
view.update(cx, |this, cx| {
|
|
if this.recording_action.is_some() {
|
|
this.recording_action = None;
|
|
cx.notify();
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.child(
|
|
Settings::new("settings")
|
|
.sidebar_width(px(180.))
|
|
.sidebar_style(div().bg(cx.theme().background).style())
|
|
.page(
|
|
SettingPage::new(t!("settings_general").to_string())
|
|
.icon(IconName::Settings)
|
|
.default_open(true)
|
|
.group(
|
|
SettingGroup::new()
|
|
.title(t!("settings_group_appearance").to_string())
|
|
.item(
|
|
SettingItem::new(
|
|
t!("theme_mode").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, _window, cx| {
|
|
let (follow_system, is_dark_mode) = {
|
|
let state = view.read(cx);
|
|
(state.follow_system_theme, state.theme_mode.is_dark())
|
|
};
|
|
Button::new("theme-mode-dropdown")
|
|
.small()
|
|
.icon(if follow_system { IconName::Sun } else if is_dark_mode { IconName::Moon } else { IconName::Sun })
|
|
.label(if follow_system { t!("follow_system").to_string() } else if is_dark_mode { t!("use_dark_mode").to_string() } else { t!("use_light_mode").to_string() })
|
|
.dropdown_menu_with_anchor(Anchor::BottomRight, {
|
|
let view = view.clone();
|
|
move |mut menu, window, cx| {
|
|
let (follow_system, is_dark_mode) = {
|
|
let state = view.read(cx);
|
|
(state.follow_system_theme, state.theme_mode.is_dark())
|
|
};
|
|
menu = menu.min_w(160.)
|
|
.item(
|
|
PopupMenuItem::new(t!("follow_system").to_string())
|
|
.checked(follow_system)
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.set_follow_system_theme(true, window, cx)
|
|
}))
|
|
)
|
|
.item(
|
|
PopupMenuItem::new(t!("use_light_mode").to_string())
|
|
.checked(!follow_system && !is_dark_mode)
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.switch_theme_mode(crate::app::ThemeMode::Light, window, cx)
|
|
}))
|
|
)
|
|
.item(
|
|
PopupMenuItem::new(t!("use_dark_mode").to_string())
|
|
.checked(!follow_system && is_dark_mode)
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.switch_theme_mode(crate::app::ThemeMode::Dark, window, cx)
|
|
}))
|
|
);
|
|
menu
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
.item(
|
|
SettingItem::new(
|
|
t!("light_theme").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, _window, cx| {
|
|
let current_theme = view.read(cx).light_theme_name.to_string();
|
|
Button::new("light-theme-dropdown")
|
|
.small()
|
|
.icon(IconName::Sun)
|
|
.label(current_theme.clone())
|
|
.dropdown_menu_with_anchor(Anchor::BottomRight, {
|
|
let view = view.clone();
|
|
move |mut menu, window, cx| {
|
|
let current_theme = view.read(cx).light_theme_name.to_string();
|
|
let themes = gpui_component::ThemeRegistry::global(cx).sorted_themes();
|
|
let light_themes: Vec<_> = themes.into_iter().filter(|t| !t.mode.is_dark()).map(|t| t.name.clone()).collect();
|
|
menu = menu.min_w(160.).max_h(px(320.)).scrollable(true);
|
|
for theme_name in light_themes {
|
|
let checked = theme_name == current_theme;
|
|
menu = menu.item(
|
|
PopupMenuItem::new(theme_name.clone())
|
|
.checked(checked)
|
|
.on_click(window.listener_for(&view, move |this, _, window, cx| {
|
|
this.apply_theme(theme_name.clone(), window, cx)
|
|
}))
|
|
);
|
|
}
|
|
menu
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
.item(
|
|
SettingItem::new(
|
|
t!("dark_theme").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, _window, cx| {
|
|
let current_theme = view.read(cx).dark_theme_name.to_string();
|
|
Button::new("dark-theme-dropdown")
|
|
.small()
|
|
.icon(IconName::Moon)
|
|
.label(current_theme.clone())
|
|
.dropdown_menu_with_anchor(Anchor::BottomRight, {
|
|
let view = view.clone();
|
|
move |mut menu, window, cx| {
|
|
let current_theme = view.read(cx).dark_theme_name.to_string();
|
|
let themes = gpui_component::ThemeRegistry::global(cx).sorted_themes();
|
|
let dark_themes: Vec<_> = themes.into_iter().filter(|t| t.mode.is_dark()).map(|t| t.name.clone()).collect();
|
|
menu = menu.min_w(160.).max_h(px(320.)).scrollable(true);
|
|
for theme_name in dark_themes {
|
|
let checked = theme_name == current_theme;
|
|
menu = menu.item(
|
|
PopupMenuItem::new(theme_name.clone())
|
|
.checked(checked)
|
|
.on_click(window.listener_for(&view, move |this, _, window, cx| {
|
|
this.apply_theme(theme_name.clone(), window, cx)
|
|
}))
|
|
);
|
|
}
|
|
menu
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
)
|
|
.group(
|
|
SettingGroup::new()
|
|
.title(t!("settings_group_font").to_string())
|
|
.item(
|
|
SettingItem::new(
|
|
t!("ui_font_size").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, window, cx| {
|
|
h_flex()
|
|
.items_center()
|
|
.gap_3()
|
|
.child(Button::new("ui-font-size-down").small().label("-").on_click(window.listener_for(&view, |this, _, _, cx| this.change_ui_font_size(-1.0, cx))))
|
|
.child(div().min_w(px(64.)).text_center().child(format!("{:.0}px", view.read(cx).ui_font_size)))
|
|
.child(Button::new("ui-font-size-up").small().label("+").on_click(window.listener_for(&view, |this, _, _, cx| this.change_ui_font_size(1.0, cx))))
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
.item(
|
|
SettingItem::new(
|
|
t!("terminal_font_size").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, window, cx| {
|
|
h_flex()
|
|
.items_center()
|
|
.gap_3()
|
|
.child(Button::new("terminal-font-size-down").small().label("-").on_click(window.listener_for(&view, |this, _, _, cx| this.change_terminal_font_size(-1.0, cx))))
|
|
.child(div().min_w(px(64.)).text_center().child(format!("{:.0}px", view.read(cx).terminal_font_size)))
|
|
.child(Button::new("terminal-font-size-up").small().label("+").on_click(window.listener_for(&view, |this, _, _, cx| this.change_terminal_font_size(1.0, cx))))
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
.item(
|
|
SettingItem::new(
|
|
t!("ui_font_family").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, _window, cx| {
|
|
Button::new("ui-font-dropdown")
|
|
.small()
|
|
.icon(IconName::ChevronsUpDown)
|
|
.label({
|
|
let current = view.read(cx).ui_font_family.to_string();
|
|
let names = cx.text_system().all_font_names();
|
|
if current == *".SystemUIFont" || current.is_empty() || !names.contains(¤t) {
|
|
t!("system_default").to_string()
|
|
} else if current == "Maple Mono NF CN" {
|
|
format!("Maple Mono NF CN ({})", t!("software_builtin"))
|
|
} else {
|
|
current
|
|
}
|
|
})
|
|
.dropdown_menu_with_anchor(Anchor::BottomRight, {
|
|
let view = view.clone();
|
|
move |mut menu, window, cx| {
|
|
let current = view.read(cx).ui_font_family.to_string();
|
|
let mut names = cx.text_system().all_font_names();
|
|
menu = menu.min_w(200.).max_h(px(320.)).scrollable(true);
|
|
menu = menu.item(
|
|
PopupMenuItem::new(t!("system_default").to_string())
|
|
.checked(current == *".SystemUIFont" || current.is_empty())
|
|
.on_click(window.listener_for(&view, move |this, _, window, cx| {
|
|
this.change_ui_font_family(".SystemUIFont", window, cx);
|
|
}))
|
|
);
|
|
let maple_font = "Maple Mono NF CN".to_string();
|
|
if names.contains(&maple_font) {
|
|
names.retain(|n| n != &maple_font);
|
|
menu = menu.item(
|
|
PopupMenuItem::new(format!("{} ({})", maple_font, t!("software_builtin")))
|
|
.checked(current == maple_font)
|
|
.on_click(window.listener_for(&view, move |this, _, window, cx| {
|
|
this.change_ui_font_family("Maple Mono NF CN", window, cx);
|
|
}))
|
|
).separator();
|
|
}
|
|
for name in names {
|
|
let checked = name == current;
|
|
menu = menu.item(
|
|
PopupMenuItem::new(name.clone())
|
|
.checked(checked)
|
|
.on_click(window.listener_for(&view, move |this, _, window, cx| {
|
|
this.change_ui_font_family(&name, window, cx);
|
|
}))
|
|
);
|
|
}
|
|
menu
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
.item(
|
|
SettingItem::new(
|
|
t!("terminal_font_family").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, _window, cx| {
|
|
Button::new("terminal-font-dropdown")
|
|
.small()
|
|
.icon(IconName::ChevronsUpDown)
|
|
.label({
|
|
let current = view.read(cx).terminal_font_family.to_string();
|
|
if current == "Maple Mono NF CN" {
|
|
format!("Maple Mono NF CN ({})", t!("software_builtin"))
|
|
} else {
|
|
current
|
|
}
|
|
})
|
|
.dropdown_menu_with_anchor(Anchor::BottomRight, {
|
|
let view = view.clone();
|
|
move |mut menu, window, cx| {
|
|
let current = view.read(cx).terminal_font_family.to_string();
|
|
let mut names = cx.text_system().all_font_names();
|
|
menu = menu.min_w(200.).max_h(px(320.)).scrollable(true);
|
|
let maple_font = "Maple Mono NF CN".to_string();
|
|
if names.contains(&maple_font) {
|
|
names.retain(|n| n != &maple_font);
|
|
menu = menu.item(
|
|
PopupMenuItem::new(format!("{} ({})", maple_font, t!("software_builtin")))
|
|
.checked(current == maple_font)
|
|
.on_click(window.listener_for(&view, move |this, _, _window, cx| {
|
|
this.change_terminal_font_family("Maple Mono NF CN", cx);
|
|
}))
|
|
).separator();
|
|
}
|
|
for name in names {
|
|
let checked = name == current;
|
|
menu = menu.item(
|
|
PopupMenuItem::new(name.clone())
|
|
.checked(checked)
|
|
.on_click(window.listener_for(&view, move |this, _, _window, cx| {
|
|
this.change_terminal_font_family(&name, cx);
|
|
}))
|
|
);
|
|
}
|
|
menu
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
)
|
|
.group(
|
|
SettingGroup::new()
|
|
.title(t!("settings_group_other").to_string())
|
|
.item(
|
|
SettingItem::new(
|
|
t!("right_click_copy_paste").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, window, cx| {
|
|
Switch::new("right-click-copy-paste")
|
|
.small()
|
|
.checked(view.read(cx).config.right_click_copy_paste())
|
|
.on_click(window.listener_for(&view, |this, checked, _, cx| {
|
|
this.config.set_right_click_copy_paste(*checked);
|
|
let _ = this.config.save();
|
|
cx.notify();
|
|
}))
|
|
.into_any_element()
|
|
}
|
|
})
|
|
).description(t!("copy_paste_hint", key = if cfg!(target_os = "macos") { "Command" } else { "Ctrl" }).to_string())
|
|
)
|
|
.item(
|
|
SettingItem::new(
|
|
t!("monitoring_position").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, _window, cx| {
|
|
Button::new("monitoring-position-dropdown")
|
|
.small()
|
|
.icon(IconName::PanelLeftOpen)
|
|
.label({
|
|
let pos = view.read(cx).config.monitoring_position().to_string();
|
|
if pos == "Sidebar" {
|
|
t!("position_sidebar").to_string()
|
|
} else if pos == "Hidden" {
|
|
t!("position_hidden").to_string()
|
|
} else {
|
|
t!("position_bottom").to_string()
|
|
}
|
|
})
|
|
.dropdown_menu_with_anchor(Anchor::BottomRight, {
|
|
let view = view.clone();
|
|
move |mut menu, window, cx| {
|
|
let pos = view.read(cx).config.monitoring_position().to_string();
|
|
menu = menu.min_w(160.)
|
|
.item(
|
|
PopupMenuItem::new(t!("position_bottom").to_string())
|
|
.checked(pos == "Bottom")
|
|
.on_click(window.listener_for(&view, |this, _, _window, cx| {
|
|
this.config.set_monitoring_position("Bottom");
|
|
let _ = this.config.save();
|
|
cx.notify();
|
|
}))
|
|
)
|
|
.item(
|
|
PopupMenuItem::new(t!("position_sidebar").to_string())
|
|
.checked(pos == "Sidebar")
|
|
.on_click(window.listener_for(&view, |this, _, _window, cx| {
|
|
this.config.set_monitoring_position("Sidebar");
|
|
let _ = this.config.save();
|
|
cx.notify();
|
|
}))
|
|
)
|
|
.item(
|
|
PopupMenuItem::new(t!("position_hidden").to_string())
|
|
.checked(pos == "Hidden")
|
|
.on_click(window.listener_for(&view, |this, _, _window, cx| {
|
|
this.config.set_monitoring_position("Hidden");
|
|
let _ = this.config.save();
|
|
cx.notify();
|
|
}))
|
|
);
|
|
menu
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
.item(
|
|
SettingItem::new(
|
|
t!("language").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, _window, cx| {
|
|
Button::new("language-dropdown")
|
|
.small()
|
|
.icon(IconName::Globe)
|
|
.label({
|
|
let current_locale = view.read(cx).config.locale().to_string();
|
|
if current_locale == "en" {
|
|
t!("english").to_string()
|
|
} else if current_locale == "zh-CN" {
|
|
t!("chinese").to_string()
|
|
} else {
|
|
t!("follow_system").to_string()
|
|
}
|
|
})
|
|
.dropdown_menu_with_anchor(Anchor::BottomRight, {
|
|
let view = view.clone();
|
|
move |mut menu, window, cx| {
|
|
let current_locale = view.read(cx).config.locale().to_string();
|
|
menu = menu.min_w(160.)
|
|
.item(
|
|
PopupMenuItem::new(t!("follow_system").to_string())
|
|
.checked(current_locale == "system")
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.set_display_language("system", window, cx)
|
|
}))
|
|
)
|
|
.separator()
|
|
.item(
|
|
PopupMenuItem::new(t!("english").to_string())
|
|
.checked(current_locale == "en")
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.set_display_language("en", window, cx)
|
|
}))
|
|
)
|
|
.item(
|
|
PopupMenuItem::new(t!("chinese").to_string())
|
|
.checked(current_locale == "zh-CN")
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.set_display_language("zh-CN", window, cx)
|
|
}))
|
|
);
|
|
menu
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
})
|
|
)
|
|
)
|
|
.item(
|
|
SettingItem::new(
|
|
t!("reset_layout").to_string(),
|
|
SettingField::render({
|
|
let view = view_clone_for_general.clone();
|
|
move |_, window, _cx| {
|
|
Button::new("reset-layout")
|
|
.small()
|
|
.label(t!("reset").to_string())
|
|
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
|
this.reset_layout(window, cx);
|
|
}))
|
|
.into_any_element()
|
|
}
|
|
})
|
|
).description(t!("reset_layout_hint").to_string())
|
|
)
|
|
)
|
|
)
|
|
.page({
|
|
let mut page = SettingPage::new(t!("settings_key_bindings").to_string())
|
|
.icon(IconName::SquareTerminal)
|
|
.default_open(true);
|
|
for group in crate::app::keybinding_recorder::KeybindingsPage::render_groups(&view, cx) {
|
|
page = page.group(group);
|
|
}
|
|
page
|
|
})
|
|
.page(
|
|
SettingPage::new(t!("settings_help").to_string())
|
|
.icon(IconName::BookOpen)
|
|
)
|
|
.page(
|
|
SettingPage::new(t!("settings_about").to_string())
|
|
.icon(IconName::Info)
|
|
.group(
|
|
SettingGroup::new()
|
|
.item(SettingItem::render(move |_, _window, cx| {
|
|
v_flex()
|
|
.gap_2()
|
|
.items_center()
|
|
.child(div().text_size(rems(1.5)).font_weight(FontWeight::BOLD).child("Ashell"))
|
|
.child(div().text_size(rems(0.9)).child(format!("Version {}", version)))
|
|
.child(
|
|
div()
|
|
.text_size(rems(0.9))
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child("A GPUI Component based SSH and local terminal client"),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(rems(0.9))
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child(t!("about_feedback_hint")),
|
|
)
|
|
.child(
|
|
Button::new("github-link")
|
|
.label("https://github.com/rust-kotlin/ashell")
|
|
.ghost()
|
|
.on_click(|_, _window, _cx| {
|
|
let _ = open::that("https://github.com/rust-kotlin/ashell");
|
|
}),
|
|
)
|
|
}))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
})
|
|
});
|
|
}
|
|
}
|