use arboard::Clipboard;
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, Terminal};
use std::io;
use std::time::Duration;
use tokio::sync::mpsc;
use super::dashboard::Dashboard;
use crate::metrics::MetricsCollector;
pub struct TuiApp {
metrics: MetricsCollector,
shutdown_tx: mpsc::Sender<()>,
}
impl TuiApp {
pub fn new(metrics: MetricsCollector, shutdown_tx: mpsc::Sender<()>) -> Self {
Self {
metrics,
shutdown_tx,
}
}
pub async fn run(self) -> io::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let result = self.run_loop(&mut terminal).await;
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
result
}
async fn run_loop(
&self,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> io::Result<()> {
let tick_rate = Duration::from_millis(100);
let mut last_tick = std::time::Instant::now();
let mut clipboard = Clipboard::new().ok();
let mut copy_feedback: Option<(std::time::Instant, bool)> = None;
loop {
if last_tick.elapsed() >= Duration::from_secs(1) {
self.metrics.tick();
last_tick = std::time::Instant::now();
}
if let Some((time, _)) = copy_feedback {
if time.elapsed() > Duration::from_secs(2) {
copy_feedback = None;
}
}
let snapshot = self.metrics.snapshot();
let feedback = copy_feedback.map(|(_, success)| success);
terminal.draw(|f| Dashboard::render(f, &snapshot, feedback))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
match event::read()? {
Event::Key(key) => {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
let _ = self.shutdown_tx.send(()).await;
return Ok(());
}
KeyCode::Char('c')
if key.modifiers.contains(KeyModifiers::CONTROL) =>
{
let _ = self.shutdown_tx.send(()).await;
return Ok(());
}
KeyCode::Char('c') => {
if let Some(ref info) = snapshot.tunnel_info {
if let Some(ref mut cb) = clipboard {
let success = cb.set_text(info.url.clone()).is_ok();
copy_feedback =
Some((std::time::Instant::now(), success));
} else {
copy_feedback =
Some((std::time::Instant::now(), false));
}
}
}
_ => {}
}
}
}
Event::Resize(_, _) => {
terminal.clear()?;
}
_ => {}
}
}
}
}
}