mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 03:25:23 +00:00
feat(cli): add --select-1
cli flag to automatically select unique result
Adds a `--select-1` flag that enables tv to automatically select the only result and exit when such a case appears.
This commit is contained in:
parent
69c4dcc5c5
commit
64671b2b5b
24
man/tv.1
24
man/tv.1
@ -1,10 +1,10 @@
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.TH television 1 "television 0.11.0"
|
||||
.TH television 1 "television 0.11.5"
|
||||
.SH NAME
|
||||
television \- A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
|
||||
.SH SYNOPSIS
|
||||
\fBtelevision\fR [\fB\-p\fR|\fB\-\-preview\fR] [\fB\-\-no\-preview\fR] [\fB\-\-delimiter\fR] [\fB\-t\fR|\fB\-\-tick\-rate\fR] [\fB\-f\fR|\fB\-\-frame\-rate\fR] [\fB\-k\fR|\fB\-\-keybindings\fR] [\fB\-\-passthrough\-keybindings\fR] [\fB\-i\fR|\fB\-\-input\fR] [\fB\-\-autocomplete\-prompt\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICHANNEL\fR] [\fIPATH\fR] [\fIsubcommands\fR]
|
||||
\fBtelevision\fR [\fB\-p\fR|\fB\-\-preview\fR] [\fB\-\-no\-preview\fR] [\fB\-\-delimiter\fR] [\fB\-t\fR|\fB\-\-tick\-rate\fR] [\fB\-f\fR|\fB\-\-frame\-rate\fR] [\fB\-k\fR|\fB\-\-keybindings\fR] [\fB\-i\fR|\fB\-\-input\fR] [\fB\-\-autocomplete\-prompt\fR] [\fB\-\-select\-1\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICHANNEL\fR] [\fIPATH\fR] [\fIsubcommands\fR]
|
||||
.SH DESCRIPTION
|
||||
A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
|
||||
.SH OPTIONS
|
||||
@ -53,13 +53,6 @@ expressions using the configuration file formalism.
|
||||
|
||||
Example: `tv \-\-keybindings=\*(Aqquit="esc";select_next_entry=["down","ctrl\-j"]\*(Aq`
|
||||
.TP
|
||||
\fB\-\-passthrough\-keybindings\fR=\fISTRING\fR
|
||||
Passthrough keybindings (comma separated, e.g. "q,ctrl\-w,ctrl\-t")
|
||||
|
||||
These keybindings will trigger selection of the current entry and be
|
||||
passed through to stdout along with the entry to be handled by the
|
||||
parent process.
|
||||
.TP
|
||||
\fB\-i\fR, \fB\-\-input\fR=\fISTRING\fR
|
||||
Input text to pass to the channel to prefill the prompt.
|
||||
|
||||
@ -73,6 +66,17 @@ This can be used to automatically select a channel based on the input
|
||||
prompt by using the `shell_integration` mapping in the configuration
|
||||
file.
|
||||
.TP
|
||||
\fB\-\-select\-1\fR
|
||||
Automatically select and output the first entry if there is only one
|
||||
entry.
|
||||
|
||||
Note that most channels stream entries asynchronously which means that
|
||||
knowing if there\*(Aqs only one entry will require waiting for the channel
|
||||
to finish loading first.
|
||||
|
||||
For most channels and workloads this shouldn\*(Aqt be a problem since the
|
||||
loading times are usually very short and will go unnoticed by the user.
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
Print help (see a summary with \*(Aq\-h\*(Aq)
|
||||
.TP
|
||||
@ -104,6 +108,6 @@ Initializes shell completion ("tv init zsh")
|
||||
television\-help(1)
|
||||
Print this message or the help of the given subcommand(s)
|
||||
.SH VERSION
|
||||
v0.11.0
|
||||
v0.11.5
|
||||
.SH AUTHORS
|
||||
Alexandre Pasmantier <alex.pasmant@gmail.com>
|
||||
|
@ -5,7 +5,7 @@ use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::channels::entry::{Entry, PreviewType};
|
||||
use crate::channels::TelevisionChannel;
|
||||
use crate::channels::{OnAir, TelevisionChannel};
|
||||
use crate::config::Config;
|
||||
use crate::keymap::Keymap;
|
||||
use crate::render::UiState;
|
||||
@ -53,10 +53,13 @@ pub struct App {
|
||||
ui_state_tx: mpsc::UnboundedSender<UiState>,
|
||||
/// Render task handle
|
||||
render_task: Option<tokio::task::JoinHandle<Result<()>>>,
|
||||
/// Whether the application should automatically select the first entry if there is only one
|
||||
/// entry available.
|
||||
select_1: bool,
|
||||
}
|
||||
|
||||
/// The outcome of an action.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ActionOutcome {
|
||||
Entries(FxHashSet<Entry>),
|
||||
Input(String),
|
||||
@ -96,6 +99,7 @@ impl App {
|
||||
channel: TelevisionChannel,
|
||||
config: Config,
|
||||
input: Option<String>,
|
||||
select_1: bool,
|
||||
) -> Self {
|
||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||
@ -124,6 +128,7 @@ impl App {
|
||||
ui_state_rx,
|
||||
ui_state_tx,
|
||||
render_task: None,
|
||||
select_1,
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,6 +151,7 @@ impl App {
|
||||
is_output_tty: bool,
|
||||
headless: bool,
|
||||
) -> Result<AppOutput> {
|
||||
// Event loop
|
||||
if !headless {
|
||||
debug!("Starting backend event loop");
|
||||
let event_loop = EventLoop::new(self.tick_rate, true);
|
||||
@ -169,11 +175,13 @@ impl App {
|
||||
.expect("Unable to send init render action.");
|
||||
}
|
||||
|
||||
// event handling loop
|
||||
// Main loop
|
||||
debug!("Starting event handling loop");
|
||||
let action_tx = self.action_tx.clone();
|
||||
let mut event_buf = Vec::with_capacity(EVENT_BUF_SIZE);
|
||||
let mut action_buf = Vec::with_capacity(ACTION_BUF_SIZE);
|
||||
let mut action_outcome;
|
||||
|
||||
loop {
|
||||
// handle event and convert to action
|
||||
if self
|
||||
@ -192,7 +200,18 @@ impl App {
|
||||
}
|
||||
}
|
||||
// It's important that this shouldn't block if no actions are available
|
||||
let action_outcome = self.handle_actions(&mut action_buf).await?;
|
||||
action_outcome = self.handle_actions(&mut action_buf).await?;
|
||||
|
||||
// If `self.select_1` is true, the channel is not running, and there is
|
||||
// only one entry available, automatically select the first entry.
|
||||
if self.select_1
|
||||
&& !self.television.channel.running()
|
||||
&& self.television.channel.total_count() == 1
|
||||
{
|
||||
if let Some(outcome) = self.maybe_select_1() {
|
||||
action_outcome = outcome;
|
||||
}
|
||||
}
|
||||
|
||||
if self.should_quit {
|
||||
// send a termination signal to the event loop
|
||||
@ -349,4 +368,52 @@ impl App {
|
||||
}
|
||||
Ok(ActionOutcome::None)
|
||||
}
|
||||
|
||||
/// Maybe select the first entry if there is only one entry available.
|
||||
fn maybe_select_1(&mut self) -> Option<ActionOutcome> {
|
||||
debug!("Automatically selecting the first entry");
|
||||
if let Some(unique_entry) =
|
||||
self.television.results_picker.entries.first()
|
||||
{
|
||||
self.should_quit = true;
|
||||
|
||||
if !self.render_tx.is_closed() {
|
||||
let _ = self.render_tx.send(RenderingTask::Quit);
|
||||
}
|
||||
|
||||
return Some(ActionOutcome::Entries(FxHashSet::from_iter([
|
||||
unique_entry.clone(),
|
||||
])));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::channels::stdin::Channel as StdinChannel;
|
||||
|
||||
#[test]
|
||||
fn test_maybe_select_1() {
|
||||
let mut app = App::new(
|
||||
TelevisionChannel::Stdin(StdinChannel::new(PreviewType::None)),
|
||||
Config::default(),
|
||||
None,
|
||||
true,
|
||||
);
|
||||
app.television
|
||||
.results_picker
|
||||
.entries
|
||||
.push(Entry::new("test".to_string(), PreviewType::None));
|
||||
let outcome = app.maybe_select_1();
|
||||
assert!(outcome.is_some());
|
||||
assert_eq!(
|
||||
outcome.unwrap(),
|
||||
ActionOutcome::Entries(FxHashSet::from_iter([Entry::new(
|
||||
"test".to_string(),
|
||||
PreviewType::None
|
||||
)]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ pub struct Channel {
|
||||
matcher: Matcher<Alias>,
|
||||
file_icon: FileIcon,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
crawl_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
const NUM_THREADS: usize = 1;
|
||||
@ -44,6 +45,7 @@ fn get_raw_aliases(shell: &str) -> Vec<String> {
|
||||
.arg("alias")
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
let aliases = String::from_utf8_lossy(&output.stdout);
|
||||
aliases.lines().map(ToString::to_string).collect()
|
||||
}
|
||||
@ -52,12 +54,13 @@ impl Channel {
|
||||
pub fn new() -> Self {
|
||||
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
|
||||
let injector = matcher.injector();
|
||||
tokio::spawn(load_aliases(injector));
|
||||
let crawl_handle = tokio::spawn(load_aliases(injector));
|
||||
|
||||
Self {
|
||||
matcher,
|
||||
file_icon: FileIcon::from(FILE_ICON_STR),
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
crawl_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,7 +139,7 @@ impl OnAir for Channel {
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {}
|
||||
|
@ -33,6 +33,7 @@ pub struct Channel {
|
||||
entries_command: String,
|
||||
preview_kind: PreviewKind,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
crawl_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Default for Channel {
|
||||
@ -82,7 +83,10 @@ impl Channel {
|
||||
) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
tokio::spawn(load_candidates(entries_command.to_string(), injector));
|
||||
let crawl_handle = tokio::spawn(load_candidates(
|
||||
entries_command.to_string(),
|
||||
injector,
|
||||
));
|
||||
let preview_kind = match preview_command {
|
||||
Some(command) => {
|
||||
parse_preview_kind(&command).unwrap_or_else(|_| {
|
||||
@ -98,6 +102,7 @@ impl Channel {
|
||||
preview_kind,
|
||||
name: name.to_string(),
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
crawl_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -209,7 +214,7 @@ impl OnAir for Channel {
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {}
|
||||
|
@ -136,7 +136,7 @@ impl OnAir for Channel {
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
|
@ -142,7 +142,7 @@ impl OnAir for Channel {
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
|
@ -106,7 +106,7 @@ impl OnAir for Channel {
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
|
@ -15,6 +15,7 @@ pub struct Channel {
|
||||
matcher: Matcher<String>,
|
||||
preview_type: PreviewType,
|
||||
selected_entries: FxHashSet<Entry>,
|
||||
instream_handle: std::thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
@ -22,12 +23,13 @@ impl Channel {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
|
||||
spawn(move || stream_from_stdin(&injector));
|
||||
let instream_handle = spawn(move || stream_from_stdin(&injector));
|
||||
|
||||
Self {
|
||||
matcher,
|
||||
preview_type,
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
instream_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,6 +40,33 @@ impl Default for Channel {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<E> for Channel
|
||||
where
|
||||
E: AsRef<Vec<String>>,
|
||||
{
|
||||
fn from(entries: E) -> Self {
|
||||
let matcher = Matcher::new(Config::default());
|
||||
let injector = matcher.injector();
|
||||
|
||||
let entries = entries.as_ref().clone();
|
||||
|
||||
let instream_handle = spawn(move || {
|
||||
for entry in entries {
|
||||
injector.push(entry.clone(), |e, cols| {
|
||||
cols[0] = e.to_string().into();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
matcher,
|
||||
preview_type: PreviewType::default(),
|
||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||
instream_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
fn stream_from_stdin(injector: &Injector<String>) {
|
||||
@ -116,7 +145,7 @@ impl OnAir for Channel {
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
self.matcher.status.running || !self.instream_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {}
|
||||
|
@ -239,7 +239,7 @@ impl OnAir for Channel {
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.matcher.status.running
|
||||
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
|
@ -90,6 +90,18 @@ pub struct Cli {
|
||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
|
||||
/// Automatically select and output the first entry if there is only one
|
||||
/// entry.
|
||||
///
|
||||
/// Note that most channels stream entries asynchronously which means that
|
||||
/// knowing if there's only one entry will require waiting for the channel
|
||||
/// to finish loading first.
|
||||
///
|
||||
/// For most channels and workloads this shouldn't be a problem since the
|
||||
/// loading times are usually very short and will go unnoticed by the user.
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
pub select_1: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ pub struct PostProcessedCli {
|
||||
pub working_directory: Option<String>,
|
||||
pub autocomplete_prompt: Option<String>,
|
||||
pub keybindings: Option<KeyBindings>,
|
||||
pub select_1: bool,
|
||||
}
|
||||
|
||||
impl Default for PostProcessedCli {
|
||||
@ -44,6 +45,7 @@ impl Default for PostProcessedCli {
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
keybindings: None,
|
||||
select_1: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -108,6 +110,7 @@ impl From<Cli> for PostProcessedCli {
|
||||
working_directory,
|
||||
autocomplete_prompt: cli.autocomplete_prompt,
|
||||
keybindings,
|
||||
select_1: cli.select_1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -322,6 +325,7 @@ mod tests {
|
||||
command: None,
|
||||
working_directory: Some("/home/user".to_string()),
|
||||
autocomplete_prompt: None,
|
||||
select_1: false,
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
@ -360,6 +364,7 @@ mod tests {
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
select_1: false,
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
@ -389,6 +394,7 @@ mod tests {
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
select_1: false,
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
@ -413,6 +419,7 @@ mod tests {
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
select_1: false,
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
@ -440,6 +447,7 @@ mod tests {
|
||||
command: None,
|
||||
working_directory: None,
|
||||
autocomplete_prompt: None,
|
||||
select_1: false,
|
||||
};
|
||||
|
||||
let post_processed_cli: PostProcessedCli = cli.into();
|
||||
|
@ -65,8 +65,9 @@ async fn main() -> Result<()> {
|
||||
CLIPBOARD.with(<_>::default);
|
||||
|
||||
debug!("Creating application...");
|
||||
let mut app = App::new(channel, config, args.input);
|
||||
let mut app = App::new(channel, config, args.input, args.select_1);
|
||||
stdout().flush()?;
|
||||
debug!("Running application...");
|
||||
let output = app.run(stdout().is_terminal(), false).await?;
|
||||
info!("App output: {:?}", output);
|
||||
let stdout_handle = stdout().lock();
|
||||
|
@ -84,7 +84,7 @@ where
|
||||
/// This can be used at any time to push items into the fuzzy matcher.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// use television::matcher::{config::Config, Matcher};
|
||||
///
|
||||
/// let config = Config::default();
|
||||
@ -134,7 +134,7 @@ where
|
||||
/// indices of the matched characters.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// use television::matcher::{config::Config, Matcher};
|
||||
///
|
||||
/// let config = Config::default();
|
||||
@ -190,7 +190,7 @@ where
|
||||
/// Get a single matched item.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// use television::matcher::{config::Config, Matcher};
|
||||
///
|
||||
/// let config = Config::default();
|
||||
|
96
tests/app.rs
96
tests/app.rs
@ -9,7 +9,7 @@ use tokio::{task::JoinHandle, time::timeout};
|
||||
/// Default timeout for tests.
|
||||
///
|
||||
/// This is kept quite high to avoid flakiness in CI.
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Sets up an app with a file channel and default config.
|
||||
///
|
||||
@ -18,21 +18,28 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
///
|
||||
/// The app is started in a separate task and can be interacted with by sending
|
||||
/// actions to the action channel.
|
||||
fn setup_app() -> (
|
||||
fn setup_app(
|
||||
channel: Option<TelevisionChannel>,
|
||||
select_1: bool,
|
||||
) -> (
|
||||
JoinHandle<television::app::AppOutput>,
|
||||
tokio::sync::mpsc::UnboundedSender<Action>,
|
||||
) {
|
||||
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("target_dir");
|
||||
std::env::set_current_dir(&target_dir).unwrap();
|
||||
let channel = TelevisionChannel::Files(
|
||||
television::channels::files::Channel::new(vec![target_dir]),
|
||||
);
|
||||
let config = default_config_from_file().unwrap();
|
||||
let chan: TelevisionChannel = channel.unwrap_or_else(|| {
|
||||
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("target_dir");
|
||||
std::env::set_current_dir(&target_dir).unwrap();
|
||||
TelevisionChannel::Files(television::channels::files::Channel::new(
|
||||
vec![target_dir],
|
||||
))
|
||||
});
|
||||
let mut config = default_config_from_file().unwrap();
|
||||
// this speeds up the tests
|
||||
config.application.tick_rate = 100.0;
|
||||
let input = None;
|
||||
|
||||
let mut app = App::new(channel, config, input);
|
||||
let mut app = App::new(chan, config, input, select_1);
|
||||
|
||||
// retrieve the app's action channel handle in order to send a quit action
|
||||
let tx = app.action_tx.clone();
|
||||
@ -41,29 +48,29 @@ fn setup_app() -> (
|
||||
let f = tokio::spawn(async move { app.run_headless().await.unwrap() });
|
||||
|
||||
// let the app spin up
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
(f, tx)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_does_quit() {
|
||||
let (f, tx) = setup_app();
|
||||
let (f, tx) = setup_app(None, false);
|
||||
|
||||
// send a quit action to the app
|
||||
tx.send(Action::Quit).unwrap();
|
||||
|
||||
// assert that the app quits within a default timeout
|
||||
std::thread::sleep(DEFAULT_TIMEOUT / 4);
|
||||
std::thread::sleep(DEFAULT_TIMEOUT);
|
||||
assert!(f.is_finished());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_starts_normally() {
|
||||
let (f, _) = setup_app();
|
||||
let (f, _) = setup_app(None, false);
|
||||
|
||||
// assert that the app is still running after the default timeout
|
||||
std::thread::sleep(DEFAULT_TIMEOUT / 4);
|
||||
std::thread::sleep(DEFAULT_TIMEOUT);
|
||||
assert!(!f.is_finished());
|
||||
|
||||
f.abort();
|
||||
@ -71,7 +78,7 @@ async fn test_app_starts_normally() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_basic_search() {
|
||||
let (f, tx) = setup_app();
|
||||
let (f, tx) = setup_app(None, false);
|
||||
|
||||
// send actions to the app
|
||||
for c in "file1".chars() {
|
||||
@ -100,7 +107,7 @@ async fn test_app_basic_search() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_basic_search_multiselect() {
|
||||
let (f, tx) = setup_app();
|
||||
let (f, tx) = setup_app(None, false);
|
||||
|
||||
// send actions to the app
|
||||
for c in "file".chars() {
|
||||
@ -132,3 +139,56 @@ async fn test_app_basic_search_multiselect() {
|
||||
HashSet::from([&"file1.txt".to_string(), &"file2.txt".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_exits_when_select_1_and_only_one_result() {
|
||||
let channel =
|
||||
TelevisionChannel::Stdin(television::channels::stdin::Channel::from(
|
||||
vec!["file1.txt".to_string()],
|
||||
));
|
||||
let (f, tx) = setup_app(Some(channel), true);
|
||||
|
||||
// tick a few times to get the results
|
||||
for _ in 0..=10 {
|
||||
tx.send(Action::Tick).unwrap();
|
||||
}
|
||||
|
||||
// check the output with a timeout
|
||||
// Note: we don't need to send a confirm action here, as the app should
|
||||
// exit automatically when there's only one result
|
||||
let output = timeout(DEFAULT_TIMEOUT, f)
|
||||
.await
|
||||
.expect("app did not finish within the default timeout")
|
||||
.unwrap();
|
||||
|
||||
assert!(output.selected_entries.is_some());
|
||||
assert_eq!(
|
||||
&output
|
||||
.selected_entries
|
||||
.unwrap()
|
||||
.drain()
|
||||
.next()
|
||||
.unwrap()
|
||||
.name,
|
||||
"file1.txt"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||
async fn test_app_does_not_exit_when_select_1_and_more_than_one_result() {
|
||||
let channel =
|
||||
TelevisionChannel::Stdin(television::channels::stdin::Channel::from(
|
||||
vec!["file1.txt".to_string(), "file2.txt".to_string()],
|
||||
));
|
||||
let (f, tx) = setup_app(Some(channel), true);
|
||||
|
||||
// tick a few times to get the results
|
||||
for _ in 0..=10 {
|
||||
tx.send(Action::Tick).unwrap();
|
||||
}
|
||||
|
||||
// check that the app is still running after the default timeout
|
||||
let output = timeout(DEFAULT_TIMEOUT, f).await;
|
||||
|
||||
assert!(output.is_err());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user