pub mod keybinds;
mod spinner;
mod tree;
use std::{
cmp::Ordering,
io::{self, Read},
rc::Rc,
time::Duration,
};
use tokio::sync::mpsc::{UnboundedReceiver, error::TryRecvError};
use anyhow::{Context, Result};
use cnf_lib::Query;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
execute,
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use logerr::LoggableError;
use ratatui::{
Terminal,
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
};
use spinner::Spinner;
use tree::UiTree;
fn sort_by(lhs: &Rc<Query>, rhs: &Rc<Query>) -> Ordering {
use cnf_lib::ProviderError::Requirements;
match (&lhs.results, &rhs.results) {
(Ok(_), Ok(_)) => lhs.provider.to_string().cmp(&rhs.provider.to_string()),
(Ok(_), Err(_)) => Ordering::Greater,
(Err(_), Ok(_)) => Ordering::Less,
(Err(Requirements(_)), Err(Requirements(_))) => {
lhs.provider.to_string().cmp(&rhs.provider.to_string())
}
(Err(_), Err(Requirements(_))) => Ordering::Greater,
(Err(Requirements(_)), Err(_)) => Ordering::Less,
_ => lhs.provider.to_string().cmp(&rhs.provider.to_string()),
}
}
pub(crate) async fn tui(channel: UnboundedReceiver<Query>, args: &[String]) -> Result<()> {
terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let app = App::new(channel);
let result = app.run(&mut terminal).await;
terminal::disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
match result {
Ok(Some((query, index))) => {
let status = query
.run(
index,
&args.iter().skip(1).map(|s| &s[..]).collect::<Vec<_>>(),
)
.await
.context("failed to execute selection")?;
match status.code() {
Some(code) if code != 0 => std::process::exit(code),
None => std::process::exit(255),
_ => Ok(()),
}
}
Ok(None) => Ok(()),
Err(e) => Err::<(), _>(e),
}
}
async fn install_selection<B: Backend>(
query: &Query,
index: usize,
terminal: &mut Terminal<B>,
) -> Result<()> {
terminal.clear()?;
terminal.show_cursor()?;
terminal.set_cursor(0, 0)?;
terminal::disable_raw_mode()?;
query.install(index).await?;
terminal::enable_raw_mode()?;
terminal.hide_cursor()?;
let raw_text = "Press Enter to continue";
let message = ansi_term::Color::Green.bold().paint(raw_text);
let offset = match terminal.size() {
Ok(rect) => rect
.width
.saturating_sub(raw_text.len().try_into().unwrap_or(0))
.saturating_div(2),
_ => 0,
};
println!("\n\n");
(0..offset).for_each(|_| print!(" "));
println!("{}", message);
std::io::stdin().read_exact(&mut [0])?;
terminal.clear()?;
Ok(())
}
#[derive(Debug)]
struct App {
queries: Vec<Rc<Query>>,
channel: UnboundedReceiver<Query>,
tree: UiTree,
spinner: Spinner,
all_there: bool,
keybinds: &'static keybinds::AppKeybinds,
}
impl App {
fn new(channel: UnboundedReceiver<Query>) -> Self {
App {
queries: vec![],
channel,
tree: UiTree::new(),
spinner: Spinner::new(),
all_there: false,
keybinds: &crate::config::get().keybindings,
}
}
async fn run<B: Backend>(
mut self,
terminal: &mut Terminal<B>,
) -> Result<Option<(Rc<Query>, usize)>> {
loop {
match self.channel.try_recv() {
Ok(result) => self.push(result),
Err(TryRecvError::Empty) => (),
Err(TryRecvError::Disconnected) => self.all_there = true,
}
let mut cur_candidate = self.tree.get_selected_candidate();
if event::poll(Duration::from_millis(50))?
&& let Event::Key(key) = event::read()?
{
if self.keybinds.select_up == key {
self.tree.selection_minus();
} else if self.keybinds.select_down == key {
self.tree.selection_plus();
} else if self.keybinds.expand == key {
self.tree.expand_node();
} else if self.keybinds.collapse == key {
self.tree.collapse_node();
} else if self.keybinds.add_alias == key {
if let Some((query, index)) = cur_candidate.clone() {
let err_context = "failed to create new command alias";
let candidate =
match query.results.as_ref().map(|result| result.get(index)) {
Ok(Some(candidate)) => Ok(candidate),
Ok(None) => Err(anyhow::anyhow!(
"selected query '{:?}' has no candidate at index '{}'",
query,
index
)),
Err(e) => Err(anyhow::anyhow!(
"selected query '{:?}' has no valid results; error: '{}'",
query,
e
)),
}
.context(err_context)
.to_log()?;
let mut cmd = candidate.actions.execute.clone();
cmd.is_interactive(true);
let alias = crate::alias::Alias {
source_env: cnf_lib::env::current().to_json(),
target_env: query.env.to_json(),
command: query.term.clone(),
alias: cmd,
};
alias.to_shell_wrapper().with_context(|| {
format!("failed to create alias for '{}'", query.term)
})?;
}
} else if self.keybinds.install == key {
if let Some((query, index)) = cur_candidate.clone() {
install_selection(&query, index, terminal)
.await
.context("failed to install selected entry")?;
}
} else if self.keybinds.execute == key {
if let Some((query, index)) = cur_candidate.clone() {
return Ok(Some((query, index)));
}
} else if self.keybinds.quit == key {
return Ok(None);
} else {
}
}
terminal.draw(|f| {
let spinner_chunk_size = if self.all_there { 0 } else { 1 };
let chunks = Layout::default()
.constraints(
[
Constraint::Length(spinner_chunk_size),
Constraint::Min(0),
Constraint::Length(1),
]
.as_ref(),
)
.split(f.size());
if !self.all_there {
f.render_widget(self.spinner.display(), chunks[0]);
}
f.render_widget(&mut self.tree, chunks[1]);
f.render_stateful_widget(&mut self.keybinds, chunks[2], &mut cur_candidate);
})?;
}
}
fn push(&mut self, query: Query) {
let query = Rc::new(query);
self.queries.push(query.clone());
self.tree.add_env(query.env.clone());
self.tree.add_query(query);
}
}