Files
termscp/src/ui/activities/filetransfer/actions/open.rs
T
Christian Visintin 6252df2959 build: migrate to tui-realm 4.0
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.
2026-04-19 01:54:46 +05:30

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);
}
}
}
}