use std::{cell::RefCell, error::Error, rc::Rc, sync::Arc};
use ratatui::{
style::{Color, Modifier, Style},
text::{Span, Spans},
};
use cnf_lib::prelude::*;
const DEFAULT_SCROLL_BORDER: usize = 2;
trait UiTreeNode<'a> {
fn value_collapsed(&self) -> Vec<Spans<'a>>;
fn value_expanded(&self) -> Vec<Spans<'a>> {
self.value_collapsed()
}
fn get_id(&self) -> u64;
fn enumerate(&self) -> Vec<u64>;
fn collapse(&mut self, id: u64);
fn expand(&mut self, id: u64);
fn get_candidate(&self, id: u64) -> Option<(Rc<Query>, usize)>;
}
#[derive(Debug)]
struct NodeInfo {
expanded: bool,
id: u64,
}
#[derive(Debug)]
struct CandidateNode {
value: (Rc<Query>, usize),
info: NodeInfo,
}
impl<'a> UiTreeNode<'a> for CandidateNode {
fn value_collapsed(&self) -> Vec<Spans<'a>> {
let query = self.value.0.clone();
let index = self.value.1;
let value = query.results.as_ref().unwrap().get(index).unwrap();
let needs_install = if value.actions.install.is_some() {
"needs install"
} else {
"installed"
};
vec![
if value.version.is_empty() {
Span::raw(format!(" + {}, {}", value.package, needs_install))
} else {
Span::raw(format!(
" + {} @ {}, {}",
value.package, value.version, needs_install
))
}
.into(),
]
}
fn value_expanded(&self) -> Vec<Spans<'a>> {
macro_rules! write_format {
($string:expr, $var:expr) => {
Span::raw(format!(
" {:<14}: {}\n",
$string,
if $var.is_empty() { "n/a" } else { $var }
))
};
}
let query = self.value.0.clone();
let index = self.value.1;
let value = query.results.as_ref().unwrap().get(index).unwrap();
vec![
Spans::from(vec![
Span::raw(" - "),
Span::styled(
format!("Package: {}\n", &value.package),
Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
),
]),
write_format!("Description", &value.description).into(),
write_format!("Version", &value.version).into(),
write_format!("Origin", &value.origin).into(),
write_format!(
"Needs install",
if value.actions.install.is_some() {
"yes"
} else {
"no"
}
)
.into(),
]
}
fn get_id(&self) -> u64 {
self.info.id
}
fn enumerate(&self) -> Vec<u64> {
vec![self.info.id]
}
fn collapse(&mut self, id: u64) {
if id == self.info.id {
self.info.expanded = false;
}
}
fn expand(&mut self, id: u64) {
if id == self.info.id {
self.info.expanded = true;
}
}
fn get_candidate(&self, id: u64) -> Option<(Rc<Query>, usize)> {
if id == self.info.id {
Some((self.value.0.clone(), self.value.1))
} else {
None
}
}
}
#[derive(Debug)]
struct ProviderNode {
value: Rc<Query>,
children: Vec<CandidateNode>,
info: NodeInfo,
}
impl<'a> UiTreeNode<'a> for ProviderNode {
fn value_collapsed(&self) -> Vec<Spans<'a>> {
let provider = &self.value.provider;
let mut span_vec = match &self.value.results {
Ok(results) => vec![
Span::raw("["),
Span::styled("✔", Style::default().fg(Color::Green)),
Span::raw(format!("] {} - {} results found", provider, results.len())),
],
Err(error) => {
if matches!(error, ProviderError::Requirements(_)) {
vec![
Span::raw("["),
Span::styled("-", Style::default().add_modifier(Modifier::DIM)),
Span::raw("] "),
Span::raw(format!("{} - {}", provider, error)),
]
} else {
vec![
Span::raw("["),
Span::styled("✘", Style::default().fg(Color::Red)),
Span::raw(format!("] {} - {}", provider, error)),
]
}
}
};
let mut spans = vec![Span::raw(" + ")];
spans.append(&mut span_vec);
vec![Spans::from(spans)]
}
fn value_expanded(&self) -> Vec<Spans<'a>> {
let provider = &self.value.provider;
let span_prefix = Span::raw(" - [");
match &self.value.results {
Ok(results) => vec![Spans::from(vec![
span_prefix,
Span::styled("✔", Style::default().fg(Color::Green)),
Span::raw(format!("] {} - {} results found", provider, results.len())),
])],
Err(error) => {
if matches!(error, ProviderError::Requirements(_)) {
vec![Spans::from(vec![
span_prefix,
Span::styled("-", Style::default().add_modifier(Modifier::DIM)),
Span::raw("] "),
Span::raw(format!("{} - {}", provider, error)),
])]
} else {
let mut output = vec![Spans::from(vec![
span_prefix,
Span::styled("✘", Style::default().fg(Color::Red)),
Span::raw(format!("] {} - {}", provider, error)),
])];
let mut source_err: Option<&(dyn Error + 'static)> = error.source();
while let Some(error) = source_err {
let line = Spans::from(vec![
Span::styled(" '->", Style::default().fg(Color::Red)),
Span::styled(
" caused by: ",
Style::default().add_modifier(Modifier::ITALIC),
),
Span::raw(error.to_string()),
]);
output.push(line);
source_err = error.source();
}
output
}
}
}
}
fn get_id(&self) -> u64 {
self.info.id
}
fn enumerate(&self) -> Vec<u64> {
let mut ret = vec![self.info.id];
if self.info.expanded {
let mut ids = self
.children
.iter()
.flat_map(|child| child.enumerate())
.collect::<Vec<u64>>();
ret.append(&mut ids);
}
ret
}
fn collapse(&mut self, id: u64) {
if id == self.info.id {
self.info.expanded = false;
} else {
self.children
.iter_mut()
.for_each(|child| child.collapse(id));
}
}
fn expand(&mut self, id: u64) {
if id == self.info.id {
self.info.expanded = true;
} else {
self.children.iter_mut().for_each(|child| child.expand(id));
}
}
fn get_candidate(&self, id: u64) -> Option<(Rc<Query>, usize)> {
if (id == self.info.id) && (self.children.len() == 1) {
self.children
.first()
.and_then(|child| child.get_candidate(child.get_id()))
} else {
let mut candidates = self
.children
.iter()
.filter_map(|child| child.get_candidate(id))
.collect::<Vec<_>>();
candidates.pop()
}
}
}
#[derive(Debug)]
struct EnvNode {
value: Arc<Environment>,
children: RefCell<Vec<ProviderNode>>,
info: NodeInfo,
}
impl<'a> UiTreeNode<'a> for EnvNode {
fn value_collapsed(&self) -> Vec<Spans<'a>> {
vec![Spans::from(vec![
Span::raw("+ "),
Span::raw(format!("In {}", self.value)),
])]
}
fn value_expanded(&self) -> Vec<Spans<'a>> {
vec![Spans::from(vec![
Span::raw("- "),
Span::raw(format!("In {}", self.value)),
])]
}
fn get_id(&self) -> u64 {
self.info.id
}
fn enumerate(&self) -> Vec<u64> {
let mut ret = vec![self.info.id];
if self.info.expanded {
let mut ids = self
.children
.borrow()
.iter()
.flat_map(|child| child.enumerate())
.collect::<Vec<u64>>();
ret.append(&mut ids);
}
ret
}
fn collapse(&mut self, id: u64) {
if id == self.info.id {
self.info.expanded = false;
} else {
self.children
.borrow_mut()
.iter_mut()
.for_each(|child| child.collapse(id));
}
}
fn expand(&mut self, id: u64) {
if id == self.info.id {
self.info.expanded = true;
} else {
self.children
.borrow_mut()
.iter_mut()
.for_each(|child| child.expand(id));
}
}
fn get_candidate(&self, id: u64) -> Option<(Rc<Query>, usize)> {
let mut candidates = self
.children
.borrow()
.iter()
.filter_map(|child| child.get_candidate(id))
.collect::<Vec<_>>();
candidates.pop()
}
}
#[derive(Debug, Default)]
pub(crate) struct UiTree {
next_id: RefCell<u64>,
children: Vec<EnvNode>,
ui: UiState,
}
#[derive(Debug, Default)]
struct UiState {
select_id: u64,
last_viewport_scroll: usize,
}
impl UiTree {
pub(crate) fn to_spans<'a>(&self) -> Vec<Vec<Spans<'a>>> {
let mut spans = vec![];
for env_node in &self.children {
if env_node.info.expanded {
spans.push(env_node.value_expanded());
for provider_node in &*env_node.children.borrow() {
if provider_node.info.expanded {
spans.push(provider_node.value_expanded());
for query_node in &provider_node.children {
if query_node.info.expanded {
spans.push(query_node.value_expanded());
} else {
spans.push(query_node.value_collapsed());
}
}
} else {
spans.push(provider_node.value_collapsed());
}
}
} else {
spans.push(env_node.value_collapsed());
}
}
spans
}
pub(crate) fn new() -> Self {
Default::default()
}
pub(crate) fn enumerate(&self) -> Vec<u64> {
self.children
.iter()
.flat_map(|child| child.enumerate())
.collect::<Vec<_>>()
}
pub(crate) fn selection_plus(&mut self) {
let ids = self.enumerate();
let next = ids
.iter()
.skip_while(|id| **id != self.ui.select_id)
.nth(1)
.unwrap_or(&self.ui.select_id);
self.ui.select_id = *next;
}
pub(crate) fn selection_minus(&mut self) {
let ids = self.enumerate();
let mut last_id = ids.first().copied();
for id in self.enumerate() {
if id == self.ui.select_id {
break;
} else {
last_id = Some(id);
}
}
if let Some(id) = last_id {
self.ui.select_id = id;
}
}
pub(crate) fn collapse_node(&mut self) {
self.children
.iter_mut()
.for_each(|child| child.collapse(self.ui.select_id));
}
pub(crate) fn expand_node(&mut self) {
self.children
.iter_mut()
.for_each(|child| child.expand(self.ui.select_id));
}
fn next_node_info(&self) -> NodeInfo {
let id = self.next_id.replace_with(|&mut old| old + 1);
NodeInfo {
expanded: false,
id,
}
}
pub(crate) fn add_env(&mut self, env: Arc<Environment>) {
if !self.children.iter().any(|child| child.value == env) {
let mut node = EnvNode {
value: env,
children: RefCell::new(vec![]),
info: self.next_node_info(),
};
node.info.expanded = true;
self.children.push(node);
}
}
pub(crate) fn add_query(&mut self, query: Rc<Query>) {
if let Some(node) = self
.children
.iter()
.find(|child_env| child_env.value == query.env)
{
let mut provider_node = ProviderNode {
value: query.clone(),
children: vec![],
info: self.next_node_info(),
};
let candidate_nodes = match &query.results {
Err(_) => vec![],
Ok(results) => results
.iter()
.enumerate()
.map(|(index, _)| CandidateNode {
value: (query.clone(), index),
info: self.next_node_info(),
})
.collect::<Vec<_>>(),
};
provider_node.children = candidate_nodes;
if provider_node.children.len() == 1 {
provider_node.info.expanded = true;
}
let mut child_vec = node.children.borrow_mut();
let pos = child_vec
.binary_search_by(|elem| crate::ui::sort_by(&provider_node.value, &elem.value))
.unwrap_or_else(|e| e);
child_vec.insert(pos, provider_node);
} else {
tracing::warn!(
"tried to add query '{:?}' to non-existent env '{:?}'",
query,
query.env
);
}
}
pub(crate) fn get_selected_candidate(&self) -> Option<(Rc<Query>, usize)> {
let mut candidates = self
.children
.iter()
.filter_map(|child| child.get_candidate(self.ui.select_id))
.collect::<Vec<_>>();
candidates.pop()
}
}
impl ratatui::widgets::Widget for &mut UiTree {
fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
let symbol = ">> ";
let (x, mut y) = (area.left() + symbol.chars().count() as u16, area.top());
let area_height = area.height as usize;
let tree_spans = self.to_spans();
let span_ids = tree_spans.iter().zip(self.enumerate());
let num_lines = tree_spans.iter().flatten().count();
let skip_at_start = if num_lines <= area_height {
0
} else {
struct SelectionLines {
pub first_line: usize,
pub last_line: usize,
pub height: usize,
}
let mut lines_so_far = 0;
let selection_lines = 'escape: {
for (span_vec, id) in span_ids.clone() {
let spans_height = span_vec.len();
if id == self.ui.select_id {
break 'escape SelectionLines {
first_line: lines_so_far,
last_line: lines_so_far + spans_height,
height: spans_height,
};
}
lines_so_far += spans_height;
}
panic!(
"current selection {} is not part of the visible tree with IDs '{:?}'",
self.ui.select_id,
self.enumerate()
);
};
let scroll_border = std::cmp::min(
DEFAULT_SCROLL_BORDER,
(area_height.saturating_sub(selection_lines.height)) / 2,
);
let to_skip = (selection_lines.last_line + scroll_border).saturating_sub(area_height);
if area_height <= selection_lines.height {
selection_lines.first_line
} else if to_skip > self.ui.last_viewport_scroll {
std::cmp::min(to_skip, num_lines - area_height)
} else {
let to_skip = self.ui.last_viewport_scroll;
let selection_start = selection_lines.first_line.saturating_sub(scroll_border);
if selection_start < to_skip {
selection_start
} else {
to_skip
}
}
};
let mut lines_seen = 0;
for (span_vec, id) in span_ids {
if id == self.ui.select_id {
buf.set_string(0, y, symbol, Style::default());
}
for inner_span in span_vec {
if (lines_seen >= skip_at_start) && (y <= area.height) {
buf.set_spans(x, y, inner_span, area.width - 10);
y += 1;
}
lines_seen += 1;
}
}
self.ui.last_viewport_scroll = skip_at_start;
}
}