/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
//use log::debug;
use std::collections::HashMap;
use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
Frame,
};
use darkfi::util::time::NanoTimestamp;
use crate::{
error::{DnetViewError, DnetViewResult},
model::{NodeInfo, SelectableObject},
};
type MsgLog = Vec<(NanoTimestamp, String, String)>;
type MsgMap = HashMap;
#[derive(Debug, Clone)]
pub struct View {
pub id_menu: IdMenu,
pub msg_list: MsgList,
pub selectables: HashMap,
pub ordered_list: Vec,
}
impl Default for View {
fn default() -> Self {
Self::new()
}
}
impl<'a> View {
pub fn new() -> Self {
let msg_map = HashMap::new();
let msg_list = MsgList::new(msg_map, 0);
let selectables = HashMap::new();
let id_menu = IdMenu::new(Vec::new());
let ordered_list = Vec::new();
Self { id_menu, msg_list, selectables, ordered_list }
}
pub fn update(&mut self, msg_map: MsgMap, selectables: HashMap) {
self.update_selectable(selectables.clone());
self.update_msg_list(msg_map);
self.update_id_menu(selectables);
self.update_msg_index();
self.make_ordered_list();
}
// We copy the values into a string to initialize List as a StatefulObject.
fn update_id_menu(&mut self, selectables: HashMap) {
for id in selectables.keys() {
if !self.id_menu.ids.iter().any(|i| i == id) {
self.id_menu.ids.push(id.to_string());
}
}
}
// We first add every selectable object into a hashmap to avoid duplicates.
fn update_selectable(&mut self, selectables: HashMap) {
for (id, obj) in selectables {
self.selectables.insert(id, obj);
}
}
// The order of the ordered_list created here must match the order
// of the Vec created in render_left().
// This is used to render_right() correctly.
fn make_ordered_list(&mut self) {
for obj in self.selectables.values() {
match obj {
SelectableObject::Node(node) => {
if !self.ordered_list.iter().any(|i| i == &node.dnet_id) {
self.ordered_list.push(node.dnet_id.clone());
}
if !node.is_offline && node.dnet_enabled {
for inbound in &node.inbound {
if !inbound.is_empty {
if !self.ordered_list.iter().any(|i| i == &inbound.dnet_id) {
self.ordered_list.push(inbound.dnet_id.clone());
}
if !self.ordered_list.iter().any(|i| i == &inbound.info.dnet_id) {
self.ordered_list.push(inbound.info.dnet_id.clone());
}
}
}
for outbound in &node.outbound {
if !outbound.is_empty {
if !self.ordered_list.iter().any(|i| i == &outbound.dnet_id) {
self.ordered_list.push(outbound.dnet_id.clone());
}
if !self.ordered_list.iter().any(|i| i == &outbound.info.dnet_id) {
self.ordered_list.push(outbound.info.dnet_id.clone());
}
}
}
}
}
SelectableObject::Lilith(lilith) => {
if !self.ordered_list.iter().any(|i| i == &lilith.id) {
self.ordered_list.push(lilith.id.clone());
}
for network in &lilith.networks {
if !self.ordered_list.iter().any(|i| i == &network.id) {
self.ordered_list.push(network.id.clone());
}
}
}
_ => (),
}
}
}
// TODO: this function displays msgs according to what id is
// selected. It's ugly, would prefer something more simple.
fn update_msg_index(&mut self) {
if let Some(sel) = self.id_menu.state.selected() {
if let Some(ord) = self.ordered_list.get(sel) {
if let Some(i) = self.msg_list.msg_map.get(ord) {
self.msg_list.index = i.len();
}
}
}
}
fn update_msg_list(&mut self, msg_map: MsgMap) {
for (id, msg) in msg_map {
self.msg_list.msg_map.insert(id, msg);
}
}
pub fn render(&mut self, f: &mut Frame<'_, B>) -> DnetViewResult<()> {
let margin = 2;
let direction = Direction::Horizontal;
let cnstrnts = vec![Constraint::Percentage(50), Constraint::Percentage(50)];
let slice = Layout::default()
.direction(direction)
.margin(margin)
.constraints(cnstrnts)
.split(f.size());
self.render_left(f, slice[0])?;
if self.ordered_list.is_empty() {
// we have not received any data
Ok(())
} else {
// get the id at the current index
match self.id_menu.state.selected() {
Some(i) => match self.ordered_list.get(i) {
Some(i) => {
let id = i.clone();
self.render_right(f, slice[1], id)?;
Ok(())
}
None => Err(DnetViewError::NoIdAtIndex),
},
// nothing is selected right now
None => Ok(()),
}
}
}
fn render_left(&mut self, f: &mut Frame<'_, B>, slice: Rect) -> DnetViewResult<()> {
let style = Style::default();
let mut nodes = Vec::new();
for obj in self.selectables.values() {
match obj {
SelectableObject::Node(node) => {
if node.is_offline {
let style =
Style::default().fg(Color::LightBlue).add_modifier(Modifier::ITALIC);
let mut name = String::new();
name.push_str(&node.name);
name.push_str("(Offline)");
let name_span = Span::styled(name, style);
let lines = vec![Line::from(name_span)];
let names = ListItem::new(lines);
nodes.push(names);
} else {
if !node.dnet_enabled {
let style =
Style::default().fg(Color::LightBlue).add_modifier(Modifier::BOLD);
let mut name = String::new();
name.push_str(&node.name);
name.push_str("(dnetview is not enabled)");
let name_span = Span::styled(name, style);
let lines = vec![Line::from(name_span)];
let names = ListItem::new(lines);
nodes.push(names);
} else {
let name_span = Span::raw(&node.name);
let lines = vec![Line::from(name_span)];
let names = ListItem::new(lines);
nodes.push(names);
if !node.inbound.is_empty() {
let name = Span::styled(format!(" Inbound"), style);
let lines = vec![Line::from(name)];
let names = ListItem::new(lines);
nodes.push(names);
for inbound in &node.inbound {
let mut infos = Vec::new();
match inbound.info.addr.as_str() {
"Null" => {
let style = Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::ITALIC);
let name = Span::styled(
format!(" {} ", inbound.info.addr),
style,
);
infos.push(name);
}
addr => {
let name =
Span::styled(format!(" {}", addr), style);
infos.push(name);
if !inbound.info.remote_id.is_empty() {
let remote_id = Span::styled(
format!("({})", inbound.info.remote_id),
style,
);
infos.push(remote_id)
}
}
}
let lines = vec![Line::from(infos)];
let names = ListItem::new(lines);
nodes.push(names);
}
}
if !&node.outbound.is_empty() {
let name = Span::styled(format!(" Outbound"), style);
let lines = vec![Line::from(name)];
let names = ListItem::new(lines);
nodes.push(names);
for outbound in &node.outbound {
let mut infos = Vec::new();
match outbound.info.addr.as_str() {
"Null" => {
let style = Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::ITALIC);
let name = Span::styled(
format!(" {} ", outbound.info.addr),
style,
);
infos.push(name);
}
addr => {
let name =
Span::styled(format!(" {}", addr), style);
infos.push(name);
if !outbound.info.remote_id.is_empty() {
let remote_id = Span::styled(
format!("({})", outbound.info.remote_id),
style,
);
infos.push(remote_id)
}
}
}
let lines = vec![Line::from(infos)];
let names = ListItem::new(lines);
nodes.push(names);
}
}
}
}
}
SelectableObject::Lilith(lilith) => {
let name_span = Span::raw(&lilith.name);
let lines = vec![Line::from(name_span)];
let names = ListItem::new(lines);
nodes.push(names);
for network in &lilith.networks {
let name = Span::styled(format!(" {}", network.name), style);
let lines = vec![Line::from(name)];
let names = ListItem::new(lines);
nodes.push(names);
}
}
_ => (),
}
}
let nodes =
List::new(nodes).block(Block::default().borders(Borders::ALL)).highlight_symbol(">> ");
f.render_stateful_widget(nodes, slice, &mut self.id_menu.state);
Ok(())
}
fn parse_msg_list(&self, info_id: String) -> DnetViewResult> {
let send_style = Style::default().fg(Color::LightCyan);
let recv_style = Style::default().fg(Color::DarkGray);
let mut texts = Vec::new();
let mut lines = Vec::new();
let log = self.msg_list.msg_map.get(&info_id);
match log {
Some(values) => {
for (i, (t, k, v)) in values.iter().enumerate() {
lines.push(match k.as_str() {
"send" => {
Span::styled(format!("{} {} S: {}", i, t, v), send_style)
}
"recv" => {
Span::styled(format!("{} {} R: {}", i, t, v), recv_style)
}
data => return Err(DnetViewError::UnexpectedData(data.to_string())),
});
}
}
None => return Err(DnetViewError::CannotFindId),
}
for line in lines.clone() {
let text = ListItem::new(line);
texts.push(text);
}
let msg_list = List::new(texts).block(Block::default().borders(Borders::ALL));
Ok(msg_list)
}
fn render_right(
&mut self,
f: &mut Frame<'_, B>,
slice: Rect,
selected: String,
) -> DnetViewResult<()> {
//debug!(target: "dnetview", "render_right() selected ID: {}", selected.clone());
let style = Style::default();
let mut lines = Vec::new();
if self.selectables.is_empty() {
// we have not received any selectable data
return Ok(())
} else {
let info = self.selectables.get(&selected);
match info {
Some(SelectableObject::Node(node)) => {
lines.push(Line::from(Span::styled("Type: Normal", style)));
lines.push(Line::from(Span::styled("Hosts:", style)));
for host in &node.hosts {
lines.push(Line::from(Span::styled(format!(" {}", host), style)));
}
}
Some(SelectableObject::Session(session)) => {
let addr = Span::styled(format!("Addr: {}", session.addr), style);
lines.push(Line::from(addr));
if session.state.is_some() {
let addr = Span::styled(
format!("State: {}", session.state.as_ref().unwrap()),
style,
);
lines.push(Line::from(addr));
}
}
Some(SelectableObject::Slot(slot)) => {
let text = self.parse_msg_list(slot.dnet_id.clone())?;
f.render_stateful_widget(text, slice, &mut self.msg_list.state);
}
Some(SelectableObject::Lilith(_lilith)) => {
lines.push(Line::from(Span::styled("Type: Lilith", style)));
}
Some(SelectableObject::Network(network)) => {
lines.push(Line::from(Span::styled("URLs:", style)));
for url in &network.urls {
lines.push(Line::from(Span::styled(format!(" {}", url), style)));
}
lines.push(Line::from(Span::styled("Hosts:", style)));
for node in &network.nodes {
lines.push(Line::from(Span::styled(format!(" {}", node), style)));
}
}
None => return Err(DnetViewError::NotSelectableObject),
}
}
let graph = Paragraph::new(lines)
.block(Block::default().borders(Borders::ALL))
.style(Style::default());
f.render_widget(graph, slice);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct IdMenu {
pub state: ListState,
pub ids: Vec,
}
impl IdMenu {
pub fn new(ids: Vec) -> IdMenu {
IdMenu { state: ListState::default(), ids }
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.ids.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.ids.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn unselect(&mut self) {
self.state.select(None);
}
}
#[derive(Debug, Clone)]
pub struct MsgList {
pub state: ListState,
pub msg_map: MsgMap,
pub index: usize,
}
impl MsgList {
pub fn new(msg_map: MsgMap, index: usize) -> MsgList {
MsgList { state: ListState::default(), msg_map, index }
}
// TODO: reimplement
//pub fn next(&mut self) {
// let i = match self.state.selected() {
// Some(i) => {
// if i >= self.msg_len - 1 {
// 0
// } else {
// i + 1
// }
// }
// None => 0,
// };
// self.state.select(Some(i));
//}
//pub fn previous(&mut self) {
// let i = match self.state.selected() {
// Some(i) => {
// if i == 0 {
// self.msg_len - 1
// } else {
// i - 1
// }
// }
// None => 0,
// };
// self.state.select(Some(i));
//}
pub fn scroll(&mut self) -> DnetViewResult<()> {
let i = match self.state.selected() {
Some(i) => i + self.index,
None => 0,
};
self.state.select(Some(i));
Ok(())
}
pub fn unselect(&mut self) {
self.state.select(None);
}
}
#[derive(Debug, Clone)]
pub struct NodeInfoView {
pub index: usize,
pub infos: HashMap,
}
impl NodeInfoView {
pub fn new(infos: HashMap) -> NodeInfoView {
let index = 0;
NodeInfoView { index, infos }
}
//pub fn next(&mut self) {
// self.index = (self.index + 1) % self.infos.len();
//}
//pub fn previous(&mut self) {
// if self.index > 0 {
// self.index -= 1;
// } else {
// self.index = self.infos.len() - 1;
// }
//}
}