mod overlay;
mod types;
mod upload;
pub(crate) use overlay::render_file_transfer_overlay;
pub(crate) use types::{
FileTransferState, PendingSave, PendingUpload, RecentTransfer, TransferInfo,
};
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use par_term_emu_core_rust::terminal::file_transfer::{
FileTransfer, TransferDirection, TransferStatus,
};
use super::window_state::WindowState;
use crate::config::DownloadSaveLocation;
const UPLOAD_CHUNK_SIZE: usize = 65536;
const SAVE_DIALOG_DELAY_MS: u64 = 750;
const RECENT_TRANSFER_DISPLAY_SECS: u64 = 3;
pub(super) fn format_bytes(bytes: usize) -> String {
if bytes < 1024 {
format!("{} B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.1} KB", bytes as f64 / 1024.0)
} else {
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
}
}
fn transfer_to_info(ft: &FileTransfer) -> TransferInfo {
let (bytes_transferred, total_bytes) = match &ft.status {
TransferStatus::InProgress {
bytes_transferred,
total_bytes,
} => (*bytes_transferred, *total_bytes),
TransferStatus::Completed => (ft.data.len(), Some(ft.data.len())),
_ => (0, None),
};
TransferInfo {
filename: if ft.filename.is_empty() {
format!("transfer-{}", ft.id)
} else {
ft.filename.clone()
},
direction: ft.direction,
bytes_transferred,
total_bytes,
}
}
impl WindowState {
pub(crate) fn check_file_transfers(&mut self) {
let tab = if let Some(t) = self.tab_manager.active_tab() {
t
} else {
return;
};
self.file_transfer_state.active_transfers.clear();
if let Ok(term) = tab.terminal.try_write() {
let active = term.get_active_transfers();
self.file_transfer_state
.active_transfers
.extend(active.iter().map(transfer_to_info));
let completed = term.get_completed_transfers();
let completed_ids: Vec<u64> = completed
.iter()
.filter(|ft| {
ft.direction == TransferDirection::Download
&& ft.status == TransferStatus::Completed
})
.map(|ft| ft.id)
.collect();
drop(term);
let terminal_arc = std::sync::Arc::clone(&tab.terminal);
for id in completed_ids {
if let Ok(term) = terminal_arc.try_write()
&& let Some(ft) = term.take_completed_transfer(id)
{
let filename = if ft.filename.is_empty() {
format!("download-{}", ft.id)
} else {
ft.filename.clone()
};
crate::debug_info!(
"FILE_TRANSFER",
"Download completed: {} ({} bytes)",
filename,
ft.data.len()
);
let size = ft.data.len();
self.file_transfer_state
.pending_saves
.push_back(PendingSave {
filename: filename.clone(),
data: ft.data,
});
self.file_transfer_state
.recent_transfers
.push(RecentTransfer {
filename: filename.clone(),
size,
direction: TransferDirection::Download,
completed_at: std::time::Instant::now(),
});
self.file_transfer_state.last_completion_time = Some(std::time::Instant::now());
self.deliver_notification(
"Download Received",
&format!("Received {} ({})", filename, format_bytes(size)),
);
}
}
if let Ok(term) = terminal_arc.try_write() {
let failed: Vec<(u64, String)> = term
.get_completed_transfers()
.iter()
.filter_map(|ft| {
if let TransferStatus::Failed(reason) = &ft.status {
Some((ft.id, reason.clone()))
} else {
None
}
})
.collect();
drop(term);
for (id, reason) in &failed {
if let Ok(term) = terminal_arc.try_write() {
let _ = term.take_completed_transfer(*id);
}
self.deliver_notification(
"File Transfer Failed",
&format!("Transfer failed: {}", reason),
);
self.file_transfer_state.last_completion_time = Some(std::time::Instant::now());
}
}
if let Ok(term) = terminal_arc.try_write() {
let upload_requests = term.poll_upload_requests();
for _format in upload_requests {
self.file_transfer_state
.pending_uploads
.push_back(PendingUpload {});
self.deliver_notification(
"Upload Requested",
"Remote application is requesting a file upload",
);
}
}
}
self.file_transfer_state.recent_transfers.retain(|t| {
t.completed_at.elapsed() < std::time::Duration::from_secs(RECENT_TRANSFER_DISPLAY_SECS)
});
if !self.file_transfer_state.recent_transfers.is_empty() {
self.request_redraw();
}
self.poll_active_uploads();
if !self.file_transfer_state.dialog_open {
let save_ready = self.file_transfer_state.pending_saves.front().is_some()
&& self
.file_transfer_state
.last_completion_time
.is_some_and(|t| {
t.elapsed() >= std::time::Duration::from_millis(SAVE_DIALOG_DELAY_MS)
});
if save_ready {
if let Some(pending) = self.file_transfer_state.pending_saves.pop_front() {
self.process_save_dialog(pending);
let now = std::time::Instant::now();
for t in &mut self.file_transfer_state.recent_transfers {
t.completed_at = now;
}
self.file_transfer_state.last_completion_time = Some(now);
}
} else if self.file_transfer_state.pending_saves.front().is_some() {
self.request_redraw();
} else if let Some(pending) = self.file_transfer_state.pending_uploads.pop_front() {
self.process_upload_dialog(pending);
}
}
}
fn poll_active_uploads(&mut self) {
let mut completed_info: Vec<(String, usize, Option<String>)> = Vec::new();
self.file_transfer_state.active_uploads.retain(|upload| {
if upload.completed.load(Ordering::Relaxed) {
let error = upload.error.lock().take();
completed_info.push((upload.filename.clone(), upload.file_size, error));
false
} else {
true
}
});
for (filename, file_size, error) in completed_info {
if let Some(e) = error {
self.deliver_notification("Upload Failed", &e);
} else {
self.file_transfer_state
.recent_transfers
.push(RecentTransfer {
filename: filename.clone(),
size: file_size,
direction: TransferDirection::Upload,
completed_at: std::time::Instant::now(),
});
self.deliver_notification(
"Upload Complete",
&format!("Uploaded {} ({})", filename, format_bytes(file_size)),
);
}
self.file_transfer_state.last_completion_time = Some(std::time::Instant::now());
}
for upload in &self.file_transfer_state.active_uploads {
let wire_written = upload.bytes_written.load(Ordering::Relaxed);
let bytes_transferred = if upload.total_wire_bytes > 0 {
((wire_written as f64 / upload.total_wire_bytes as f64) * upload.file_size as f64)
as usize
} else {
0
};
self.file_transfer_state
.active_transfers
.push(TransferInfo {
filename: upload.filename.clone(),
direction: TransferDirection::Upload,
bytes_transferred,
total_bytes: Some(upload.file_size),
});
}
}
fn process_save_dialog(&mut self, pending: PendingSave) {
self.file_transfer_state.dialog_open = true;
let default_dir = self.resolve_download_directory();
let mut dialog = rfd::FileDialog::new().set_file_name(&pending.filename);
if let Some(dir) = &default_dir {
dialog = dialog.set_directory(dir);
}
let result = dialog.save_file();
self.file_transfer_state.dialog_open = false;
if let Some(path) = result {
match std::fs::write(&path, &pending.data) {
Ok(()) => {
let size_str = format_bytes(pending.data.len());
crate::debug_info!(
"FILE_TRANSFER",
"Saved download to: {} ({})",
path.display(),
size_str
);
self.deliver_notification(
"Download Saved",
&format!(
"Saved {} to {} ({})",
pending.filename,
path.display(),
size_str
),
);
if let Some(parent) = path.parent() {
self.config.last_download_directory =
Some(parent.to_string_lossy().to_string());
}
}
Err(e) => {
crate::debug_info!("FILE_TRANSFER", "Failed to save download: {}", e);
self.deliver_notification(
"Download Save Failed",
&format!("Failed to save {}: {}", pending.filename, e),
);
}
}
} else {
crate::debug_info!(
"FILE_TRANSFER",
"Save dialog cancelled for {}",
pending.filename
);
}
}
fn resolve_download_directory(&self) -> Option<PathBuf> {
match &self.config.download_save_location {
DownloadSaveLocation::Downloads => dirs::download_dir(),
DownloadSaveLocation::LastUsed => self
.config
.last_download_directory
.as_ref()
.map(PathBuf::from)
.or_else(dirs::download_dir),
DownloadSaveLocation::Cwd => {
if let Some(tab) = self.tab_manager.active_tab()
&& let Ok(term) = tab.terminal.try_write()
&& let Some(cwd) = term.shell_integration_cwd()
{
return Some(PathBuf::from(cwd));
}
dirs::download_dir()
}
DownloadSaveLocation::Custom(path) => {
let p = PathBuf::from(path);
if p.is_dir() {
Some(p)
} else {
dirs::download_dir()
}
}
}
}
}