joecalsend/src/app/widgets.rs

269 lines
8.3 KiB
Rust

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<Line> = LazyLock::new(|| {
Line::from(vec![
" Send ".into(),
"<S>".blue().bold(),
" Receive ".into(),
"<R>".blue().bold(),
" Logs ".into(),
"<L>".blue().bold(),
" Previous Screen ".into(),
"<ESC>".blue().bold(),
" Quit ".into(),
"<Q>".blue().bold(),
])
});
static LOGGING_MENU: LazyLock<Line> = LazyLock::new(|| {
Line::from(vec![
" Reduce Logging Level ".into(),
"<LEFT>".blue().bold(),
" Increase Logging Level ".into(),
"<RIGHT>".blue().bold(),
" Previous Screen ".into(),
"<ESC>".blue().bold(),
" Quit ".into(),
"<Q>".blue().bold(),
])
});
static UPLOADS_MENU: LazyLock<Line> = LazyLock::new(|| {
Line::from(vec![
" Select Previous ".into(),
"<UP>".blue().bold(),
" Select Next ".into(),
"<DOWN>".blue().bold(),
" Approve Selection ".into(),
"<A>".blue().bold(),
" Deny Selection ".into(),
"<D>".blue().bold(),
" Previous Screen ".into(),
"<ESC>".blue().bold(),
" Quit ".into(),
"<Q>".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::<Vec<_>>();
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);
}
}