feat: it is now possible to cancel find command; show find progress (#284)

This commit is contained in:
Christian Visintin
2024-10-03 11:31:16 +02:00
committed by GitHub
parent b2a8a3041c
commit fc68e2621b
13 changed files with 229 additions and 137 deletions

View File

@@ -42,6 +42,7 @@
Released on
- [Issue 249](https://github.com/veeso/termscp/issues/249): The old *find* command has been replaced with a brand new explorer with support to 🪄 **Fuzzy search** 🪄. The command is still `<F>`.
- [Issue 283](https://github.com/veeso/termscp/issues/283): **Find command can now be cancelled** by pressing `<CTRL+C>`. While scanning the directory it will also display the current progress.
- [Issue 268](https://github.com/veeso/termscp/issues/268): 📦 **Pods and container explorer** 🐳 for Kube protocol.
- BREAKING ‼️ Kube address argument has changed to `namespace[@<cluster_url>][$<path>]`
- Pod and container argumets have been removed; from now on you will connect with the following syntax to the provided namespace: `/pod-name/container-name/path/to/file`

View File

@@ -16,7 +16,6 @@ use filetime::{self, FileTime};
use remotefs::fs::UnixPex;
use remotefs::fs::{File, FileType, Metadata};
use thiserror::Error;
use wildmatch::WildMatch;
// Locals
use crate::utils::path;
@@ -112,7 +111,7 @@ impl Localhost {
));
}
// Retrieve files for provided path
host.files = match host.scan_dir(host.wrkdir.as_path()) {
host.files = match host.list_dir(host.wrkdir.as_path()) {
Ok(files) => files,
Err(err) => {
error!(
@@ -131,12 +130,6 @@ impl Localhost {
self.wrkdir.clone()
}
/// List files in current directory
#[allow(dead_code)]
pub fn list_dir(&self) -> Vec<File> {
self.files.clone()
}
/// Change working directory with the new provided directory
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
let new_dir: PathBuf = self.to_path(new_dir);
@@ -164,7 +157,7 @@ impl Localhost {
// Change dir
self.wrkdir = new_dir;
// Scan new directory
self.files = match self.scan_dir(self.wrkdir.as_path()) {
self.files = match self.list_dir(self.wrkdir.as_path()) {
Ok(files) => files,
Err(err) => {
error!("Could not scan new directory: {}", err);
@@ -204,7 +197,7 @@ impl Localhost {
Ok(_) => {
// Update dir
if dir_name.is_relative() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
}
info!("Created directory {}", dir_path.display());
Ok(())
@@ -237,7 +230,7 @@ impl Localhost {
match std::fs::remove_dir_all(entry.path()) {
Ok(_) => {
// Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
info!("Removed directory {}", entry.path().display());
Ok(())
}
@@ -265,7 +258,7 @@ impl Localhost {
match std::fs::remove_file(entry.path()) {
Ok(_) => {
// Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
info!("Removed file {}", entry.path().display());
Ok(())
}
@@ -286,7 +279,7 @@ impl Localhost {
match std::fs::rename(entry.path(), dst_path) {
Ok(_) => {
// Scan dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
debug!(
"Moved file {} to {}",
entry.path().display(),
@@ -327,7 +320,7 @@ impl Localhost {
self.mkdir(dst.as_path())?;
}
// Scan dir
let dir_files: Vec<File> = self.scan_dir(entry.path())?;
let dir_files: Vec<File> = self.list_dir(entry.path())?;
// Iterate files
for dir_entry in dir_files.iter() {
// Calculate dst
@@ -362,11 +355,11 @@ impl Localhost {
match dst.is_dir() {
true => {
if dst == self.pwd().as_path() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
} else if let Some(parent) = dst.parent() {
// If parent is pwd, scan directory
if parent == self.pwd().as_path() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
}
}
}
@@ -374,7 +367,7 @@ impl Localhost {
if let Some(parent) = dst.parent() {
// If parent is pwd, scan directory
if parent == self.pwd().as_path() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
self.files = self.list_dir(self.wrkdir.as_path())?;
}
}
}
@@ -557,7 +550,7 @@ impl Localhost {
}
/// Get content of the current directory as a list of fs entry
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<File>, HostError> {
pub fn list_dir(&self, dir: &Path) -> Result<Vec<File>, HostError> {
info!("Reading directory {}", dir.display());
match std::fs::read_dir(dir) {
Ok(e) => {
@@ -579,12 +572,6 @@ impl Localhost {
}
}
/// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course.
/// The `search` argument supports wilcards ('*', '?')
pub fn find(&self, search: &str) -> Result<Vec<File>, HostError> {
self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search))
}
/// Create a symlink at path pointing at target
#[cfg(unix)]
pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> {
@@ -600,41 +587,6 @@ impl Localhost {
})
}
// -- privates
/// Recursive call for `find` method.
/// Search in current directory for files which match `filter`.
/// If a directory is found in current directory, `iter_search` will be called using that dir as argument.
fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result<Vec<File>, HostError> {
// Scan directory
let mut drained: Vec<File> = Vec::new();
match self.scan_dir(dir) {
Err(err) => Err(err),
Ok(entries) => {
// Iter entries
/* For each entry:
- if is dir: call iter_search with `dir`
- push `iter_search` result to `drained`
- if is file: check if it matches `filter`
- if it matches `filter`: push to to filter
*/
for entry in entries.into_iter() {
if entry.is_dir() {
// If directory matches; push directory to drained
let next_path = entry.path().to_path_buf();
if filter.matches(entry.name().as_str()) {
drained.push(entry);
}
drained.append(&mut self.iter_search(next_path.as_path(), filter)?);
} else if filter.matches(entry.name().as_str()) {
drained.push(entry);
}
}
Ok(drained)
}
}
}
/// Convert path to absolute path
fn to_path(&self, p: &Path) -> PathBuf {
path::absolutize(self.wrkdir.as_path(), p)
@@ -658,7 +610,7 @@ mod tests {
use super::*;
#[cfg(unix)]
use crate::utils::test_helpers::make_fsentry;
use crate::utils::test_helpers::{create_sample_file, make_dir_at, make_file_at};
use crate::utils::test_helpers::{create_sample_file, make_file_at};
#[test]
fn test_host_error_new() {
@@ -711,19 +663,6 @@ mod tests {
assert_eq!(host.pwd(), PathBuf::from("/dev"));
}
#[test]
#[cfg(unix)]
fn test_host_localhost_list_files() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Scan dir
let entries = std::fs::read_dir(PathBuf::from("/dev").as_path()).unwrap();
let mut counter: usize = 0;
for _ in entries {
counter += 1;
}
assert_eq!(host.list_dir().len(), counter);
}
#[test]
#[cfg(unix)]
fn test_host_localhost_change_dir() {
@@ -813,7 +752,7 @@ mod tests {
.is_ok());
// Get dir
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
// Verify files
let file_0: &File = files.get(0).unwrap();
if file_0.name() == *"foo.txt" {
@@ -841,10 +780,10 @@ mod tests {
fn test_host_localhost_mkdir() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 0); // There should be 0 files now
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
// Try to re-create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
@@ -868,17 +807,17 @@ mod tests {
// Create sample file
assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
// Remove file
assert!(host.remove(files.get(0).unwrap()).is_ok());
// There should be 0 files now
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 0); // There should be 0 files now
// Create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
// Delete directory
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
assert!(host.remove(files.get(0).unwrap()).is_ok());
// Remove unexisting directory
@@ -899,7 +838,7 @@ mod tests {
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str());
assert!(StdFile::create(src_path.as_path()).is_ok());
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 1 file now
assert_eq!(files.get(0).unwrap().name(), "foo.txt");
// Rename file
@@ -909,7 +848,7 @@ mod tests {
.rename(files.get(0).unwrap(), dst_path.as_path())
.is_ok());
// There should be still 1 file now, but named bar.txt
let files: Vec<File> = host.list_dir();
let files: Vec<File> = host.files.clone();
assert_eq!(files.len(), 1); // There should be 0 files now
assert_eq!(files.get(0).unwrap().name(), "bar.txt");
// Fail
@@ -1084,39 +1023,6 @@ mod tests {
assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5"));
}
#[test]
fn test_host_find() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let dir_path: &Path = tmpdir.path();
// Make files
assert!(make_file_at(dir_path, "pippo.txt").is_ok());
assert!(make_file_at(dir_path, "foo.jpg").is_ok());
// Make nested struct
assert!(make_dir_at(dir_path, "examples").is_ok());
let mut subdir: PathBuf = PathBuf::from(dir_path);
subdir.push("examples/");
assert!(make_file_at(subdir.as_path(), "omar.txt").is_ok());
assert!(make_file_at(subdir.as_path(), "errors.txt").is_ok());
assert!(make_file_at(subdir.as_path(), "screenshot.png").is_ok());
assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok());
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
// Find txt files
let mut result: Vec<File> = host.find("*.txt").ok().unwrap();
result.sort_by_key(|x: &File| x.name().to_lowercase());
// There should be 3 entries
assert_eq!(result.len(), 3);
// Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt)
assert_eq!(result[0].name(), "errors.txt");
assert_eq!(result[1].name(), "omar.txt");
assert_eq!(result[2].name(), "pippo.txt");
// Search for directory
let mut result: Vec<File> = host.find("examples*").ok().unwrap();
result.sort_by_key(|x: &File| x.name().to_lowercase());
assert_eq!(result.len(), 2);
assert_eq!(result[0].name(), "examples");
assert_eq!(result[1].name(), "examples.csv");
}
#[cfg(unix)]
#[test]
fn should_create_symlink() {

View File

@@ -9,20 +9,6 @@ use super::super::browser::FileExplorerTab;
use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload};
impl FileTransferActivity {
pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, String> {
match self.host.find("*") {
Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {err}")),
}
}
pub(crate) fn action_walkdir_remote(&mut self) -> Result<Vec<File>, String> {
match self.client.as_mut().find("*") {
Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {err}")),
}
}
pub(crate) fn action_find_changedir(&mut self) {
// Match entry
if let SelectedFile::One(entry) = self.get_found_selected_entries() {

View File

@@ -29,6 +29,7 @@ pub(crate) mod rename;
pub(crate) mod save;
pub(crate) mod submit;
pub(crate) mod symlink;
pub(crate) mod walkdir;
pub(crate) mod watcher;
#[derive(Debug)]

View File

@@ -0,0 +1,97 @@
//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
// locals
use std::path::{Path, PathBuf};
use super::{File, FileTransferActivity};
use crate::ui::activities::filetransfer::lib::walkdir::WalkdirStates;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WalkdirError {
Aborted,
Error(String),
}
impl FileTransferActivity {
pub(crate) fn action_walkdir_local(&mut self) -> Result<Vec<File>, WalkdirError> {
let mut acc = Vec::with_capacity(32_768);
self.walkdir(&mut acc, &self.host.pwd(), |activity, path| {
activity.host.list_dir(path).map_err(|e| e.to_string())
})?;
Ok(acc)
}
pub(crate) fn action_walkdir_remote(&mut self) -> Result<Vec<File>, WalkdirError> {
let mut acc = Vec::with_capacity(32_768);
let pwd = self
.client
.pwd()
.map_err(|e| WalkdirError::Error(e.to_string()))?;
self.walkdir(&mut acc, &pwd, |activity, path| {
activity.client.list_dir(path).map_err(|e| e.to_string())
})?;
Ok(acc)
}
fn walkdir<F>(
&mut self,
acc: &mut Vec<File>,
path: &Path,
list_dir_fn: F,
) -> Result<(), WalkdirError>
where
F: Fn(&mut Self, &Path) -> Result<Vec<File>, String> + Copy,
{
// init acc if empty
if acc.is_empty() {
self.init_walkdir();
}
// list current directory
let dir_entries = list_dir_fn(self, path).map_err(WalkdirError::Error)?;
// get dirs to scan later
let dirs = dir_entries
.iter()
.filter(|entry| entry.is_dir())
.map(|entry| entry.path.clone())
.collect::<Vec<PathBuf>>();
// extend acc
acc.extend(dir_entries.clone());
// update view
self.update_walkdir_entries(acc.len());
// check aborted
self.check_aborted()?;
for dir in dirs {
self.walkdir(acc, &dir, list_dir_fn)?;
}
Ok(())
}
fn check_aborted(&mut self) -> Result<(), WalkdirError> {
// read events
self.tick();
// check if the user wants to abort
if self.walkdir.aborted {
return Err(WalkdirError::Aborted);
}
Ok(())
}
fn init_walkdir(&mut self) {
self.walkdir = WalkdirStates::default();
}
}

View File

@@ -20,7 +20,8 @@ pub use popups::{
FileInfoPopup, FilterPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
WatcherPopup,
};
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};

View File

@@ -1897,6 +1897,47 @@ impl Component<Msg, NoUserEvent> for WaitPopup {
}
}
#[derive(MockComponent)]
pub struct WalkdirWaitPopup {
component: Paragraph,
}
impl WalkdirWaitPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self {
component: Paragraph::default()
.alignment(Alignment::Center)
.borders(
Borders::default()
.color(color)
.modifiers(BorderType::Rounded),
)
.foreground(color)
.text(&[
TextSpan::from(text.as_ref()),
TextSpan::from("Press 'CTRL+C' to abort"),
])
.wrap(true),
}
}
}
impl Component<Msg, NoUserEvent> for WalkdirWaitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
if matches!(
ev,
Event::Keyboard(KeyEvent {
code: Key::Char('c'),
modifiers: KeyModifiers::CONTROL
})
) {
Some(Msg::Transfer(TransferMsg::AbortWalkdir))
} else {
None
}
}
}
#[derive(MockComponent)]
pub struct WatchedPathsList {
component: List,

View File

@@ -4,3 +4,4 @@
pub(crate) mod browser;
pub(crate) mod transfer;
pub(crate) mod walkdir;

View File

@@ -0,0 +1,4 @@
#[derive(Debug, Default)]
pub struct WalkdirStates {
pub aborted: bool,
}

View File

@@ -21,6 +21,7 @@ use chrono::{DateTime, Local};
use lib::browser;
use lib::browser::Browser;
use lib::transfer::{TransferOpts, TransferStates};
use lib::walkdir::WalkdirStates;
use remotefs::RemoteFs;
use session::TransferPayload;
use tempfile::TempDir;
@@ -93,6 +94,7 @@ enum PendingActionMsg {
#[derive(Debug, PartialEq)]
enum TransferMsg {
AbortWalkdir,
AbortTransfer,
Chmod(remotefs::fs::UnixPex),
CopyFileTo(String),
@@ -217,6 +219,9 @@ pub struct FileTransferActivity {
browser: Browser,
/// Current log lines
log_records: VecDeque<LogRecord>,
/// Fuzzy search states
walkdir: WalkdirStates,
/// Transfer states
transfer: TransferStates,
/// Temporary directory where to store temporary stuff
cache: Option<TempDir>,
@@ -244,6 +249,7 @@ impl FileTransferActivity {
client: Builder::build(params.protocol, params.params.clone(), &config_client),
browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
walkdir: WalkdirStates::default(),
transfer: TransferStates::default(),
cache: match TempDir::new() {
Ok(d) => Some(d),

View File

@@ -174,7 +174,7 @@ impl FileTransferActivity {
/// Scan current local directory
fn local_scan(&mut self, path: &Path) -> Result<(), HostError> {
match self.host.scan_dir(path) {
match self.host.list_dir(path) {
Ok(files) => {
// Set files and sort (sorting is implicit)
self.local_mut().set_files(files);
@@ -358,7 +358,7 @@ impl FileTransferActivity {
}
}
// Get files in dir
match self.host.scan_dir(entry.path()) {
match self.host.list_dir(entry.path()) {
Ok(entries) => {
// Iterate over files
for entry in entries.iter() {
@@ -1156,7 +1156,7 @@ impl FileTransferActivity {
fn get_total_transfer_size_local(&mut self, entry: &File) -> usize {
if entry.is_dir() {
// List dir
match self.host.scan_dir(entry.path()) {
match self.host.list_dir(entry.path()) {
Ok(files) => files
.iter()
.map(|x| self.get_total_transfer_size_local(x))

View File

@@ -8,6 +8,7 @@ use remotefs::fs::File;
use tuirealm::props::{AttrValue, Attribute};
use tuirealm::{State, StateValue, Update};
use super::actions::walkdir::WalkdirError;
use super::actions::SelectedFile;
use super::browser::{FileExplorerTab, FoundExplorerTab};
use super::{ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg};
@@ -32,6 +33,9 @@ impl FileTransferActivity {
TransferMsg::AbortTransfer => {
self.transfer.abort();
}
TransferMsg::AbortWalkdir => {
self.walkdir.aborted = true;
}
TransferMsg::Chmod(mode) => {
self.umount_chmod();
self.mount_blocking_wait("Applying new file mode…");
@@ -213,9 +217,9 @@ impl FileTransferActivity {
}
TransferMsg::InitFuzzySearch => {
// Mount wait
self.mount_blocking_wait("Scanning current directory…");
self.mount_walkdir_wait();
// Find
let res: Result<Vec<File>, String> = match self.browser.tab() {
let res: Result<Vec<File>, WalkdirError> = match self.browser.tab() {
FileExplorerTab::Local => self.action_walkdir_local(),
FileExplorerTab::Remote => self.action_walkdir_remote(),
_ => panic!("Trying to search for files, while already in a find result"),
@@ -224,10 +228,13 @@ impl FileTransferActivity {
self.umount_wait();
// Match result
match res {
Err(err) => {
Err(WalkdirError::Error(err)) => {
// Mount error
self.mount_error(err.as_str());
}
Err(WalkdirError::Aborted) => {
self.mount_info("Search aborted");
}
Ok(files) if files.is_empty() => {
// If no file has been found notify user
self.mount_info("There are no files in the current directory");

View File

@@ -6,9 +6,10 @@
// Ext
use remotefs::fs::{File, UnixPex};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{PropPayload, PropValue, TextSpan};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear;
use tuirealm::{Sub, SubClause, SubEventClause};
use tuirealm::{AttrValue, Attribute, Sub, SubClause, SubEventClause};
use unicode_width::UnicodeWidthStr;
use super::browser::{FileExplorerTab, FoundExplorerTab};
@@ -302,7 +303,15 @@ impl FileTransferActivity {
// make popup
self.app.view(&Id::ErrorPopup, f, popup);
} else if self.app.mounted(&Id::WaitPopup) {
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.size());
let wait_popup_lines = self
.app
.query(&Id::WaitPopup, Attribute::Text)
.map(|x| x.map(|x| x.unwrap_payload().unwrap_vec().len()))
.unwrap_or_default()
.unwrap_or(1) as u16;
let popup =
Popup(Size::Percentage(50), Size::Unit(2 + wait_popup_lines)).draw_in(f.size());
f.render_widget(Clear, popup);
// make popup
self.app.view(&Id::WaitPopup, f, popup);
@@ -392,6 +401,38 @@ impl FileTransferActivity {
assert!(self.app.active(&Id::WaitPopup).is_ok());
}
pub(super) fn mount_walkdir_wait(&mut self) {
let color = self.theme().misc_info_dialog;
assert!(self
.app
.remount(
Id::WaitPopup,
Box::new(components::WalkdirWaitPopup::new(
"Scanning current directory…",
color
)),
vec![],
)
.is_ok());
assert!(self.app.active(&Id::WaitPopup).is_ok());
self.view();
}
pub(super) fn update_walkdir_entries(&mut self, entries: usize) {
let text = format!("Scanning current directory… ({entries} items found)",);
let _ = self.app.attr(
&Id::WaitPopup,
Attribute::Text,
AttrValue::Payload(PropPayload::Vec(vec![
PropValue::TextSpan(TextSpan::from(text)),
PropValue::TextSpan(TextSpan::from("Press 'CTRL+C' to abort")),
])),
);
self.view();
}
pub(super) fn mount_blocking_wait<S: AsRef<str>>(&mut self, text: S) {
self.mount_wait(text);
self.view();