use std::sync::Arc;
use color_eyre::eyre::eyre;
use crossterm::event::KeyEvent;
use ratatui::prelude::Rect;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info};
use crate::{
action::Action,
api::ApiClient,
components::{Component, command_log::CommandLog, health::Health, stamps::Stamps},
config::Config,
log_capture,
tui::{Event, Tui},
watch::BeeWatch,
};
pub struct App {
config: Config,
tick_rate: f64,
frame_rate: f64,
screens: Vec<Box<dyn Component>>,
current_screen: usize,
command_log: Box<dyn Component>,
should_quit: bool,
should_suspend: bool,
mode: Mode,
last_tick_key_events: Vec<KeyEvent>,
action_tx: mpsc::UnboundedSender<Action>,
action_rx: mpsc::UnboundedReceiver<Action>,
root_cancel: CancellationToken,
#[allow(dead_code)]
api: Arc<ApiClient>,
watch: BeeWatch,
}
const SCREEN_NAMES: &[&str] = &["Health", "Stamps"];
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Mode {
#[default]
Home,
}
impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> color_eyre::Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
let config = Config::new()?;
let node = config
.active_node()
.ok_or_else(|| eyre!("no Bee node configured (config.nodes is empty)"))?;
let api = Arc::new(ApiClient::from_node(node)?);
let root_cancel = CancellationToken::new();
let watch = BeeWatch::start(api.clone(), &root_cancel);
let health = Health::new(api.clone(), watch.health());
let stamps = Stamps::new(watch.stamps());
let command_log: Box<dyn Component> = Box::new(CommandLog::new(log_capture::handle()));
Ok(Self {
tick_rate,
frame_rate,
screens: vec![Box::new(health), Box::new(stamps)],
current_screen: 0,
command_log,
should_quit: false,
should_suspend: false,
config,
mode: Mode::Home,
last_tick_key_events: Vec::new(),
action_tx,
action_rx,
root_cancel,
api,
watch,
})
}
pub async fn run(&mut self) -> color_eyre::Result<()> {
let mut tui = Tui::new()?
.tick_rate(self.tick_rate)
.frame_rate(self.frame_rate);
tui.enter()?;
let tx = self.action_tx.clone();
let cfg = self.config.clone();
let size = tui.size()?;
for component in self.iter_components_mut() {
component.register_action_handler(tx.clone())?;
component.register_config_handler(cfg.clone())?;
component.init(size)?;
}
let action_tx = self.action_tx.clone();
loop {
self.handle_events(&mut tui).await?;
self.handle_actions(&mut tui)?;
if self.should_suspend {
tui.suspend()?;
action_tx.send(Action::Resume)?;
action_tx.send(Action::ClearScreen)?;
tui.enter()?;
} else if self.should_quit {
tui.stop()?;
break;
}
}
self.watch.shutdown();
self.root_cancel.cancel();
tui.exit()?;
Ok(())
}
async fn handle_events(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
let Some(event) = tui.next_event().await else {
return Ok(());
};
let action_tx = self.action_tx.clone();
match event {
Event::Quit => action_tx.send(Action::Quit)?,
Event::Tick => action_tx.send(Action::Tick)?,
Event::Render => action_tx.send(Action::Render)?,
Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
Event::Key(key) => self.handle_key_event(key)?,
_ => {}
}
for component in self.iter_components_mut() {
if let Some(action) = component.handle_events(Some(event.clone()))? {
action_tx.send(action)?;
}
}
Ok(())
}
fn iter_components_mut(&mut self) -> impl Iterator<Item = &mut Box<dyn Component>> {
self.screens
.iter_mut()
.chain(std::iter::once(&mut self.command_log))
}
fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<()> {
let action_tx = self.action_tx.clone();
if matches!(key.code, crossterm::event::KeyCode::Tab) {
if !self.screens.is_empty() {
self.current_screen = (self.current_screen + 1) % self.screens.len();
debug!(
"switched to screen {}",
SCREEN_NAMES.get(self.current_screen).unwrap_or(&"?")
);
}
return Ok(());
}
let Some(keymap) = self.config.keybindings.0.get(&self.mode) else {
return Ok(());
};
match keymap.get(&vec![key]) {
Some(action) => {
info!("Got action: {action:?}");
action_tx.send(action.clone())?;
}
_ => {
self.last_tick_key_events.push(key);
if let Some(action) = keymap.get(&self.last_tick_key_events) {
info!("Got action: {action:?}");
action_tx.send(action.clone())?;
}
}
}
Ok(())
}
fn handle_actions(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
while let Ok(action) = self.action_rx.try_recv() {
if action != Action::Tick && action != Action::Render {
debug!("{action:?}");
}
match action {
Action::Tick => {
self.last_tick_key_events.drain(..);
}
Action::Quit => self.should_quit = true,
Action::Suspend => self.should_suspend = true,
Action::Resume => self.should_suspend = false,
Action::ClearScreen => tui.terminal.clear()?,
Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
Action::Render => self.render(tui)?,
_ => {}
}
let tx = self.action_tx.clone();
for component in self.iter_components_mut() {
if let Some(action) = component.update(action.clone())? {
tx.send(action)?
};
}
}
Ok(())
}
fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> color_eyre::Result<()> {
tui.resize(Rect::new(0, 0, w, h))?;
self.render(tui)?;
Ok(())
}
fn render(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
let active = self.current_screen;
let tx = self.action_tx.clone();
let screens = &mut self.screens;
let command_log = &mut self.command_log;
tui.draw(|frame| {
use ratatui::layout::{Constraint, Layout};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
let chunks = Layout::vertical([
Constraint::Length(1), Constraint::Min(0), Constraint::Length(8), ])
.split(frame.area());
let mut tabs = Vec::with_capacity(SCREEN_NAMES.len() * 2);
for (i, name) in SCREEN_NAMES.iter().enumerate() {
let style = if i == active {
Style::default()
.fg(Color::Black)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::DarkGray)
};
tabs.push(Span::styled(format!(" {name} "), style));
tabs.push(Span::raw(" "));
}
tabs.push(Span::styled(
"Tab to switch",
Style::default().fg(Color::DarkGray),
));
frame.render_widget(Paragraph::new(Line::from(tabs)), chunks[0]);
if let Some(screen) = screens.get_mut(active) {
if let Err(err) = screen.draw(frame, chunks[1]) {
let _ = tx.send(Action::Error(format!("Failed to draw screen: {err:?}")));
}
}
if let Err(err) = command_log.draw(frame, chunks[2]) {
let _ = tx.send(Action::Error(format!("Failed to draw log: {err:?}")));
}
})?;
Ok(())
}
}