/* 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; // } //} }