mirror of
https://github.com/veeso/termscp.git
synced 2026-09-27 06:21:22 -07:00
Upgrade tuirealm (3.x -> 4.0.0), tui-realm-stdlib (3 -> 4), tui-term (0.2 -> 0.3). Apply all breaking changes from the 4.0 migration guide across the termscp UI. Key changes: - Root-level re-exports removed; imports moved to module-qualified paths (`tuirealm::application`, `::component`, `::event`, `::props`, `::state`, `::subscription`, `::listener`, `::ratatui`). Same for stdlib component types (`tui_realm_stdlib::components::*`). - `MockComponent` trait renamed to `Component`; old `Component` trait renamed to `AppComponent`. `#[derive(MockComponent)]` is now `#[derive(Component)]`. `Component::on` now takes `&Event<_>`. - `TextSpan` replaced with `SpanStatic`/`LineStatic`/`TextStatic` (ratatui-based); tuple `(String, Alignment)` titles replaced with the new `Title` builder; `Alignment` split into `HorizontalAlignment`/`VerticalAlignment`; stdlib components use `.alignment_horizontal` instead of `.alignment`. - `State::One`/`PropPayload::One` -> `Single`. `CmdResult::None` -> `NoChange`. `Props::get_or` removed; `Props::get` now returns a borrowed `Option<&AttrValue>` (call sites switched to `.and_then(AttrValue::as_*)`). `Component::query` returns `Option<QueryResult<'a>>`. - `Attribute::HighlightedColor` -> `HighlightStyle` (a full `Style`). `.highlighted_*` helpers renamed to `.highlight_*`. - `PollStrategy::UpTo(n)` now requires a `Duration`; tick timeout moved from `EventListenerCfg::poll_timeout` into `PollStrategy`. - `TerminalBridge` removed; `Context` now holds `CrosstermTerminalAdapter` directly and enables raw mode + alternate screen explicitly. The `TerminalAdapter` trait is imported where its methods are used. - `Update` trait removed; activity `update` methods are plain inherent functions. - `ProgressBar` replaced by stdlib `Gauge`. Paragraph `.wrap` renamed to `.wrap_trim`; `.text` now takes an `Into<Text>`. Stdlib `List` row items are now individual lines (`Vec<Span>` per row) rather than a `Table` of spans; custom `FileList`/`Log` convert between the two models. - Radio builders drop `.foreground(color)` so unselected items render with the terminal default foreground, and set `highlight_style(Style::default().fg(color).add_modifier(REVERSED))` so the selected entry is visibly highlighted only with the theme color. - Custom `FileList` keeps the selected row highlighted with the full highlight style when focused and falls back to a foreground-only style when unfocused. - Theme loading is now backwards compatible: `Theme` uses a custom `Deserialize` through an intermediate `ThemeFile` with optional fields, so missing keys, unknown values or legacy aliases (`transfer_progress_bar_full`/`_partial`) fall back to defaults on a per-field basis instead of failing the whole load.
163 lines
5.6 KiB
Rust
163 lines
5.6 KiB
Rust
//! ## FileTransferActivity
|
|
//!
|
|
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
|
|
|
|
// locals
|
|
// ext
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use tuirealm::terminal::TerminalAdapter;
|
|
|
|
use super::{File, FileTransferActivity, LogLevel, TransferPayload};
|
|
|
|
impl FileTransferActivity {
|
|
/// Open selected file(s) with default application
|
|
pub(crate) fn action_open(&mut self) {
|
|
let entries = self.get_selected_entries().get_files();
|
|
entries.iter().for_each(|x| self.open_file(x, None));
|
|
|
|
// clear queue
|
|
self.browser.explorer_mut().clear_queue();
|
|
self.reload_browser_file_list();
|
|
}
|
|
|
|
/// Open selected file(s) with provided application
|
|
pub(crate) fn action_open_with(&mut self, with: &str) {
|
|
let entries = self.get_selected_entries().get_files();
|
|
entries.iter().for_each(|x| self.open_file(x, Some(with)));
|
|
|
|
// clear queue
|
|
self.browser.explorer_mut().clear_queue();
|
|
self.reload_browser_file_list();
|
|
}
|
|
|
|
/// Open a file, dispatching based on whether the active pane is localhost.
|
|
pub(crate) fn open_file(&mut self, entry: &File, open_with: Option<&str>) {
|
|
if self.browser.fs_pane().fs.is_localhost() {
|
|
// Direct open from local path
|
|
self.open_path_with(entry.path(), open_with);
|
|
} else if self.is_local_tab() {
|
|
// Non-localhost host bridge: download via HostBridge API
|
|
self.open_bridged_file(entry, open_with);
|
|
} else {
|
|
// Remote: download via filetransfer_recv
|
|
self.action_open_remote_file(entry, open_with);
|
|
}
|
|
}
|
|
|
|
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
|
fn action_open_remote_file(&mut self, entry: &File, open_with: Option<&str>) {
|
|
// Download file
|
|
let tmpfile: String =
|
|
match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) {
|
|
None => {
|
|
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
|
return;
|
|
}
|
|
Some(p) => p,
|
|
};
|
|
let cache: PathBuf = match self.cache.as_ref() {
|
|
None => {
|
|
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
|
return;
|
|
}
|
|
Some(p) => p.path().to_path_buf(),
|
|
};
|
|
match self.filetransfer_recv(
|
|
TransferPayload::Any(entry.clone()),
|
|
cache.as_path(),
|
|
Some(tmpfile.clone()),
|
|
) {
|
|
Ok(_) => {
|
|
// Make file and open if file exists
|
|
let mut tmp: PathBuf = cache;
|
|
tmp.push(tmpfile.as_str());
|
|
if tmp.exists() {
|
|
self.open_path_with(tmp.as_path(), open_with);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
self.log(
|
|
LogLevel::Error,
|
|
format!("Failed to download remote entry: {err}"),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn open_bridged_file(&mut self, entry: &File, open_with: Option<&str>) {
|
|
// Download file
|
|
let tmpfile: String =
|
|
match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) {
|
|
None => {
|
|
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
|
return;
|
|
}
|
|
Some(p) => p,
|
|
};
|
|
let cache: PathBuf = match self.cache.as_ref() {
|
|
None => {
|
|
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
|
return;
|
|
}
|
|
Some(p) => p.path().to_path_buf(),
|
|
};
|
|
|
|
let tmpfile = cache.join(tmpfile);
|
|
|
|
// open from host bridge
|
|
let mut reader = match self.browser.local_pane_mut().fs.open_file(entry.path()) {
|
|
Ok(reader) => reader,
|
|
Err(err) => {
|
|
self.log(
|
|
LogLevel::Error,
|
|
format!("Failed to open bridged entry: {err}"),
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// write to file
|
|
let mut writer = match std::fs::File::create(tmpfile.as_path()) {
|
|
Ok(writer) => writer,
|
|
Err(err) => {
|
|
self.log(LogLevel::Error, format!("Failed to create file: {err}"));
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Err(err) = std::io::copy(&mut reader, &mut writer) {
|
|
self.log(LogLevel::Error, format!("Failed to write file: {err}"));
|
|
return;
|
|
}
|
|
|
|
if tmpfile.exists() {
|
|
self.open_path_with(tmpfile.as_path(), open_with);
|
|
}
|
|
}
|
|
|
|
/// Common function which opens a path with default or specified program.
|
|
fn open_path_with(&mut self, p: &Path, with: Option<&str>) {
|
|
// Open file
|
|
let result = match with {
|
|
None => open::that(p),
|
|
Some(with) => open::with(p, with),
|
|
};
|
|
// Log result
|
|
match result {
|
|
Ok(_) => self.log(LogLevel::Info, format!("Opened file `{}`", p.display())),
|
|
Err(err) => self.log(
|
|
LogLevel::Error,
|
|
format!("Failed to open file `{}`: {}", p.display(), err),
|
|
),
|
|
}
|
|
// NOTE: clear screen in order to prevent crap on stderr
|
|
if let Some(ctx) = self.context.as_mut() {
|
|
// Clear screen
|
|
if let Err(err) = ctx.terminal().clear_screen() {
|
|
error!("Could not clear screen screen: {}", err);
|
|
}
|
|
}
|
|
}
|
|
}
|