mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd866d1400 |
@@ -13,6 +13,16 @@ size: "SIZE"
|
|||||||
modified: "MODIFIED"
|
modified: "MODIFIED"
|
||||||
new_ssh_connection: "New SSH Connection"
|
new_ssh_connection: "New SSH Connection"
|
||||||
create_or_edit_ssh_session: "Create or edit an SSH session"
|
create_or_edit_ssh_session: "Create or edit an SSH session"
|
||||||
|
new_folder: "New Folder"
|
||||||
|
create_folder_success: "Created folder: %{name}"
|
||||||
|
create_folder_failed: "Failed to create folder: %{err}"
|
||||||
|
delete_selected: "Delete"
|
||||||
|
confirm_delete: "Confirm Delete"
|
||||||
|
confirm_delete_desc: "Are you sure you want to delete the selected %{count} items? This action cannot be undone."
|
||||||
|
system_path_warning: "WARNING: You are attempting to delete critical system paths! This may cause system corruption!"
|
||||||
|
delete_success: "Successfully deleted %{count} items"
|
||||||
|
delete_failed: "Failed to delete: %{err}"
|
||||||
|
deleting_paths: "Deleting %{count} items..."
|
||||||
local_terminal: "Local Terminal"
|
local_terminal: "Local Terminal"
|
||||||
open_local_shell_tab: "Open a new local shell tab"
|
open_local_shell_tab: "Open a new local shell tab"
|
||||||
connect: "Connect"
|
connect: "Connect"
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ size: "大小"
|
|||||||
modified: "修改时间"
|
modified: "修改时间"
|
||||||
new_ssh_connection: "新建 SSH 连接"
|
new_ssh_connection: "新建 SSH 连接"
|
||||||
create_or_edit_ssh_session: "创建或编辑一个 SSH 会话"
|
create_or_edit_ssh_session: "创建或编辑一个 SSH 会话"
|
||||||
|
new_folder: "新建文件夹"
|
||||||
|
create_folder_success: "成功创建文件夹: %{name}"
|
||||||
|
create_folder_failed: "创建文件夹失败: %{err}"
|
||||||
|
delete_selected: "删除"
|
||||||
|
confirm_delete: "确认删除"
|
||||||
|
confirm_delete_desc: "确定要删除以下选中的 %{count} 个项吗?此操作无法撤销。"
|
||||||
|
system_path_warning: "警告:您即将删除系统关键路径!这可能会导致系统损坏!"
|
||||||
|
delete_success: "成功删除 %{count} 个项"
|
||||||
|
delete_failed: "删除失败: %{err}"
|
||||||
|
deleting_paths: "正在删除 %{count} 个项..."
|
||||||
|
cancel: "取消"
|
||||||
|
confirm: "确认"
|
||||||
local_terminal: "本地终端"
|
local_terminal: "本地终端"
|
||||||
open_local_shell_tab: "打开一个新的本地终端页面"
|
open_local_shell_tab: "打开一个新的本地终端页面"
|
||||||
connect: "连接"
|
connect: "连接"
|
||||||
|
|||||||
+282
-3
@@ -171,6 +171,9 @@ struct Ashell {
|
|||||||
connection_progress: Option<ConnectionProgress>,
|
connection_progress: Option<ConnectionProgress>,
|
||||||
pending_sftp_path_sync: Option<String>,
|
pending_sftp_path_sync: Option<String>,
|
||||||
sftp_context_menu: Option<SftpContextMenuState>,
|
sftp_context_menu: Option<SftpContextMenuState>,
|
||||||
|
sftp_creating_folder: bool,
|
||||||
|
sftp_new_folder_input: Entity<InputState>,
|
||||||
|
sftp_delete_scroll_handle: gpui::ScrollHandle,
|
||||||
show_hidden_files: bool,
|
show_hidden_files: bool,
|
||||||
transfers: Vec<crate::terminal::Transfer>,
|
transfers: Vec<crate::terminal::Transfer>,
|
||||||
show_transfers_dialog: bool,
|
show_transfers_dialog: bool,
|
||||||
@@ -235,6 +238,7 @@ impl Ashell {
|
|||||||
.placeholder("-----BEGIN OPENSSH PRIVATE KEY-----")
|
.placeholder("-----BEGIN OPENSSH PRIVATE KEY-----")
|
||||||
});
|
});
|
||||||
let sftp_path_input = cx.new(|cx| InputState::new(window, cx).default_value("/"));
|
let sftp_path_input = cx.new(|cx| InputState::new(window, cx).default_value("/"));
|
||||||
|
let sftp_new_folder_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("new_folder").to_string()));
|
||||||
|
|
||||||
let _subscriptions = vec![
|
let _subscriptions = vec![
|
||||||
cx.subscribe_in(&host_input, window, Self::on_input_event),
|
cx.subscribe_in(&host_input, window, Self::on_input_event),
|
||||||
@@ -245,6 +249,7 @@ impl Ashell {
|
|||||||
cx.subscribe_in(&key_path_input, window, Self::on_input_event),
|
cx.subscribe_in(&key_path_input, window, Self::on_input_event),
|
||||||
cx.subscribe_in(&key_inline_input, window, Self::on_input_event),
|
cx.subscribe_in(&key_inline_input, window, Self::on_input_event),
|
||||||
cx.subscribe_in(&sftp_path_input, window, Self::on_input_event),
|
cx.subscribe_in(&sftp_path_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&sftp_new_folder_input, window, Self::on_input_event),
|
||||||
];
|
];
|
||||||
|
|
||||||
let (events_tx, events_rx) = mpsc::channel();
|
let (events_tx, events_rx) = mpsc::channel();
|
||||||
@@ -330,6 +335,9 @@ impl Ashell {
|
|||||||
connection_progress: None,
|
connection_progress: None,
|
||||||
pending_sftp_path_sync: Some("/".into()),
|
pending_sftp_path_sync: Some("/".into()),
|
||||||
sftp_context_menu: None,
|
sftp_context_menu: None,
|
||||||
|
sftp_creating_folder: false,
|
||||||
|
sftp_new_folder_input,
|
||||||
|
sftp_delete_scroll_handle: gpui::ScrollHandle::new(),
|
||||||
show_hidden_files: false,
|
show_hidden_files: false,
|
||||||
transfers: config.transfers(),
|
transfers: config.transfers(),
|
||||||
show_transfers_dialog: false,
|
show_transfers_dialog: false,
|
||||||
@@ -377,6 +385,28 @@ impl Ashell {
|
|||||||
window.prevent_default();
|
window.prevent_default();
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
}
|
}
|
||||||
|
} else if input == &self.sftp_new_folder_input {
|
||||||
|
match event {
|
||||||
|
InputEvent::PressEnter { .. } => {
|
||||||
|
let name = self.sftp_new_folder_input.read(cx).text().to_string();
|
||||||
|
if !name.is_empty() {
|
||||||
|
let base_path = self.sftp_path_input.read(cx).text().to_string();
|
||||||
|
let path = crate::sftp::join_remote(&base_path, &name);
|
||||||
|
if let Some(id) = self.active_tab.clone() {
|
||||||
|
if let Some(handle) = self.sftp_handles.get(&id) {
|
||||||
|
let _ = handle.commands.send(crate::sftp::SftpCommand::CreateDir(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.sftp_creating_folder = false;
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
InputEvent::Blur => {
|
||||||
|
self.sftp_creating_folder = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -517,6 +547,13 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
self.status = reason.into();
|
self.status = reason.into();
|
||||||
}
|
}
|
||||||
|
BackendEvent::SftpHome { tab_id, home } => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
if let Some(sftp) = tab.sftp.as_mut() {
|
||||||
|
sftp.home_dir = home;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BackendEvent::TerminalTitleChanged { tab_id, title } => {
|
BackendEvent::TerminalTitleChanged { tab_id, title } => {
|
||||||
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
if let Some(sftp) = &tab.sftp {
|
if let Some(sftp) = &tab.sftp {
|
||||||
@@ -527,7 +564,14 @@ impl Ashell {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if potential_path.starts_with('/') || potential_path.starts_with('~') {
|
if potential_path.starts_with('/') || potential_path.starts_with('~') {
|
||||||
let path_to_sync = potential_path.to_string();
|
let path_to_sync = if potential_path == "~" {
|
||||||
|
sftp.home_dir.clone()
|
||||||
|
} else if let Some(rest) = potential_path.strip_prefix("~/") {
|
||||||
|
crate::sftp::join_remote(&sftp.home_dir, rest)
|
||||||
|
} else {
|
||||||
|
potential_path.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
if path_to_sync != sftp.current_path {
|
if path_to_sync != sftp.current_path {
|
||||||
if let Some(handle) = self.sftp_handles.get(&tab_id) {
|
if let Some(handle) = self.sftp_handles.get(&tab_id) {
|
||||||
handle.list_dir(path_to_sync);
|
handle.list_dir(path_to_sync);
|
||||||
@@ -1385,6 +1429,177 @@ impl Ashell {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(id) = this.active_tab.clone() {
|
||||||
|
if let Some(handle) = this.sftp_handles.get(&id) {
|
||||||
|
let _ = handle.commands.send(crate::sftp::SftpCommand::DeletePaths(paths_to_delete.clone()));
|
||||||
|
}
|
||||||
|
if let Some(tab) = this.tabs.iter_mut().find(|t| t.id == id) {
|
||||||
|
if let Some(sftp) = tab.sftp.as_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(id) = this.active_tab.clone() {
|
||||||
|
if let Some(handle) = this.sftp_handles.get(&id) {
|
||||||
|
let _ = handle.commands.send(crate::sftp::SftpCommand::DeletePaths(paths_to_delete.clone()));
|
||||||
|
}
|
||||||
|
if let Some(tab) = this.tabs.iter_mut().find(|t| t.id == id) {
|
||||||
|
if let Some(sftp) = tab.sftp.as_mut() {
|
||||||
|
sftp.selected_entries.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
window.close_dialog(cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn show_settings_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn show_settings_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let view = cx.entity();
|
let view = cx.entity();
|
||||||
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
|
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
|
||||||
@@ -3254,6 +3469,36 @@ impl Ashell {
|
|||||||
.label(t!("refresh").to_string())
|
.label(t!("refresh").to_string())
|
||||||
.on_click(cx.listener(|this, _, _, cx| this.refresh_sftp(cx))),
|
.on_click(cx.listener(|this, _, _, cx| this.refresh_sftp(cx))),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("sftp-new-folder")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.icon(IconName::Folder)
|
||||||
|
.label(t!("new_folder").to_string())
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.sftp_creating_folder = true;
|
||||||
|
this.sftp_new_folder_input.update(cx, |input, cx| {
|
||||||
|
input.set_value("", window, cx);
|
||||||
|
input.focus_handle(cx).focus(window, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("sftp-delete-selected")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.label(if selected_entries.is_empty() {
|
||||||
|
t!("delete_selected").to_string()
|
||||||
|
} else {
|
||||||
|
format!("{} ({})", t!("delete_selected").to_string(), selected_entries.len())
|
||||||
|
})
|
||||||
|
.disabled(selected_entries.is_empty())
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.show_delete_confirm_dialog(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("sftp-upload-file")
|
Button::new("sftp-upload-file")
|
||||||
.ghost()
|
.ghost()
|
||||||
@@ -3454,14 +3699,48 @@ impl Ashell {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.relative()
|
.relative()
|
||||||
.child(
|
.child(
|
||||||
uniform_list("remote-files-list", entries.len(), {
|
uniform_list("remote-files-list", entries.len() + if self.sftp_creating_folder { 1 } else { 0 }, {
|
||||||
let entries = entries.clone();
|
let entries = entries.clone();
|
||||||
let selected_path = selected_path.clone();
|
let selected_path = selected_path.clone();
|
||||||
let view = view.clone();
|
let view = view.clone();
|
||||||
|
let creating_folder = self.sftp_creating_folder;
|
||||||
|
let new_folder_input = self.sftp_new_folder_input.clone();
|
||||||
move |visible_range, window, cx| {
|
move |visible_range, window, cx| {
|
||||||
visible_range
|
visible_range
|
||||||
.map(|ix| {
|
.map(|ix| {
|
||||||
let entry = entries[ix].clone();
|
if creating_folder && ix == 0 {
|
||||||
|
return h_flex()
|
||||||
|
.w_full()
|
||||||
|
.h(px(28.))
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.px_3()
|
||||||
|
.bg(cx.theme().tab_active)
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().border.opacity(0.35))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w(px(24.))
|
||||||
|
.flex_none()
|
||||||
|
.items_center()
|
||||||
|
.justify_center(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w(px(24.))
|
||||||
|
.flex_none()
|
||||||
|
.text_size(rems(1.0))
|
||||||
|
.child("📁"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
gpui_component::input::Input::new(&new_folder_input)
|
||||||
|
.flex_1()
|
||||||
|
)
|
||||||
|
.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual_ix = if creating_folder { ix - 1 } else { ix };
|
||||||
|
let entry = entries[actual_ix].clone();
|
||||||
let left_row = entry.clone();
|
let left_row = entry.clone();
|
||||||
let right_row = entry.clone();
|
let right_row = entry.clone();
|
||||||
let remote_path = entry.full_path.clone();
|
let remote_path = entry.full_path.clone();
|
||||||
|
|||||||
+141
-1
@@ -61,6 +61,8 @@ pub enum SftpCommand {
|
|||||||
EditFile {
|
EditFile {
|
||||||
remote_path: String,
|
remote_path: String,
|
||||||
},
|
},
|
||||||
|
CreateDir(String),
|
||||||
|
DeletePaths(Vec<String>),
|
||||||
UploadEditedFile {
|
UploadEditedFile {
|
||||||
local_path: String,
|
local_path: String,
|
||||||
remote_path: String,
|
remote_path: String,
|
||||||
@@ -254,6 +256,12 @@ async fn run_sftp(
|
|||||||
.canonicalize(".")
|
.canonicalize(".")
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| "/".to_string());
|
.unwrap_or_else(|_| "/".to_string());
|
||||||
|
|
||||||
|
let _ = events.send(BackendEvent::SftpHome {
|
||||||
|
tab_id: tab_id.clone(),
|
||||||
|
home: home.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
emit_entries(&events, &tab_id, &sftp, &home).await?;
|
emit_entries(&events, &tab_id, &sftp, &home).await?;
|
||||||
|
|
||||||
let mut active_transfers: std::collections::HashMap<String, TransferStateFlag> =
|
let mut active_transfers: std::collections::HashMap<String, TransferStateFlag> =
|
||||||
@@ -624,6 +632,85 @@ async fn run_sftp(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
SftpCommand::CreateDir(path) => {
|
||||||
|
let actual_path = if path == "~" {
|
||||||
|
home.clone()
|
||||||
|
} else if let Some(rest) = path.strip_prefix("~/") {
|
||||||
|
crate::sftp::join_remote(&home, rest)
|
||||||
|
} else {
|
||||||
|
path.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
match sftp.create_dir(&actual_path).await {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = events.send(BackendEvent::SftpStatus {
|
||||||
|
tab_id: tab_id.clone(),
|
||||||
|
text: t!("create_folder_success", name = base_name(&actual_path)).to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-fetch the parent directory to show the newly created folder
|
||||||
|
if let Some(parent) = parent_dir(&actual_path) {
|
||||||
|
let _ = commands_tx.send(SftpCommand::ListDir(parent));
|
||||||
|
} else {
|
||||||
|
let _ = commands_tx.send(SftpCommand::ListDir("/".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = events.send(BackendEvent::SftpStatus {
|
||||||
|
tab_id: tab_id.clone(),
|
||||||
|
text: t!("create_folder_failed", err = format!("{err:#}")).to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SftpCommand::DeletePaths(paths) => {
|
||||||
|
let _ = events.send(BackendEvent::SftpStatus {
|
||||||
|
tab_id: tab_id.clone(),
|
||||||
|
text: t!("deleting_paths", count = paths.len()).to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for path in paths.clone() {
|
||||||
|
let actual_path = if path == "~" {
|
||||||
|
home.clone()
|
||||||
|
} else if let Some(rest) = path.strip_prefix("~/") {
|
||||||
|
crate::sftp::join_remote(&home, rest)
|
||||||
|
} else {
|
||||||
|
path.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = recursive_delete(&sftp, actual_path).await {
|
||||||
|
errors.push(format!("{path}: {e:#}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
let _ = events.send(BackendEvent::SftpStatus {
|
||||||
|
tab_id: tab_id.clone(),
|
||||||
|
text: t!("delete_success", count = paths.len()).to_string(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let _ = events.send(BackendEvent::SftpStatus {
|
||||||
|
tab_id: tab_id.clone(),
|
||||||
|
text: t!("delete_failed", err = errors.join(", ")).to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(first) = paths.first() {
|
||||||
|
let actual_path = if first == "~" {
|
||||||
|
home.clone()
|
||||||
|
} else if let Some(rest) = first.strip_prefix("~/") {
|
||||||
|
crate::sftp::join_remote(&home, rest)
|
||||||
|
} else {
|
||||||
|
first.clone()
|
||||||
|
};
|
||||||
|
if let Some(parent) = parent_dir(&actual_path) {
|
||||||
|
let _ = commands_tx.send(SftpCommand::ListDir(parent));
|
||||||
|
} else {
|
||||||
|
let _ = commands_tx.send(SftpCommand::ListDir("/".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,6 +720,43 @@ async fn run_sftp(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
fn recursive_delete<'a>(
|
||||||
|
sftp: &'a SftpSession,
|
||||||
|
path: String,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
match sftp.read_dir(&path).await {
|
||||||
|
Ok(entries) => {
|
||||||
|
for entry in entries {
|
||||||
|
let name = entry.file_name();
|
||||||
|
if name == "." || name == ".." {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let child_path = crate::sftp::join_remote(&path, &name);
|
||||||
|
|
||||||
|
let meta = entry.metadata();
|
||||||
|
let permissions = meta.permissions.unwrap_or(0);
|
||||||
|
let is_dir = (permissions & 0o170_000) == 0o040_000;
|
||||||
|
|
||||||
|
if is_dir {
|
||||||
|
recursive_delete(sftp, child_path).await?;
|
||||||
|
} else {
|
||||||
|
sftp.remove_file(&child_path).await.with_context(|| format!("Failed to delete file {child_path}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sftp.remove_dir(&path).await.with_context(|| format!("Failed to delete dir {path}"))?;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
sftp.remove_file(&path).await.with_context(|| format!("Failed to delete {path}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn emit_entries(
|
async fn emit_entries(
|
||||||
events: &std::sync::mpsc::Sender<BackendEvent>,
|
events: &std::sync::mpsc::Sender<BackendEvent>,
|
||||||
tab_id: &str,
|
tab_id: &str,
|
||||||
@@ -773,7 +897,23 @@ fn base_name(path: &str) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn join_remote(parent: &str, child: &str) -> String {
|
pub(crate) fn parent_dir(path: &str) -> Option<String> {
|
||||||
|
if path == "/" || path.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let trimmed = path.trim_end_matches('/');
|
||||||
|
if let Some(idx) = trimmed.rfind('/') {
|
||||||
|
if idx == 0 {
|
||||||
|
Some("/".to_string())
|
||||||
|
} else {
|
||||||
|
Some(trimmed[..idx].to_string())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some("/".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn join_remote(parent: &str, child: &str) -> String {
|
||||||
if parent == "/" {
|
if parent == "/" {
|
||||||
format!("/{child}")
|
format!("/{child}")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ pub enum BackendEvent {
|
|||||||
tab_id: String,
|
tab_id: String,
|
||||||
reason: String,
|
reason: String,
|
||||||
},
|
},
|
||||||
|
SftpHome {
|
||||||
|
tab_id: String,
|
||||||
|
home: String,
|
||||||
|
},
|
||||||
TransferProgress {
|
TransferProgress {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
tab_id: String,
|
tab_id: String,
|
||||||
@@ -159,6 +163,7 @@ pub struct SftpUiState {
|
|||||||
pub selected_path: Option<String>,
|
pub selected_path: Option<String>,
|
||||||
pub preview: Option<PreviewData>,
|
pub preview: Option<PreviewData>,
|
||||||
pub selected_entries: std::collections::HashSet<String>,
|
pub selected_entries: std::collections::HashSet<String>,
|
||||||
|
pub home_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalTab {
|
impl TerminalTab {
|
||||||
@@ -204,6 +209,7 @@ impl TerminalTab {
|
|||||||
selected_path: None,
|
selected_path: None,
|
||||||
preview: None,
|
preview: None,
|
||||||
selected_entries: std::collections::HashSet::new(),
|
selected_entries: std::collections::HashSet::new(),
|
||||||
|
home_dir: "/".into(),
|
||||||
});
|
});
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user