Rendering now happens from a separate task
All checks were successful
Build and deploy / Build container and manifests (push) Successful in 6m9s
All checks were successful
Build and deploy / Build container and manifests (push) Successful in 6m9s
This commit is contained in:
parent
9dab64c2e6
commit
fe1d5b8f72
|
@ -1,5 +1,4 @@
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::io::Write;
|
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
@ -57,11 +56,9 @@ pub struct Handler {
|
||||||
user: Option<String>,
|
user: Option<String>,
|
||||||
pty_channel: Option<ChannelId>,
|
pty_channel: Option<ChannelId>,
|
||||||
|
|
||||||
terminal: Option<Terminal<CrosstermBackend<TerminalHandle>>>,
|
|
||||||
renderer: super::Renderer,
|
renderer: super::Renderer,
|
||||||
selected: Option<usize>,
|
selected: Option<usize>,
|
||||||
|
rename_input: Option<String>,
|
||||||
rename_buffer: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler {
|
impl Handler {
|
||||||
|
@ -72,10 +69,10 @@ impl Handler {
|
||||||
tunnels: Default::default(),
|
tunnels: Default::default(),
|
||||||
user: None,
|
user: None,
|
||||||
pty_channel: None,
|
pty_channel: None,
|
||||||
terminal: None,
|
|
||||||
renderer: Default::default(),
|
renderer: Default::default(),
|
||||||
selected: None,
|
selected: None,
|
||||||
rename_buffer: None,
|
rename_input: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,46 +82,6 @@ impl Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn resize(&mut self, width: u32, height: u32) -> std::io::Result<()> {
|
|
||||||
if let Some(terminal) = &mut self.terminal {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: width as u16,
|
|
||||||
height: height as u16,
|
|
||||||
};
|
|
||||||
|
|
||||||
terminal.resize(rect)?;
|
|
||||||
self.redraw().await?;
|
|
||||||
} else {
|
|
||||||
warn!("Resize called without valid terminal");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close(&mut self) -> std::io::Result<()> {
|
|
||||||
if let Some(terminal) = self.terminal.take() {
|
|
||||||
drop(terminal);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn redraw(&mut self) -> std::io::Result<()> {
|
|
||||||
if let Some(terminal) = &mut self.terminal {
|
|
||||||
trace!("redraw");
|
|
||||||
self.renderer.update(&self.tunnels, self.selected).await;
|
|
||||||
terminal.draw(|frame| {
|
|
||||||
self.renderer.render(frame, &self.rename_buffer);
|
|
||||||
})?;
|
|
||||||
} else {
|
|
||||||
warn!("Redraw called without valid terminal");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_access_selection(&mut self, access: TunnelAccess) {
|
async fn set_access_selection(&mut self, access: TunnelAccess) {
|
||||||
if let Some(selected) = self.selected {
|
if let Some(selected) = self.selected {
|
||||||
if let Some(tunnel) = self.tunnels.get_mut(selected) {
|
if let Some(tunnel) = self.tunnels.get_mut(selected) {
|
||||||
|
@ -135,17 +92,17 @@ impl Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_input(&mut self, input: Input) -> std::io::Result<bool> {
|
async fn handle_input(&mut self, input: Input) -> std::io::Result<()> {
|
||||||
if self.rename_buffer.is_some() {
|
if self.rename_input.is_some() {
|
||||||
match input {
|
match input {
|
||||||
Input::Char(c) if c.is_alphanumeric() => {
|
Input::Char(c) if c.is_alphanumeric() => {
|
||||||
self.rename_buffer
|
self.rename_input
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("input buffer should be some")
|
.expect("input buffer should be some")
|
||||||
.push(c.to_ascii_lowercase());
|
.push(c.to_ascii_lowercase());
|
||||||
}
|
}
|
||||||
Input::Backspace => {
|
Input::Backspace => {
|
||||||
self.rename_buffer
|
self.rename_input
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("input buffer should be some")
|
.expect("input buffer should be some")
|
||||||
.pop();
|
.pop();
|
||||||
|
@ -154,85 +111,100 @@ impl Handler {
|
||||||
debug!("Input accepted");
|
debug!("Input accepted");
|
||||||
if let Some(selected) = self.selected
|
if let Some(selected) = self.selected
|
||||||
&& let Some(tunnel) = self.tunnels.get_mut(selected)
|
&& let Some(tunnel) = self.tunnels.get_mut(selected)
|
||||||
&& let Some(buffer) = self.rename_buffer.take()
|
&& let Some(buffer) = self.rename_input.take()
|
||||||
{
|
{
|
||||||
tunnel.set_name(buffer).await;
|
tunnel.set_name(buffer).await;
|
||||||
|
self.renderer.rows(&self.tunnels).await;
|
||||||
} else {
|
} else {
|
||||||
warn!("Trying to rename invalid tunnel");
|
warn!("Trying to rename invalid tunnel");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Input::Esc => {
|
Input::Esc => {
|
||||||
debug!("Input rejected");
|
debug!("Input rejected");
|
||||||
self.rename_buffer = None;
|
self.rename_input = None;
|
||||||
}
|
}
|
||||||
_ => return Ok(false),
|
_ => return Ok(()),
|
||||||
}
|
}
|
||||||
debug!("Input: {:?}", self.rename_buffer);
|
debug!("Input: {:?}", self.rename_input);
|
||||||
|
self.renderer.rename(&self.rename_input);
|
||||||
} else {
|
} else {
|
||||||
match input {
|
match input {
|
||||||
Input::Char('q') => {
|
Input::Char('q') => {
|
||||||
self.close()?;
|
self.renderer.close();
|
||||||
return Ok(false);
|
}
|
||||||
|
Input::Char('k') | Input::Up => {
|
||||||
|
self.previous_row();
|
||||||
|
self.renderer.select(self.selected);
|
||||||
|
}
|
||||||
|
Input::Char('j') | Input::Down => {
|
||||||
|
self.next_row();
|
||||||
|
self.renderer.select(self.selected);
|
||||||
|
}
|
||||||
|
Input::Esc => {
|
||||||
|
self.selected = None;
|
||||||
|
self.renderer.select(self.selected);
|
||||||
}
|
}
|
||||||
Input::Char('k') | Input::Up => self.previous_row(),
|
|
||||||
Input::Char('j') | Input::Down => self.next_row(),
|
|
||||||
Input::Esc => self.selected = None,
|
|
||||||
Input::Char('P') => {
|
Input::Char('P') => {
|
||||||
self.set_access_selection(TunnelAccess::Public).await;
|
self.set_access_selection(TunnelAccess::Public).await;
|
||||||
|
self.renderer.rows(&self.tunnels).await;
|
||||||
}
|
}
|
||||||
Input::Char('p') => {
|
Input::Char('p') => {
|
||||||
if let Some(user) = self.user.clone() {
|
if let Some(user) = self.user.clone() {
|
||||||
self.set_access_selection(TunnelAccess::Private(user)).await;
|
self.set_access_selection(TunnelAccess::Private(user)).await;
|
||||||
|
self.renderer.rows(&self.tunnels).await;
|
||||||
} else {
|
} else {
|
||||||
warn!("User not set");
|
warn!("User not set");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Input::Char('R') => {
|
Input::Char('R') => {
|
||||||
let Some(selected) = self.selected else {
|
let Some(selected) = self.selected else {
|
||||||
return Ok(false);
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(tunnel) = self.tunnels.get_mut(selected) else {
|
let Some(tunnel) = self.tunnels.get_mut(selected) else {
|
||||||
warn!("Trying to retry invalid tunnel");
|
warn!("Trying to retry invalid tunnel");
|
||||||
return Ok(false);
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
tunnel.retry().await;
|
tunnel.retry().await;
|
||||||
|
self.renderer.rows(&self.tunnels).await;
|
||||||
}
|
}
|
||||||
Input::Char('r') => {
|
Input::Char('r') => {
|
||||||
if self.selected.is_some() {
|
if self.selected.is_some() {
|
||||||
trace!("Renaming tunnel");
|
trace!("Renaming tunnel");
|
||||||
self.rename_buffer = Some(String::new());
|
self.rename_input = Some(String::new());
|
||||||
|
self.renderer.rename(&self.rename_input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Input::Delete => {
|
Input::Delete => {
|
||||||
let Some(selected) = self.selected else {
|
let Some(selected) = self.selected else {
|
||||||
return Ok(false);
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
if selected >= self.tunnels.len() {
|
if selected >= self.tunnels.len() {
|
||||||
warn!("Trying to delete tunnel out of bounds");
|
warn!("Trying to delete tunnel out of bounds");
|
||||||
return Ok(false);
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tunnels.remove(selected);
|
self.tunnels.remove(selected);
|
||||||
|
self.renderer.rows(&self.tunnels).await;
|
||||||
|
|
||||||
if self.tunnels.is_empty() {
|
if self.tunnels.is_empty() {
|
||||||
self.selected = None;
|
self.selected = None;
|
||||||
} else {
|
} else {
|
||||||
self.selected = Some(min(self.tunnels.len() - 1, selected));
|
self.selected = Some(min(self.tunnels.len() - 1, selected));
|
||||||
}
|
}
|
||||||
|
self.renderer.select(self.selected);
|
||||||
}
|
}
|
||||||
Input::CtrlP => {
|
Input::CtrlP => {
|
||||||
self.set_access_selection(TunnelAccess::Protected).await;
|
self.set_access_selection(TunnelAccess::Protected).await;
|
||||||
|
self.renderer.rows(&self.tunnels).await;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {}
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_row(&mut self) {
|
fn next_row(&mut self) {
|
||||||
|
@ -316,9 +288,7 @@ impl russh::server::Handler for Handler {
|
||||||
let input: Input = data.into();
|
let input: Input = data.into();
|
||||||
trace!(?input, "input");
|
trace!(?input, "input");
|
||||||
|
|
||||||
if self.handle_input(input).await? {
|
self.handle_input(input).await?;
|
||||||
self.redraw().await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -341,31 +311,17 @@ impl russh::server::Handler for Handler {
|
||||||
if args.make_public() {
|
if args.make_public() {
|
||||||
trace!("Making tunnels public");
|
trace!("Making tunnels public");
|
||||||
self.set_access_all(TunnelAccess::Public).await;
|
self.set_access_all(TunnelAccess::Public).await;
|
||||||
self.redraw().await?;
|
self.renderer.rows(&self.tunnels).await;
|
||||||
} else if args.make_protected() {
|
} else if args.make_protected() {
|
||||||
trace!("Making tunnels protected");
|
trace!("Making tunnels protected");
|
||||||
self.set_access_all(TunnelAccess::Protected).await;
|
self.set_access_all(TunnelAccess::Protected).await;
|
||||||
self.redraw().await?;
|
self.renderer.rows(&self.tunnels).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Sending help message and disconnecting");
|
trace!("Sending help message and disconnecting");
|
||||||
|
|
||||||
if let Some(terminal) = &mut self.terminal {
|
self.renderer.help(err.render().ansi().to_string());
|
||||||
let writer = terminal.backend_mut().writer_mut();
|
|
||||||
|
|
||||||
writer.leave_alternate_screen()?;
|
|
||||||
writer.write_all(
|
|
||||||
err.render()
|
|
||||||
.ansi()
|
|
||||||
.to_string()
|
|
||||||
.replace('\n', "\n\r")
|
|
||||||
.as_bytes(),
|
|
||||||
)?;
|
|
||||||
writer.flush()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.close()?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,7 +367,7 @@ impl russh::server::Handler for Handler {
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
trace!(col_width, row_height, "window_change_request");
|
trace!(col_width, row_height, "window_change_request");
|
||||||
|
|
||||||
self.resize(col_width, row_height).await?;
|
self.renderer.resize(col_width as u16, row_height as u16);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -440,8 +396,10 @@ impl russh::server::Handler for Handler {
|
||||||
let options = TerminalOptions {
|
let options = TerminalOptions {
|
||||||
viewport: Viewport::Fixed(rect),
|
viewport: Viewport::Fixed(rect),
|
||||||
};
|
};
|
||||||
self.terminal = Some(Terminal::with_options(backend, options)?);
|
let terminal = Terminal::with_options(backend, options)?;
|
||||||
self.redraw().await?;
|
self.renderer.start(terminal);
|
||||||
|
|
||||||
|
self.renderer.rows(&self.tunnels).await;
|
||||||
|
|
||||||
self.pty_channel = Some(channel);
|
self.pty_channel = Some(channel);
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,58 @@
|
||||||
use std::cmp::{self, max};
|
use std::cmp::{self, max};
|
||||||
|
use std::io::Write as _;
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use git_version::git_version;
|
use git_version::git_version;
|
||||||
use ratatui::Frame;
|
|
||||||
use ratatui::layout::{Constraint, Flex, Layout, Position, Rect};
|
use ratatui::layout::{Constraint, Flex, Layout, Position, Rect};
|
||||||
|
use ratatui::prelude::CrosstermBackend;
|
||||||
use ratatui::style::{Style, Stylize as _};
|
use ratatui::style::{Style, Stylize as _};
|
||||||
use ratatui::text::{Line, Span, Text};
|
use ratatui::text::{Line, Span, Text};
|
||||||
use ratatui::widgets::{
|
use ratatui::widgets::{
|
||||||
Block, BorderType, Cell, Clear, HighlightSpacing, Paragraph, Row, Table, TableState,
|
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 unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::io::TerminalHandle;
|
||||||
use crate::tunnel::Tunnel;
|
use crate::tunnel::Tunnel;
|
||||||
|
|
||||||
#[derive(Default)]
|
enum Message {
|
||||||
pub struct Renderer {
|
Resize { width: u16, height: u16 },
|
||||||
table_state: TableState,
|
Redraw,
|
||||||
table_rows: Vec<Vec<Span<'static>>>,
|
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>> {
|
struct RendererInner {
|
||||||
vec![key.bold().light_cyan(), " ".into(), text.dim()]
|
state: TableState,
|
||||||
|
rows: Vec<Vec<Span<'static>>>,
|
||||||
|
input: Option<String>,
|
||||||
|
rx: UnboundedReceiver<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl RendererInner {
|
||||||
// NOTE: This needs to be a separate function as the render functions can not be async
|
fn new(rx: UnboundedReceiver<Message>) -> Self {
|
||||||
pub async fn update(&mut self, tunnels: &[Tunnel], index: Option<usize>) {
|
Self {
|
||||||
self.table_rows = futures::stream::iter(tunnels)
|
state: Default::default(),
|
||||||
.then(Tunnel::to_row)
|
rows: Default::default(),
|
||||||
.collect::<Vec<_>>()
|
input: None,
|
||||||
.await;
|
rx,
|
||||||
|
}
|
||||||
self.table_state.select(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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![
|
vec![
|
||||||
command("q", "quit"),
|
command("q", "quit"),
|
||||||
command("esc", "deselect"),
|
command("esc", "deselect"),
|
||||||
|
@ -92,7 +107,7 @@ impl Renderer {
|
||||||
(height as u16, Paragraph::new(text).centered().block(block))
|
(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());
|
self.render_title(frame, frame.area());
|
||||||
|
|
||||||
let mut area = frame.area().inner(ratatui::layout::Margin {
|
let mut area = frame.area().inner(ratatui::layout::Margin {
|
||||||
|
@ -108,12 +123,12 @@ impl Renderer {
|
||||||
self.render_table(frame, chunks[0]);
|
self.render_table(frame, chunks[0]);
|
||||||
frame.render_widget(footer, chunks[1]);
|
frame.render_widget(footer, chunks[1]);
|
||||||
|
|
||||||
if let Some(input) = input {
|
if let Some(input) = &self.input {
|
||||||
self.render_rename(frame, area, 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 vertical = Layout::vertical([Constraint::Length(3)]).flex(Flex::Center);
|
||||||
let horizontal = Layout::horizontal([Constraint::Max(max(20, input.width() as u16 + 4))])
|
let horizontal = Layout::horizontal([Constraint::Max(max(20, input.width() as u16 + 4))])
|
||||||
.flex(Flex::Center);
|
.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));
|
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 = format!("{} ({})", std::env!("CARGO_PKG_NAME"), git_version!()).bold();
|
||||||
let title = Line::from(title).centered();
|
let title = Line::from(title).centered();
|
||||||
frame.render_widget(title, rect);
|
frame.render_widget(title, rect);
|
||||||
|
@ -140,7 +155,7 @@ impl Renderer {
|
||||||
fn compute_widths(&mut self) -> Vec<Constraint> {
|
fn compute_widths(&mut self) -> Vec<Constraint> {
|
||||||
let table_header = Tunnel::header();
|
let table_header = Tunnel::header();
|
||||||
std::iter::once(&table_header)
|
std::iter::once(&table_header)
|
||||||
.chain(&self.table_rows)
|
.chain(&self.rows)
|
||||||
.map(|row| row.iter().map(|cell| cell.width() as u16))
|
.map(|row| row.iter().map(|cell| cell.width() as u16))
|
||||||
.fold(vec![0; table_header.len()], |acc, row| {
|
.fold(vec![0; table_header.len()], |acc, row| {
|
||||||
acc.into_iter()
|
acc.into_iter()
|
||||||
|
@ -158,7 +173,7 @@ impl Renderer {
|
||||||
let header_style = Style::default().bold().reversed();
|
let header_style = Style::default().bold().reversed();
|
||||||
let row_style = Style::default();
|
let row_style = Style::default();
|
||||||
|
|
||||||
let rows = self.table_rows.iter().map(|row| {
|
let rows = self.rows.iter().map(|row| {
|
||||||
row.iter()
|
row.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(Cell::from)
|
.map(Cell::from)
|
||||||
|
@ -185,6 +200,114 @@ impl Renderer {
|
||||||
.highlight_symbol(Line::from("> "))
|
.highlight_symbol(Line::from("> "))
|
||||||
.highlight_spacing(HighlightSpacing::Always);
|
.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user