mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-07 03:55:23 +00:00
feat(cli): add --select-1
cli flag to automatically select unique result (#448)
Adds a `--select-1` flag that enables `tv` to automatically select the only result and exit when such a case appears. Fixes #440
This commit is contained in:
parent
69c4dcc5c5
commit
4892dc3c3c
24
man/tv.1
24
man/tv.1
@ -1,10 +1,10 @@
|
|||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.TH television 1 "television 0.11.0"
|
.TH television 1 "television 0.11.5"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
television \- A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
|
television \- A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
|
||||||
.SH SYNOPSIS
|
.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
|
.SH DESCRIPTION
|
||||||
A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
|
A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
@ -53,13 +53,6 @@ expressions using the configuration file formalism.
|
|||||||
|
|
||||||
Example: `tv \-\-keybindings=\*(Aqquit="esc";select_next_entry=["down","ctrl\-j"]\*(Aq`
|
Example: `tv \-\-keybindings=\*(Aqquit="esc";select_next_entry=["down","ctrl\-j"]\*(Aq`
|
||||||
.TP
|
.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
|
\fB\-i\fR, \fB\-\-input\fR=\fISTRING\fR
|
||||||
Input text to pass to the channel to prefill the prompt.
|
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
|
prompt by using the `shell_integration` mapping in the configuration
|
||||||
file.
|
file.
|
||||||
.TP
|
.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
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
Print help (see a summary with \*(Aq\-h\*(Aq)
|
Print help (see a summary with \*(Aq\-h\*(Aq)
|
||||||
.TP
|
.TP
|
||||||
@ -104,6 +108,6 @@ Initializes shell completion ("tv init zsh")
|
|||||||
television\-help(1)
|
television\-help(1)
|
||||||
Print this message or the help of the given subcommand(s)
|
Print this message or the help of the given subcommand(s)
|
||||||
.SH VERSION
|
.SH VERSION
|
||||||
v0.11.0
|
v0.11.5
|
||||||
.SH AUTHORS
|
.SH AUTHORS
|
||||||
Alexandre Pasmantier <alex.pasmant@gmail.com>
|
Alexandre Pasmantier <alex.pasmant@gmail.com>
|
||||||
|
@ -5,7 +5,7 @@ use tokio::sync::mpsc;
|
|||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use crate::channels::entry::{Entry, PreviewType};
|
use crate::channels::entry::{Entry, PreviewType};
|
||||||
use crate::channels::TelevisionChannel;
|
use crate::channels::{OnAir, TelevisionChannel};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::keymap::Keymap;
|
use crate::keymap::Keymap;
|
||||||
use crate::render::UiState;
|
use crate::render::UiState;
|
||||||
@ -53,10 +53,13 @@ pub struct App {
|
|||||||
ui_state_tx: mpsc::UnboundedSender<UiState>,
|
ui_state_tx: mpsc::UnboundedSender<UiState>,
|
||||||
/// Render task handle
|
/// Render task handle
|
||||||
render_task: Option<tokio::task::JoinHandle<Result<()>>>,
|
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.
|
/// The outcome of an action.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum ActionOutcome {
|
pub enum ActionOutcome {
|
||||||
Entries(FxHashSet<Entry>),
|
Entries(FxHashSet<Entry>),
|
||||||
Input(String),
|
Input(String),
|
||||||
@ -96,6 +99,7 @@ impl App {
|
|||||||
channel: TelevisionChannel,
|
channel: TelevisionChannel,
|
||||||
config: Config,
|
config: Config,
|
||||||
input: Option<String>,
|
input: Option<String>,
|
||||||
|
select_1: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||||
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
let (render_tx, render_rx) = mpsc::unbounded_channel();
|
||||||
@ -124,6 +128,7 @@ impl App {
|
|||||||
ui_state_rx,
|
ui_state_rx,
|
||||||
ui_state_tx,
|
ui_state_tx,
|
||||||
render_task: None,
|
render_task: None,
|
||||||
|
select_1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +151,7 @@ impl App {
|
|||||||
is_output_tty: bool,
|
is_output_tty: bool,
|
||||||
headless: bool,
|
headless: bool,
|
||||||
) -> Result<AppOutput> {
|
) -> Result<AppOutput> {
|
||||||
|
// Event loop
|
||||||
if !headless {
|
if !headless {
|
||||||
debug!("Starting backend event loop");
|
debug!("Starting backend event loop");
|
||||||
let event_loop = EventLoop::new(self.tick_rate, true);
|
let event_loop = EventLoop::new(self.tick_rate, true);
|
||||||
@ -169,11 +175,13 @@ impl App {
|
|||||||
.expect("Unable to send init render action.");
|
.expect("Unable to send init render action.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// event handling loop
|
// Main loop
|
||||||
debug!("Starting event handling loop");
|
debug!("Starting event handling loop");
|
||||||
let action_tx = self.action_tx.clone();
|
let action_tx = self.action_tx.clone();
|
||||||
let mut event_buf = Vec::with_capacity(EVENT_BUF_SIZE);
|
let mut event_buf = Vec::with_capacity(EVENT_BUF_SIZE);
|
||||||
let mut action_buf = Vec::with_capacity(ACTION_BUF_SIZE);
|
let mut action_buf = Vec::with_capacity(ACTION_BUF_SIZE);
|
||||||
|
let mut action_outcome;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// handle event and convert to action
|
// handle event and convert to action
|
||||||
if self
|
if self
|
||||||
@ -192,7 +200,18 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// It's important that this shouldn't block if no actions are available
|
// 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 {
|
if self.should_quit {
|
||||||
// send a termination signal to the event loop
|
// send a termination signal to the event loop
|
||||||
@ -349,4 +368,52 @@ impl App {
|
|||||||
}
|
}
|
||||||
Ok(ActionOutcome::None)
|
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>,
|
matcher: Matcher<Alias>,
|
||||||
file_icon: FileIcon,
|
file_icon: FileIcon,
|
||||||
selected_entries: FxHashSet<Entry>,
|
selected_entries: FxHashSet<Entry>,
|
||||||
|
crawl_handle: tokio::task::JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const NUM_THREADS: usize = 1;
|
const NUM_THREADS: usize = 1;
|
||||||
@ -44,6 +45,7 @@ fn get_raw_aliases(shell: &str) -> Vec<String> {
|
|||||||
.arg("alias")
|
.arg("alias")
|
||||||
.output()
|
.output()
|
||||||
.expect("failed to execute process");
|
.expect("failed to execute process");
|
||||||
|
|
||||||
let aliases = String::from_utf8_lossy(&output.stdout);
|
let aliases = String::from_utf8_lossy(&output.stdout);
|
||||||
aliases.lines().map(ToString::to_string).collect()
|
aliases.lines().map(ToString::to_string).collect()
|
||||||
}
|
}
|
||||||
@ -52,12 +54,13 @@ impl Channel {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
|
let matcher = Matcher::new(Config::default().n_threads(NUM_THREADS));
|
||||||
let injector = matcher.injector();
|
let injector = matcher.injector();
|
||||||
tokio::spawn(load_aliases(injector));
|
let crawl_handle = tokio::spawn(load_aliases(injector));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
matcher,
|
matcher,
|
||||||
file_icon: FileIcon::from(FILE_ICON_STR),
|
file_icon: FileIcon::from(FILE_ICON_STR),
|
||||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||||
|
crawl_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,7 +139,7 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.matcher.status.running
|
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&self) {}
|
fn shutdown(&self) {}
|
||||||
|
@ -33,6 +33,7 @@ pub struct Channel {
|
|||||||
entries_command: String,
|
entries_command: String,
|
||||||
preview_kind: PreviewKind,
|
preview_kind: PreviewKind,
|
||||||
selected_entries: FxHashSet<Entry>,
|
selected_entries: FxHashSet<Entry>,
|
||||||
|
crawl_handle: tokio::task::JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Channel {
|
impl Default for Channel {
|
||||||
@ -82,7 +83,10 @@ impl Channel {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let matcher = Matcher::new(Config::default());
|
let matcher = Matcher::new(Config::default());
|
||||||
let injector = matcher.injector();
|
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 {
|
let preview_kind = match preview_command {
|
||||||
Some(command) => {
|
Some(command) => {
|
||||||
parse_preview_kind(&command).unwrap_or_else(|_| {
|
parse_preview_kind(&command).unwrap_or_else(|_| {
|
||||||
@ -98,6 +102,7 @@ impl Channel {
|
|||||||
preview_kind,
|
preview_kind,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
||||||
|
crawl_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,7 +214,7 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.matcher.status.running
|
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&self) {}
|
fn shutdown(&self) {}
|
||||||
|
@ -136,7 +136,7 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.matcher.status.running
|
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&self) {
|
fn shutdown(&self) {
|
||||||
|
@ -142,7 +142,7 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.matcher.status.running
|
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&self) {
|
fn shutdown(&self) {
|
||||||
|
@ -106,7 +106,7 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.matcher.status.running
|
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&self) {
|
fn shutdown(&self) {
|
||||||
|
@ -15,6 +15,7 @@ pub struct Channel {
|
|||||||
matcher: Matcher<String>,
|
matcher: Matcher<String>,
|
||||||
preview_type: PreviewType,
|
preview_type: PreviewType,
|
||||||
selected_entries: FxHashSet<Entry>,
|
selected_entries: FxHashSet<Entry>,
|
||||||
|
instream_handle: std::thread::JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@ -22,12 +23,13 @@ impl Channel {
|
|||||||
let matcher = Matcher::new(Config::default());
|
let matcher = Matcher::new(Config::default());
|
||||||
let injector = matcher.injector();
|
let injector = matcher.injector();
|
||||||
|
|
||||||
spawn(move || stream_from_stdin(&injector));
|
let instream_handle = spawn(move || stream_from_stdin(&injector));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
matcher,
|
matcher,
|
||||||
preview_type,
|
preview_type,
|
||||||
selected_entries: HashSet::with_hasher(FxBuildHasher),
|
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);
|
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
|
||||||
fn stream_from_stdin(injector: &Injector<String>) {
|
fn stream_from_stdin(injector: &Injector<String>) {
|
||||||
@ -116,7 +145,7 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.matcher.status.running
|
self.matcher.status.running || !self.instream_handle.is_finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&self) {}
|
fn shutdown(&self) {}
|
||||||
|
@ -239,7 +239,7 @@ impl OnAir for Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn running(&self) -> bool {
|
fn running(&self) -> bool {
|
||||||
self.matcher.status.running
|
self.matcher.status.running || !self.crawl_handle.is_finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&self) {
|
fn shutdown(&self) {
|
||||||
|
@ -90,6 +90,18 @@ pub struct Cli {
|
|||||||
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
|
||||||
pub autocomplete_prompt: Option<String>,
|
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)]
|
#[command(subcommand)]
|
||||||
pub command: Option<Command>,
|
pub command: Option<Command>,
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ pub struct PostProcessedCli {
|
|||||||
pub working_directory: Option<String>,
|
pub working_directory: Option<String>,
|
||||||
pub autocomplete_prompt: Option<String>,
|
pub autocomplete_prompt: Option<String>,
|
||||||
pub keybindings: Option<KeyBindings>,
|
pub keybindings: Option<KeyBindings>,
|
||||||
|
pub select_1: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PostProcessedCli {
|
impl Default for PostProcessedCli {
|
||||||
@ -44,6 +45,7 @@ impl Default for PostProcessedCli {
|
|||||||
working_directory: None,
|
working_directory: None,
|
||||||
autocomplete_prompt: None,
|
autocomplete_prompt: None,
|
||||||
keybindings: None,
|
keybindings: None,
|
||||||
|
select_1: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,6 +110,7 @@ impl From<Cli> for PostProcessedCli {
|
|||||||
working_directory,
|
working_directory,
|
||||||
autocomplete_prompt: cli.autocomplete_prompt,
|
autocomplete_prompt: cli.autocomplete_prompt,
|
||||||
keybindings,
|
keybindings,
|
||||||
|
select_1: cli.select_1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -322,6 +325,7 @@ mod tests {
|
|||||||
command: None,
|
command: None,
|
||||||
working_directory: Some("/home/user".to_string()),
|
working_directory: Some("/home/user".to_string()),
|
||||||
autocomplete_prompt: None,
|
autocomplete_prompt: None,
|
||||||
|
select_1: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_processed_cli: PostProcessedCli = cli.into();
|
let post_processed_cli: PostProcessedCli = cli.into();
|
||||||
@ -360,6 +364,7 @@ mod tests {
|
|||||||
command: None,
|
command: None,
|
||||||
working_directory: None,
|
working_directory: None,
|
||||||
autocomplete_prompt: None,
|
autocomplete_prompt: None,
|
||||||
|
select_1: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_processed_cli: PostProcessedCli = cli.into();
|
let post_processed_cli: PostProcessedCli = cli.into();
|
||||||
@ -389,6 +394,7 @@ mod tests {
|
|||||||
command: None,
|
command: None,
|
||||||
working_directory: None,
|
working_directory: None,
|
||||||
autocomplete_prompt: None,
|
autocomplete_prompt: None,
|
||||||
|
select_1: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_processed_cli: PostProcessedCli = cli.into();
|
let post_processed_cli: PostProcessedCli = cli.into();
|
||||||
@ -413,6 +419,7 @@ mod tests {
|
|||||||
command: None,
|
command: None,
|
||||||
working_directory: None,
|
working_directory: None,
|
||||||
autocomplete_prompt: None,
|
autocomplete_prompt: None,
|
||||||
|
select_1: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_processed_cli: PostProcessedCli = cli.into();
|
let post_processed_cli: PostProcessedCli = cli.into();
|
||||||
@ -440,6 +447,7 @@ mod tests {
|
|||||||
command: None,
|
command: None,
|
||||||
working_directory: None,
|
working_directory: None,
|
||||||
autocomplete_prompt: None,
|
autocomplete_prompt: None,
|
||||||
|
select_1: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_processed_cli: PostProcessedCli = cli.into();
|
let post_processed_cli: PostProcessedCli = cli.into();
|
||||||
|
@ -65,8 +65,9 @@ async fn main() -> Result<()> {
|
|||||||
CLIPBOARD.with(<_>::default);
|
CLIPBOARD.with(<_>::default);
|
||||||
|
|
||||||
debug!("Creating application...");
|
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()?;
|
stdout().flush()?;
|
||||||
|
debug!("Running application...");
|
||||||
let output = app.run(stdout().is_terminal(), false).await?;
|
let output = app.run(stdout().is_terminal(), false).await?;
|
||||||
info!("App output: {:?}", output);
|
info!("App output: {:?}", output);
|
||||||
let stdout_handle = stdout().lock();
|
let stdout_handle = stdout().lock();
|
||||||
|
@ -84,7 +84,7 @@ where
|
|||||||
/// This can be used at any time to push items into the fuzzy matcher.
|
/// This can be used at any time to push items into the fuzzy matcher.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```ignore
|
||||||
/// use television::matcher::{config::Config, Matcher};
|
/// use television::matcher::{config::Config, Matcher};
|
||||||
///
|
///
|
||||||
/// let config = Config::default();
|
/// let config = Config::default();
|
||||||
@ -134,7 +134,7 @@ where
|
|||||||
/// indices of the matched characters.
|
/// indices of the matched characters.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```ignore
|
||||||
/// use television::matcher::{config::Config, Matcher};
|
/// use television::matcher::{config::Config, Matcher};
|
||||||
///
|
///
|
||||||
/// let config = Config::default();
|
/// let config = Config::default();
|
||||||
@ -190,7 +190,7 @@ where
|
|||||||
/// Get a single matched item.
|
/// Get a single matched item.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```ignore
|
||||||
/// use television::matcher::{config::Config, Matcher};
|
/// use television::matcher::{config::Config, Matcher};
|
||||||
///
|
///
|
||||||
/// let config = Config::default();
|
/// 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.
|
/// Default timeout for tests.
|
||||||
///
|
///
|
||||||
/// This is kept quite high to avoid flakiness in CI.
|
/// 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.
|
/// 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
|
/// The app is started in a separate task and can be interacted with by sending
|
||||||
/// actions to the action channel.
|
/// actions to the action channel.
|
||||||
fn setup_app() -> (
|
fn setup_app(
|
||||||
|
channel: Option<TelevisionChannel>,
|
||||||
|
select_1: bool,
|
||||||
|
) -> (
|
||||||
JoinHandle<television::app::AppOutput>,
|
JoinHandle<television::app::AppOutput>,
|
||||||
tokio::sync::mpsc::UnboundedSender<Action>,
|
tokio::sync::mpsc::UnboundedSender<Action>,
|
||||||
) {
|
) {
|
||||||
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
let chan: TelevisionChannel = channel.unwrap_or_else(|| {
|
||||||
.join("tests")
|
let target_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("target_dir");
|
.join("tests")
|
||||||
std::env::set_current_dir(&target_dir).unwrap();
|
.join("target_dir");
|
||||||
let channel = TelevisionChannel::Files(
|
std::env::set_current_dir(&target_dir).unwrap();
|
||||||
television::channels::files::Channel::new(vec![target_dir]),
|
TelevisionChannel::Files(television::channels::files::Channel::new(
|
||||||
);
|
vec![target_dir],
|
||||||
let config = default_config_from_file().unwrap();
|
))
|
||||||
|
});
|
||||||
|
let mut config = default_config_from_file().unwrap();
|
||||||
|
// this speeds up the tests
|
||||||
|
config.application.tick_rate = 100.0;
|
||||||
let input = None;
|
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
|
// retrieve the app's action channel handle in order to send a quit action
|
||||||
let tx = app.action_tx.clone();
|
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 f = tokio::spawn(async move { app.run_headless().await.unwrap() });
|
||||||
|
|
||||||
// let the app spin up
|
// let the app spin up
|
||||||
std::thread::sleep(Duration::from_millis(200));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
(f, tx)
|
(f, tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||||
async fn test_app_does_quit() {
|
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
|
// send a quit action to the app
|
||||||
tx.send(Action::Quit).unwrap();
|
tx.send(Action::Quit).unwrap();
|
||||||
|
|
||||||
// assert that the app quits within a default timeout
|
// assert that the app quits within a default timeout
|
||||||
std::thread::sleep(DEFAULT_TIMEOUT / 4);
|
std::thread::sleep(DEFAULT_TIMEOUT);
|
||||||
assert!(f.is_finished());
|
assert!(f.is_finished());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||||
async fn test_app_starts_normally() {
|
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
|
// 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());
|
assert!(!f.is_finished());
|
||||||
|
|
||||||
f.abort();
|
f.abort();
|
||||||
@ -71,7 +78,7 @@ async fn test_app_starts_normally() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||||
async fn test_app_basic_search() {
|
async fn test_app_basic_search() {
|
||||||
let (f, tx) = setup_app();
|
let (f, tx) = setup_app(None, false);
|
||||||
|
|
||||||
// send actions to the app
|
// send actions to the app
|
||||||
for c in "file1".chars() {
|
for c in "file1".chars() {
|
||||||
@ -100,7 +107,7 @@ async fn test_app_basic_search() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
|
||||||
async fn test_app_basic_search_multiselect() {
|
async fn test_app_basic_search_multiselect() {
|
||||||
let (f, tx) = setup_app();
|
let (f, tx) = setup_app(None, false);
|
||||||
|
|
||||||
// send actions to the app
|
// send actions to the app
|
||||||
for c in "file".chars() {
|
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()])
|
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