use std::sync::LazyLock; use joecalsend::ReceiveRequest; use log::LevelFilter; use ratatui::{ buffer::Buffer, layout::{Constraint, Flex, Layout, Margin, Rect}, style::{Color, Style, Stylize}, symbols::border, text::{Line, Text, ToLine}, widgets::{ Block, Borders, List, ListItem, ListState, Padding, Paragraph, Row, Table, TableState, Widget, }, }; use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState}; use super::{App, CurrentScreen, Peer, SendingScreen}; 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 CONTENT_RECEIVE_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(), ]) }); static CONTENT_SEND_FILE_MENU: LazyLock = LazyLock::new(|| { Line::from(vec![ " Select Previous ".into(), "".blue().bold(), " Select Next ".into(), "".blue().bold(), " Select ".into(), "".blue().bold(), " Parent Dir ".into(), "".blue().bold(), " Child Dir ".into(), "".blue().bold(), " Enter Text ".into(), "".blue().bold(), " Peers ".into(), "".blue().bold(), " Previous Screen ".into(), "".blue().bold(), " Quit ".into(), "".blue().bold(), ]) }); static CONTENT_SEND_PEERS_MENU: LazyLock = LazyLock::new(|| { Line::from(vec![ " Select Previous ".into(), "".blue().bold(), " Select Next ".into(), "".blue().bold(), " Select ".into(), "".blue().bold(), " Enter Text ".into(), "".blue().bold(), " Files ".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(3)]); let [top, 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 subscreen_margin = Margin::new(2, 4); let current_screen = self.screen.last().unwrap(); match current_screen { CurrentScreen::Main => { let rx_reqs: Vec<_> = self.receive_requests.values().collect(); outer_frame(*current_screen, &MAIN_MENU, area, buf); logger(header_right.inner(header_margin), buf); let peers = PeersWidget { peers: &self.peers }; ratatui::widgets::StatefulWidget::render( peers, footer_right.inner(footer_margin), buf, &mut self.peer_state, ); NetworkInfoWidget.render(footer_left.inner(footer_margin), buf); receive_requests( &rx_reqs, &mut self.receiving_state, header_left.inner(header_margin), buf, ); } CurrentScreen::Logging => { outer_frame(*current_screen, &LOGGING_MENU, area, buf); logger(area.inner(subscreen_margin), buf); } CurrentScreen::Receiving => { let rx_reqs: Vec<_> = self.receive_requests.values().collect(); outer_frame(*current_screen, &CONTENT_RECEIVE_MENU, area, buf); receive_requests( &rx_reqs, &mut self.receiving_state, top.inner(subscreen_margin), buf, ); logger(bottom.inner(subscreen_margin), buf); } CurrentScreen::Sending(s) => { match s { SendingScreen::Files => { outer_frame(*current_screen, &CONTENT_SEND_FILE_MENU, area, buf) } SendingScreen::Peers => { outer_frame(*current_screen, &CONTENT_SEND_PEERS_MENU, area, buf) } SendingScreen::Text => {} } let peers = PeersWidget { peers: &self.peers }; self.file_picker .widget() .render(header_left.inner(header_margin), buf); logger(header_right.inner(header_margin), buf); ratatui::widgets::StatefulWidget::render( peers, bottom.inner(subscreen_margin), buf, &mut self.peer_state, ); } _ => { outer_frame(*current_screen, &MAIN_MENU, area, buf); } } } } fn outer_frame(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 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 receive_requests( requests: &[&ReceiveRequest], 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]).yellow()); } 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 [Peer], } impl<'p> ratatui::widgets::StatefulWidget for PeersWidget<'p> { type State = ListState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) where Self: Sized, { if self.peers.is_empty() { state.select(None); } if state.selected().is_none() { state.select(Some(0)); } let mut items = Vec::with_capacity(self.peers.len()); for Peer { addr, alias, fingerprint, } in self.peers.iter() { let item = format!("{:?}: {} ({})", addr, alias, fingerprint); 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) .highlight_style(Style::new().bg(Color::Rgb(99, 99, 99))); ratatui::widgets::StatefulWidget::render(list, area, buf, state); } } #[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"; let udp = udp.to_line().left_aligned(); let uaddr = format!("{:?}", joecalsend::LISTENING_SOCKET_ADDR); let udp = Row::new(vec![udp, uaddr.to_line().right_aligned()]).yellow(); let mip = format!( "{:?}:{:?}", joecalsend::MULTICAST_IP, joecalsend::DEFAULT_PORT ); let multicast = "Multicast address"; let multicast = Row::new(vec![ multicast.to_line().left_aligned(), mip.to_line().right_aligned(), ]) .yellow(); let haddr = format!("{:?}", joecalsend::LISTENING_SOCKET_ADDR); let http = "HTTP address"; let http = Row::new(vec![ http.to_line().left_aligned(), haddr.to_line().right_aligned(), ]) .yellow(); 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); } } // helpers fn centered_rect(area: Rect, width_pct: u16, height_pct: u16) -> Rect { let horizontal = Layout::horizontal([Constraint::Percentage(width_pct)]).flex(Flex::Center); let vertical = Layout::vertical([Constraint::Percentage(height_pct)]).flex(Flex::Center); let [area] = vertical.areas(area); let [area] = horizontal.areas(area); area }