Rendering now happens from a separate task
All checks were successful
Build and deploy / Build container and manifests (push) Successful in 6m9s

This commit is contained in:
2025-04-16 20:02:43 +02:00
parent 9dab64c2e6
commit fe1d5b8f72
2 changed files with 197 additions and 116 deletions

View File

@@ -1,43 +1,58 @@
use std::cmp::{self, max};
use std::io::Write as _;
use futures::StreamExt;
use git_version::git_version;
use ratatui::Frame;
use ratatui::layout::{Constraint, Flex, Layout, Position, Rect};
use ratatui::prelude::CrosstermBackend;
use ratatui::style::{Style, Stylize as _};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{
Block, BorderType, Cell, Clear, HighlightSpacing, Paragraph, Row, Table, TableState,
};
use ratatui::{Frame, Terminal};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
use tracing::error;
use unicode_width::UnicodeWidthStr;
use crate::io::TerminalHandle;
use crate::tunnel::Tunnel;
#[derive(Default)]
pub struct Renderer {
table_state: TableState,
table_rows: Vec<Vec<Span<'static>>>,
enum Message {
Resize { width: u16, height: u16 },
Redraw,
Rows(Vec<Vec<Span<'static>>>),
Select(Option<usize>),
Rename(Option<String>),
Help(String),
Close,
}
fn command<'c>(key: &'c str, text: &'c str) -> Vec<Span<'c>> {
vec![key.bold().light_cyan(), " ".into(), text.dim()]
struct RendererInner {
state: TableState,
rows: Vec<Vec<Span<'static>>>,
input: Option<String>,
rx: UnboundedReceiver<Message>,
}
impl Renderer {
// NOTE: This needs to be a separate function as the render functions can not be async
pub async fn update(&mut self, tunnels: &[Tunnel], index: Option<usize>) {
self.table_rows = futures::stream::iter(tunnels)
.then(Tunnel::to_row)
.collect::<Vec<_>>()
.await;
self.table_state.select(index);
impl RendererInner {
fn new(rx: UnboundedReceiver<Message>) -> Self {
Self {
state: Default::default(),
rows: Default::default(),
input: None,
rx,
}
}
pub fn compute_footer_text<'a>(&self, rect: Rect) -> (u16, Paragraph<'a>) {
fn compute_footer_text<'a>(&self, rect: Rect) -> (u16, Paragraph<'a>) {
let width = rect.width as usize - 2;
let commands = if self.table_state.selected().is_some() {
fn command<'c>(key: &'c str, text: &'c str) -> Vec<Span<'c>> {
vec![key.bold().light_cyan(), " ".into(), text.dim()]
}
let commands = if self.state.selected().is_some() {
vec![
command("q", "quit"),
command("esc", "deselect"),
@@ -92,7 +107,7 @@ impl Renderer {
(height as u16, Paragraph::new(text).centered().block(block))
}
pub fn render(&mut self, frame: &mut Frame, input: &Option<String>) {
fn render(&mut self, frame: &mut Frame) {
self.render_title(frame, frame.area());
let mut area = frame.area().inner(ratatui::layout::Margin {
@@ -108,12 +123,12 @@ impl Renderer {
self.render_table(frame, chunks[0]);
frame.render_widget(footer, chunks[1]);
if let Some(input) = input {
if let Some(input) = &self.input {
self.render_rename(frame, area, input);
}
}
pub fn render_rename(&self, frame: &mut Frame, area: Rect, input: &str) {
fn render_rename(&self, frame: &mut Frame, area: Rect, input: &str) {
let vertical = Layout::vertical([Constraint::Length(3)]).flex(Flex::Center);
let horizontal = Layout::horizontal([Constraint::Max(max(20, input.width() as u16 + 4))])
.flex(Flex::Center);
@@ -131,7 +146,7 @@ impl Renderer {
frame.set_cursor_position(Position::new(area.x + input.width() as u16 + 2, area.y + 1));
}
pub fn render_title(&self, frame: &mut Frame, rect: Rect) {
fn render_title(&self, frame: &mut Frame, rect: Rect) {
let title = format!("{} ({})", std::env!("CARGO_PKG_NAME"), git_version!()).bold();
let title = Line::from(title).centered();
frame.render_widget(title, rect);
@@ -140,7 +155,7 @@ impl Renderer {
fn compute_widths(&mut self) -> Vec<Constraint> {
let table_header = Tunnel::header();
std::iter::once(&table_header)
.chain(&self.table_rows)
.chain(&self.rows)
.map(|row| row.iter().map(|cell| cell.width() as u16))
.fold(vec![0; table_header.len()], |acc, row| {
acc.into_iter()
@@ -158,7 +173,7 @@ impl Renderer {
let header_style = Style::default().bold().reversed();
let row_style = Style::default();
let rows = self.table_rows.iter().map(|row| {
let rows = self.rows.iter().map(|row| {
row.iter()
.cloned()
.map(Cell::from)
@@ -185,6 +200,114 @@ impl Renderer {
.highlight_symbol(Line::from("> "))
.highlight_spacing(HighlightSpacing::Always);
frame.render_stateful_widget(t, rect, &mut self.table_state);
frame.render_stateful_widget(t, rect, &mut self.state);
}
pub async fn start(
&mut self,
mut terminal: Terminal<CrosstermBackend<TerminalHandle>>,
) -> std::io::Result<()> {
while let Some(message) = self.rx.recv().await {
match message {
Message::Resize { width, height } => {
let rect = Rect::new(0, 0, width, height);
terminal.resize(rect)?;
}
Message::Select(selected) => self.state.select(selected),
Message::Rename(input) => self.input = input,
Message::Rows(rows) => self.rows = rows,
Message::Redraw => {
terminal.draw(|frame| {
self.render(frame);
})?;
}
Message::Help(message) => {
let writer = terminal.backend_mut().writer_mut();
writer.leave_alternate_screen()?;
writer.write_all(message.as_bytes())?;
writer.flush()?;
break;
}
Message::Close => {
break;
}
}
}
Ok(())
}
}
#[derive(Debug, Default, Clone)]
pub struct Renderer {
tx: Option<UnboundedSender<Message>>,
}
impl Renderer {
pub fn start(&mut self, terminal: Terminal<CrosstermBackend<TerminalHandle>>) {
let (tx, rx) = unbounded_channel();
let mut inner = RendererInner::new(rx);
tokio::spawn(async move {
if let Err(err) = inner.start(terminal).await {
error!("{err}");
}
});
self.tx = Some(tx)
}
pub fn select(&self, selected: Option<usize>) {
if let Some(tx) = &self.tx {
tx.send(Message::Select(selected)).ok();
self.redraw();
}
}
pub fn rename(&self, input: &Option<String>) {
if let Some(tx) = &self.tx {
tx.send(Message::Rename(input.clone())).ok();
self.redraw();
}
}
pub fn help(&self, message: String) {
if let Some(tx) = &self.tx {
tx.send(Message::Help(message.replace("\n", "\n\r"))).ok();
}
}
pub fn close(&self) {
if let Some(tx) = &self.tx {
tx.send(Message::Close).ok();
}
}
pub fn resize(&self, width: u16, height: u16) {
if let Some(tx) = &self.tx {
tx.send(Message::Resize { width, height }).ok();
self.redraw();
}
}
pub async fn rows(&self, tunnels: &[Tunnel]) {
if let Some(tx) = &self.tx {
let rows = futures::stream::iter(tunnels)
.then(Tunnel::to_row)
.collect::<Vec<_>>()
.await;
tx.send(Message::Rows(rows)).ok();
self.redraw();
}
}
pub fn redraw(&self) {
if let Some(tx) = &self.tx {
tx.send(Message::Redraw).ok();
}
}
}