/* 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 std::collections::HashMap;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
Frame,
};
use darkfi::util::time::NanoTimestamp;
use crate::{
error::{DnetViewError, DnetViewResult},
model::{NodeInfo, SelectableObject},
};
//use log::debug;
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();
}
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());
}
}
}
fn update_selectable(&mut self, selectables: HashMap) {
for (id, obj) in selectables {
self.selectables.insert(id, obj);
}
}
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.id) {
self.ordered_list.push(node.id.clone());
}
if !node.is_offline {
for session in &node.children {
if !session.is_empty {
if !self.ordered_list.iter().any(|i| i == &session.id) {
self.ordered_list.push(session.id.clone());
}
for connection in &session.children {
if !self.ordered_list.iter().any(|i| i == &connection.id) {
self.ordered_list.push(connection.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());
}
}
}
_ => (),
}
}
//debug!(target: "dnetview", "render_ids()::ordered_list: {:?}", self.ordered_list);
}
// TODO: this function is dynamically resizing the msgs index
// according to what set of msgs 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_ids(f, slice.clone())?;
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) => {
//debug!(target: "dnetview", "render()::selected index: {}", i);
match self.ordered_list.get(i) {
Some(i) => {
let id = i.clone();
self.render_info(f, slice, id)?;
Ok(())
}
None => Err(DnetViewError::NoIdAtIndex),
}
}
// nothing is selected right now
None => Ok(()),
}
}
}
fn render_ids(
&mut self,
f: &mut Frame<'_, B>,
slice: Vec,
) -> 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::Blue).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![Spans::from(name_span)];
let names = ListItem::new(lines);
nodes.push(names);
} else {
let name_span = Span::raw(&node.name);
let lines = vec![Spans::from(name_span)];
let names = ListItem::new(lines);
nodes.push(names);
for session in &node.children {
if !session.is_empty {
let name = Span::styled(format!(" {}", session.name), style);
let lines = vec![Spans::from(name)];
let names = ListItem::new(lines);
nodes.push(names);
for connection in &session.children {
let mut info = Vec::new();
match connection.addr.as_str() {
"Null" => {
let style = Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::ITALIC);
let name = Span::styled(
format!(" {} ", connection.addr),
style,
);
info.push(name);
}
addr => {
let name = Span::styled(
format!(
" {} ({})",
addr, connection.remote_node_id
),
style,
);
info.push(name);
}
}
let lines = vec![Spans::from(info)];
let names = ListItem::new(lines);
nodes.push(names);
}
}
}
}
}
SelectableObject::Lilith(lilith) => {
let name_span = Span::raw(&lilith.name);
let lines = vec![Spans::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![Spans::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[0], &mut self.id_menu.state);
Ok(())
}
fn parse_msg_list(&self, connect_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(&connect_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_info(
&mut self,
f: &mut Frame<'_, B>,
slice: Vec,
selected: String,
) -> DnetViewResult<()> {
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);
//debug!(target: "dnetview", "render_info()::selected {}", selected);
match info {
Some(SelectableObject::Node(node)) => {
//debug!(target: "dnetview", "render_info()::SelectableObject::Node");
lines.push(Spans::from(Span::styled("Type: Normal", style)));
match &node.external_addr {
Some(addr) => {
let node_info = Span::styled(format!("External addr: {}", addr), style);
lines.push(Spans::from(node_info));
}
None => {
let node_info = Span::styled("External addr: Null".to_string(), style);
lines.push(Spans::from(node_info));
}
}
lines.push(Spans::from(Span::styled(
format!("P2P state: {}", node.state),
style,
)));
}
Some(SelectableObject::Session(session)) => {
//debug!(target: "dnetview", "render_info()::SelectableObject::Session");
if session.accept_addr.is_some() {
let accept_addr = Span::styled(
format!("Accept addr: {}", session.accept_addr.as_ref().unwrap()),
style,
);
lines.push(Spans::from(accept_addr));
}
if session.hosts.is_some() {
let hosts = Span::styled("Hosts:".to_string(), style);
lines.push(Spans::from(hosts));
for host in session.hosts.as_ref().unwrap() {
let host = Span::styled(format!(" {}", host), style);
lines.push(Spans::from(host));
}
}
}
Some(SelectableObject::Connect(connect)) => {
//debug!(target: "dnetview", "render_info()::SelectableObject::Connect");
let text = self.parse_msg_list(connect.id.clone())?;
f.render_stateful_widget(text, slice[1], &mut self.msg_list.state);
}
Some(SelectableObject::Lilith(lilith)) => {
lines.push(Spans::from(Span::styled("Type: Lilith", style)));
lines.push(Spans::from(Span::styled("URLs:", style)));
for url in &lilith.urls {
lines.push(Spans::from(Span::styled(format!(" {}", url), style)));
}
}
Some(SelectableObject::Network(network)) => {
lines.push(Spans::from(Span::styled("URLs:", style)));
for url in &network.urls {
lines.push(Spans::from(Span::styled(format!(" {}", url), style)));
}
lines.push(Spans::from(Span::styled("Hosts:", style)));
for node in &network.nodes {
lines.push(Spans::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[1]);
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;
// }
//}
}