use std::sync::LazyLock; use joecalsend::JoecalUploadRequest; use log::LevelFilter; use ratatui::{ buffer::Buffer, layout::{Constraint, Layout, Margin, Rect}, style::{Color, Style, Stylize}, symbols::border, text::{Line, Text, ToLine}, widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Row, Table, TableState, Widget}, }; use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState}; use super::{App, CurrentScreen, Peers}; static MAIN_MENU: LazyLock = LazyLock::new(|| { Line::from(vec![ " Send ".into(), "".blue().bold(), " Receive ".into(), "".blue().bold(), " Logs ".into(), "".blue().bold(), " Previous Screen ".into(), "".blue().bold(), " Quit ".into(), "".blue().bold(), ]) }); static LOGGING_MENU: LazyLock = LazyLock::new(|| { Line::from(vec![ " Reduce Logging Level ".into(), "".blue().bold(), " Increase Logging Level ".into(), "".blue().bold(), " Previous Screen ".into(), "".blue().bold(), " Quit ".into(), "".blue().bold(), ]) }); static UPLOADS_MENU: LazyLock = LazyLock::new(|| { Line::from(vec![ " Select Previous ".into(), "".blue().bold(), " Select Next ".into(), "".blue().bold(), " Approve Selection ".into(), "".blue().bold(), " Deny Selection ".into(), "".blue().bold(), " Previous Screen ".into(), "".blue().bold(), " Quit ".into(), "".blue().bold(), ]) }); impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer) { let main_layout = Layout::vertical([Constraint::Min(5), Constraint::Min(10), Constraint::Min(3)]); let [top, _middle, bottom] = main_layout.areas(area); let footer_layout = Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]); let [footer_left, footer_right] = footer_layout.areas(bottom); let footer_margin = Margin::new(1, 1); let header_layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); let [header_left, header_right] = header_layout.areas(top); let header_margin = Margin::new(1, 2); let mode = self.screen.last().unwrap(); let ups: Vec<_> = self.uploads.values().collect(); match mode { CurrentScreen::Main => { main_page(*mode, &MAIN_MENU, area, buf); logger(header_right.inner(header_margin), buf); let peers = PeersWidget { peers: &self.peers }; peers.render(footer_right.inner(footer_margin), buf); NetworkInfoWidget.render(footer_left.inner(footer_margin), buf); uploads( &ups, &mut self.upload_state, header_left.inner(header_margin), buf, ); } CurrentScreen::Logging => { main_page(*mode, &LOGGING_MENU, area, buf); logger(area.inner(Margin::new(2, 4)), buf); } CurrentScreen::Receiving => { main_page(*mode, &UPLOADS_MENU, area, buf); uploads( &ups, &mut self.upload_state, area.inner(Margin::new(2, 4)), buf, ); } _ => { main_page(*mode, &MAIN_MENU, area, buf); } } } } fn logger(area: Rect, buf: &mut Buffer) { let title = Line::from(log::max_level().as_str()); let logger = TuiLoggerWidget::default() .output_separator('|') .output_timestamp(Some("%H:%M:%S%.3f".to_string())) .output_level(Some(TuiLoggerLevelOutput::Abbreviated)) .output_target(true) .output_file(false) .output_line(false) .block(Block::bordered().title(title.centered())) .style(Style::default()) .state(&TuiWidgetState::new().set_default_display_level(LevelFilter::Debug)); logger.render(area, buf); } fn main_page(screen: CurrentScreen, menu: &Line, area: Rect, buf: &mut Buffer) { let title = Line::from(" Joecalsend ".bold()); let block = Block::bordered() .title(title.centered()) .title_bottom(menu.clone().centered()) .border_set(border::THICK); let current_screen = format!("{screen:?}",); let text = Text::from(Line::from(current_screen.yellow())); Paragraph::new(text) .centered() .block(block) .render(area, buf); } fn uploads( requests: &[&JoecalUploadRequest], state: &mut TableState, area: Rect, buf: &mut Buffer, ) { let title = Line::from(" Upload Requests ").bold(); let block = Block::bordered().title(title.centered()); let mut rows = Vec::new(); for &req in requests { let src = req.alias.to_line().left_aligned(); let mut size = 0; let files = req .files .values() .map(|f| { size += f.size; f.file_name.clone() }) .collect::>(); let files = files.join(", "); let files = Line::from(files).centered(); let size = Line::from(format!("{size}")).centered(); rows.push(Row::new([src, size, files])); } if state.selected().is_none() && !rows.is_empty() { state.select(Some(0)); } else if rows.is_empty() { state.select(None); }; let widths = [ Constraint::Max(20), Constraint::Max(15), Constraint::Min(50), ]; let table = Table::new(rows, widths) .block(block) .header(Row::new([ "Sender".bold().into_left_aligned_line(), "Bytes".bold().into_centered_line(), "Files".bold().into_centered_line(), ])) .row_highlight_style(Style::new().bg(Color::Rgb(99, 99, 99))); ratatui::widgets::StatefulWidget::render(table, area, buf, state); } #[derive(Debug, Clone)] pub struct PeersWidget<'p> { pub peers: &'p Peers, } impl<'p> Widget for PeersWidget<'p> { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { let mut items = Vec::with_capacity(self.peers.len()); for (k, v) in self.peers.iter() { let item = format!("{:?}: {} ({})", k, v.0, v.1); let s = Line::from(item.yellow()); let item = ListItem::new(s); items.push(item); } let title = Line::from(" Peers ".bold()).centered(); let block = Block::default() .title(title) .borders(Borders::all()) .padding(Padding::uniform(1)); let list = List::new(items).block(block); list.render(area, buf); } } #[derive(Debug, Clone)] pub struct NetworkInfoWidget; impl Widget for NetworkInfoWidget { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { let udp = "UDP socket".yellow(); let udp = udp.to_line().left_aligned(); let uaddr = format!("{:?}", joecalsend::LISTENING_SOCKET_ADDR).yellow(); let udp = Row::new(vec![udp, uaddr.to_line().right_aligned()]); let mip = format!( "{:?}:{:?}", joecalsend::MULTICAST_IP, joecalsend::DEFAULT_PORT ) .yellow(); let multicast = "Multicast address".yellow(); let multicast = Row::new(vec![ multicast.to_line().left_aligned(), mip.to_line().right_aligned(), ]); let haddr = format!("{:?}", joecalsend::LISTENING_SOCKET_ADDR).yellow(); let http = "HTTP address".yellow(); let http = Row::new(vec![ http.to_line().left_aligned(), haddr.to_line().right_aligned(), ]); let rows = vec![udp, multicast, http]; let widths = vec![Constraint::Percentage(50), Constraint::Percentage(50)]; let title = Line::from(" Listeners ".bold()).centered(); let block = Block::default() .title(title) .borders(Borders::all()) .padding(Padding::uniform(1)); let table = Table::new(rows, widths).block(block); table.render(area, buf); } }