#![warn(rust_2018_idioms)]
#[deny(clippy::shadow_unrelated)]
mod app;
mod banner;
mod cmd;
mod event;
mod handlers;
mod network;
mod ui;
use std::{
io::{self, stdout, Stdout},
panic::{self, PanicInfo},
sync::Arc,
};
use anyhow::{anyhow, Result};
use app::App;
use banner::BANNER;
use clap::Parser;
use cmd::{CmdRunner, IoCmdEvent};
use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use event::Key;
use network::{
get_client,
stream::{IoStreamEvent, NetworkStream},
IoEvent, Network,
};
use ratatui::{
backend::{Backend, CrosstermBackend},
Terminal,
};
use tokio::sync::{mpsc, Mutex};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None, override_usage = "Press `?` while running the app to see keybindings", before_help = BANNER)]
pub struct Cli {
#[arg(short, long, value_parser, default_value_t = 250)]
pub tick_rate: u64,
#[arg(short, long, value_parser, default_value_t = 5000)]
pub poll_rate: u64,
#[arg(short, long, value_parser, default_value_t = true)]
pub enhanced_graphics: bool,
}
#[tokio::main]
async fn main() -> Result<()> {
openssl_probe::init_ssl_cert_env_vars();
panic::set_hook(Box::new(|info| {
panic_hook(info);
}));
let cli = Cli::parse();
if cli.tick_rate >= 1000 {
panic!("Tick rate must be below 1000");
}
if (cli.poll_rate % cli.tick_rate) > 0u64 {
panic!("Poll rate must be multiple of tick-rate");
}
let (sync_io_tx, sync_io_rx) = mpsc::channel::<IoEvent>(500);
let (sync_io_stream_tx, sync_io_stream_rx) = mpsc::channel::<IoStreamEvent>(500);
let (sync_io_cmd_tx, sync_io_cmd_rx) = mpsc::channel::<IoCmdEvent>(500);
let app = Arc::new(Mutex::new(App::new(
sync_io_tx,
sync_io_stream_tx,
sync_io_cmd_tx,
cli.enhanced_graphics,
cli.poll_rate / cli.tick_rate,
)));
let app_nw = Arc::clone(&app);
let app_stream = Arc::clone(&app);
let app_cli = Arc::clone(&app);
std::thread::spawn(move || {
start_network(sync_io_rx, &app_nw);
});
std::thread::spawn(move || {
start_stream_network(sync_io_stream_rx, &app_stream);
});
std::thread::spawn(move || {
start_cmd_runner(sync_io_cmd_rx, &app_cli);
});
start_ui(cli, &app).await?;
Ok(())
}
#[tokio::main]
async fn start_network(mut io_rx: mpsc::Receiver<IoEvent>, app: &Arc<Mutex<App>>) {
match get_client(None).await {
Ok(client) => {
let mut network = Network::new(client, app);
while let Some(io_event) = io_rx.recv().await {
network.handle_network_event(io_event).await;
}
}
Err(e) => {
let mut app = app.lock().await;
app.handle_error(anyhow!("Unable to obtain Kubernetes client. {:?}", e));
}
}
}
#[tokio::main]
async fn start_stream_network(mut io_rx: mpsc::Receiver<IoStreamEvent>, app: &Arc<Mutex<App>>) {
match get_client(None).await {
Ok(client) => {
let mut network = NetworkStream::new(client, app);
while let Some(io_event) = io_rx.recv().await {
network.handle_network_stream_event(io_event).await;
}
}
Err(e) => {
let mut app = app.lock().await;
app.handle_error(anyhow!("Unable to obtain Kubernetes client. {:?}", e));
}
}
}
#[tokio::main]
async fn start_cmd_runner(mut io_rx: mpsc::Receiver<IoCmdEvent>, app: &Arc<Mutex<App>>) {
let mut cmd = CmdRunner::new(app);
while let Some(io_event) = io_rx.recv().await {
cmd.handle_cmd_event(io_event).await;
}
}
async fn start_ui(cli: Cli, app: &Arc<Mutex<App>>) -> Result<()> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
terminal.hide_cursor()?;
let events = event::Events::new(cli.tick_rate);
let mut is_first_render = true;
loop {
let mut app = app.lock().await;
if let Ok(size) = terminal.backend().size() {
if app.refresh || app.size != size {
app.size = size;
if app.size.width > 8 {
app.table_cols = app.size.width - 1;
} else {
app.table_cols = 2;
}
}
};
terminal.draw(|f| ui::draw(f, &mut app))?;
match events.next()? {
event::Event::Input(key_event) => {
let key = Key::from(key_event);
if key == Key::Ctrl('c') {
break;
}
handlers::handle_key_events(key, key_event, &mut app).await
}
event::Event::MouseInput(mouse) => handlers::handle_mouse_events(mouse, &mut app).await,
event::Event::Tick => {
app.on_tick(is_first_render).await;
}
}
is_first_render = false;
if app.should_quit {
break;
}
}
terminal.show_cursor()?;
shutdown(terminal)?;
Ok(())
}
fn shutdown(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
terminal.show_cursor()?;
Ok(())
}
#[cfg(debug_assertions)]
fn panic_hook(info: &PanicInfo<'_>) {
use backtrace::Backtrace;
use crossterm::style::Print;
let (msg, location) = get_panic_info(info);
let stacktrace: String = format!("{:?}", Backtrace::new()).replace('\n', "\n\r");
disable_raw_mode().unwrap();
execute!(
io::stdout(),
LeaveAlternateScreen,
Print(format!(
"thread '<unnamed>' panicked at '{}', {}\n\r{}",
msg, location, stacktrace
)),
)
.unwrap();
}
#[cfg(not(debug_assertions))]
fn panic_hook(info: &PanicInfo<'_>) {
use crossterm::style::Print;
use human_panic::{handle_dump, print_msg, Metadata};
let meta = Metadata {
version: env!("CARGO_PKG_VERSION").into(),
name: env!("CARGO_PKG_NAME").into(),
authors: env!("CARGO_PKG_AUTHORS").replace(":", ", ").into(),
homepage: env!("CARGO_PKG_HOMEPAGE").into(),
};
let file_path = handle_dump(&meta, info);
let (msg, location) = get_panic_info(info);
disable_raw_mode().unwrap();
execute!(
io::stdout(),
LeaveAlternateScreen,
Print(format!("Error: '{}' at {}\n", msg, location)),
)
.unwrap();
print_msg(file_path, &meta).expect("human-panic: printing error message to console failed");
}
fn get_panic_info(info: &PanicInfo<'_>) -> (String, String) {
let location = info.location().unwrap();
let msg = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<Any>",
},
};
(msg.to_string(), format!("{}", location))
}