mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
396 lines
13 KiB
Rust
396 lines
13 KiB
Rust
use gpui::{Context, PathPromptOptions, Pixels, Point, Window};
|
|
|
|
use crate::{
|
|
Ashell, SftpContextMenuState,
|
|
sftp::{RemoteEntry, SftpHandle},
|
|
terminal,
|
|
};
|
|
|
|
pub(crate) fn is_editable_text_file(filename: &str) -> bool {
|
|
let lower = filename.to_lowercase();
|
|
let ext = std::path::Path::new(&lower)
|
|
.extension()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("");
|
|
let known_exts = [
|
|
"txt", "conf", "json", "yaml", "yml", "xml", "ini", "sh", "py", "rs", "js", "ts", "html",
|
|
"css", "md", "toml", "csv", "log", "cfg",
|
|
];
|
|
if known_exts.contains(&ext) {
|
|
return true;
|
|
}
|
|
let known_names = ["dockerfile", "makefile", ".gitignore", ".env"];
|
|
if known_names.contains(&lower.as_str()) {
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
impl Ashell {
|
|
pub(crate) fn active_sftp(&self) -> Option<&terminal::SftpUiState> {
|
|
self.active_group
|
|
.as_ref()
|
|
.and_then(|id| self.tab_groups.iter().find(|g| &g.id == id))
|
|
.and_then(|g| g.sftp.as_ref())
|
|
}
|
|
|
|
pub(crate) fn active_sftp_mut(&mut self) -> Option<&mut terminal::SftpUiState> {
|
|
let active_id = self.active_group.clone()?;
|
|
self.tab_groups
|
|
.iter_mut()
|
|
.find(|g| g.id == active_id)
|
|
.and_then(|g| g.sftp.as_mut())
|
|
}
|
|
|
|
pub(crate) fn active_sftp_handle(&self) -> Option<&SftpHandle> {
|
|
self.active_group
|
|
.as_ref()
|
|
.and_then(|id| self.sftp_handles.get(id))
|
|
}
|
|
|
|
pub(crate) fn navigate_sftp(&mut self, path: String, cx: &mut Context<Self>) {
|
|
if let Some(handle) = self.active_sftp_handle() {
|
|
tracing::info!("[sftp] navigating to directory: '{}'", path);
|
|
handle.list_dir(path.clone());
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
sftp.current_path = path;
|
|
self.pending_sftp_path_sync = Some(sftp.current_path.clone());
|
|
}
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn select_sftp_entry(&mut self, entry: RemoteEntry, cx: &mut Context<Self>) {
|
|
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() {
|
|
if !sftp.selected_entries.remove(&entry.full_path) {
|
|
sftp.selected_entries.insert(entry.full_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn mark_sftp_entry_selected(&mut self, path: &str, cx: &mut Context<Self>) {
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
sftp.selected_path = Some(path.to_string());
|
|
}
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn sftp_parent_path(path: &str) -> String {
|
|
if path == "/" {
|
|
return "/".to_string();
|
|
}
|
|
path.trim_end_matches('/')
|
|
.rsplit_once('/')
|
|
.map(|(parent, _)| {
|
|
if parent.is_empty() {
|
|
"/".to_string()
|
|
} else {
|
|
parent.to_string()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "/".to_string())
|
|
}
|
|
|
|
pub(crate) fn refresh_sftp(&mut self, cx: &mut Context<Self>) {
|
|
if let Some(path) = self.active_sftp().map(|sftp| sftp.current_path.clone()) {
|
|
self.navigate_sftp(path, cx);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn sync_sftp_path_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(path) = self.pending_sftp_path_sync.take() else {
|
|
return;
|
|
};
|
|
self.sftp_path_input.update(cx, |state, cx| {
|
|
state.set_value(path, window, cx);
|
|
});
|
|
}
|
|
|
|
pub(crate) fn open_sftp_context_menu(
|
|
&mut self,
|
|
remote_path: String,
|
|
is_dir: bool,
|
|
position: Point<Pixels>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.sftp_context_menu = Some(SftpContextMenuState {
|
|
remote_path,
|
|
is_dir,
|
|
position,
|
|
});
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn dismiss_sftp_context_menu(&mut self, cx: &mut Context<Self>) {
|
|
if self.sftp_context_menu.take().is_some() {
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn trigger_sftp_context_download(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(menu) = self.sftp_context_menu.take() else {
|
|
return;
|
|
};
|
|
self.download_sftp_entry(menu.remote_path, window, cx);
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn trigger_sftp_context_edit(&mut self, cx: &mut Context<Self>) {
|
|
let Some(menu) = self.sftp_context_menu.take() else {
|
|
return;
|
|
};
|
|
if let Some(handle) = self.active_sftp_handle() {
|
|
tracing::info!("[sftp] triggering edit for file: '{}'", menu.remote_path);
|
|
handle.edit_file(menu.remote_path);
|
|
}
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn download_sftp_entry(
|
|
&mut self,
|
|
remote_path: String,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
|
return;
|
|
};
|
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
|
files: false,
|
|
directories: true,
|
|
multiple: false,
|
|
prompt: Some("Select Download Folder".into()),
|
|
});
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
match path_prompt.await {
|
|
Ok(Ok(Some(mut paths))) => {
|
|
if let Some(folder) = paths.pop() {
|
|
let local_path = folder.to_string_lossy().to_string();
|
|
tracing::info!(
|
|
"[sftp] initiating download of '{}' to '{}'",
|
|
remote_path,
|
|
local_path
|
|
);
|
|
handle.download(remote_path, local_path);
|
|
this.update(cx, |this, cx| {
|
|
this.show_transfers_dialog = true;
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
}
|
|
Ok(Err(err)) => {
|
|
this.update(cx, |this, cx| {
|
|
this.status = format!("download picker failed: {err}").into();
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub(crate) fn upload_sftp_files(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
|
return;
|
|
};
|
|
let remote_dir = self
|
|
.active_sftp()
|
|
.map(|sftp| sftp.current_path.clone())
|
|
.unwrap_or_else(|| "/".into());
|
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
|
files: true,
|
|
directories: false,
|
|
multiple: false,
|
|
prompt: Some("Select File to Upload".into()),
|
|
});
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
match path_prompt.await {
|
|
Ok(Ok(Some(mut paths))) => {
|
|
if let Some(file) = paths.pop() {
|
|
let local_path = file.to_string_lossy().to_string();
|
|
tracing::info!(
|
|
"[sftp] initiating upload of file '{}' to '{}'",
|
|
local_path,
|
|
remote_dir
|
|
);
|
|
handle.upload_paths(vec![local_path], remote_dir);
|
|
this.update(cx, |this, cx| {
|
|
this.show_transfers_dialog = true;
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
}
|
|
Ok(Err(err)) => {
|
|
this.update(cx, |this, cx| {
|
|
this.status = format!("upload picker failed: {err}").into();
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub(crate) fn upload_sftp_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
|
return;
|
|
};
|
|
let remote_dir = self
|
|
.active_sftp()
|
|
.map(|sftp| sftp.current_path.clone())
|
|
.unwrap_or_else(|| "/".into());
|
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
|
files: false,
|
|
directories: true,
|
|
multiple: false,
|
|
prompt: Some("Select Folder to Upload".into()),
|
|
});
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
match path_prompt.await {
|
|
Ok(Ok(Some(mut paths))) => {
|
|
if let Some(folder) = paths.pop() {
|
|
let local_path = folder.to_string_lossy().to_string();
|
|
tracing::info!(
|
|
"[sftp] initiating upload of folder '{}' to '{}'",
|
|
local_path,
|
|
remote_dir
|
|
);
|
|
handle.upload_paths(vec![local_path], remote_dir);
|
|
this.update(cx, |this, cx| {
|
|
this.show_transfers_dialog = true;
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
}
|
|
Ok(Err(err)) => {
|
|
this.update(cx, |this, cx| {
|
|
this.status = format!("upload picker failed: {err}").into();
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub(crate) fn toggle_sftp_entry(
|
|
&mut self,
|
|
path: String,
|
|
checked: bool,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
if checked {
|
|
sftp.selected_entries.insert(path);
|
|
} else {
|
|
sftp.selected_entries.remove(&path);
|
|
}
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn toggle_all_sftp_entries(&mut self, checked: bool, cx: &mut Context<Self>) {
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
if checked {
|
|
let paths: Vec<String> = sftp.entries.iter().map(|e| e.full_path.clone()).collect();
|
|
for path in paths {
|
|
sftp.selected_entries.insert(path);
|
|
}
|
|
} else {
|
|
sftp.selected_entries.clear();
|
|
}
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn download_selected_sftp_entries(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(sftp) = self.active_sftp() else {
|
|
return;
|
|
};
|
|
let selected: Vec<String> = sftp.selected_entries.iter().cloned().collect();
|
|
if selected.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
|
return;
|
|
};
|
|
|
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
|
files: false,
|
|
directories: true,
|
|
multiple: false,
|
|
prompt: Some("Select Download Folder".into()),
|
|
});
|
|
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
if let Ok(Ok(Some(mut paths))) = path_prompt.await {
|
|
if let Some(folder) = paths.pop() {
|
|
let local_dir = folder.to_string_lossy().to_string();
|
|
tracing::info!(
|
|
"[sftp] initiating batch download of {} entries to '{}'",
|
|
selected.len(),
|
|
local_dir
|
|
);
|
|
for remote in selected {
|
|
let _ = handle.commands.send(crate::sftp::SftpCommand::Download {
|
|
remote,
|
|
local_dir: local_dir.clone(),
|
|
});
|
|
}
|
|
|
|
let _ = this.update(cx, |this, cx| {
|
|
if let Some(sftp_mut) = this.active_sftp_mut() {
|
|
sftp_mut.selected_entries.clear();
|
|
}
|
|
this.show_transfers_dialog = true;
|
|
cx.notify();
|
|
});
|
|
}
|
|
}
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub(crate) fn upload_sftp_files_batch(&mut self, paths: Vec<String>, cx: &mut Context<Self>) {
|
|
if paths.is_empty() {
|
|
return;
|
|
}
|
|
if let Some(sftp) = self.active_sftp() {
|
|
if let Some(handle) = self.active_sftp_handle() {
|
|
tracing::info!(
|
|
"[sftp] initiating batch upload of {} files to '{}'",
|
|
paths.len(),
|
|
sftp.current_path
|
|
);
|
|
let _ = handle.commands.send(crate::sftp::SftpCommand::UploadPaths {
|
|
locals: paths,
|
|
remote_dir: sftp.current_path.clone(),
|
|
});
|
|
self.show_transfers_dialog = true;
|
|
cx.notify();
|
|
}
|
|
}
|
|
}
|
|
}
|