1use std::sync::Arc;
2
3use color_eyre::eyre::eyre;
4use crossterm::event::KeyEvent;
5use ratatui::prelude::Rect;
6use serde::{Deserialize, Serialize};
7use tokio::sync::mpsc;
8use tokio_util::sync::CancellationToken;
9use tracing::{debug, info};
10
11use crate::{
12 action::Action,
13 api::ApiClient,
14 components::{Component, command_log::CommandLog, health::Health, stamps::Stamps},
15 config::Config,
16 log_capture,
17 tui::{Event, Tui},
18 watch::BeeWatch,
19};
20
21pub struct App {
22 config: Config,
23 tick_rate: f64,
24 frame_rate: f64,
25 screens: Vec<Box<dyn Component>>,
29 current_screen: usize,
31 command_log: Box<dyn Component>,
34 should_quit: bool,
35 should_suspend: bool,
36 mode: Mode,
37 last_tick_key_events: Vec<KeyEvent>,
38 action_tx: mpsc::UnboundedSender<Action>,
39 action_rx: mpsc::UnboundedReceiver<Action>,
40 root_cancel: CancellationToken,
43 #[allow(dead_code)]
46 api: Arc<ApiClient>,
47 watch: BeeWatch,
49}
50
51const SCREEN_NAMES: &[&str] = &["Health", "Stamps"];
54
55#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub enum Mode {
57 #[default]
58 Home,
59}
60
61impl App {
62 pub fn new(tick_rate: f64, frame_rate: f64) -> color_eyre::Result<Self> {
63 let (action_tx, action_rx) = mpsc::unbounded_channel();
64 let config = Config::new()?;
65
66 let node = config
68 .active_node()
69 .ok_or_else(|| eyre!("no Bee node configured (config.nodes is empty)"))?;
70 let api = Arc::new(ApiClient::from_node(node)?);
71
72 let root_cancel = CancellationToken::new();
75 let watch = BeeWatch::start(api.clone(), &root_cancel);
76
77 let health = Health::new(api.clone(), watch.health());
79 let stamps = Stamps::new(watch.stamps());
81 let command_log: Box<dyn Component> = Box::new(CommandLog::new(log_capture::handle()));
86
87 Ok(Self {
88 tick_rate,
89 frame_rate,
90 screens: vec![Box::new(health), Box::new(stamps)],
93 current_screen: 0,
94 command_log,
95 should_quit: false,
96 should_suspend: false,
97 config,
98 mode: Mode::Home,
99 last_tick_key_events: Vec::new(),
100 action_tx,
101 action_rx,
102 root_cancel,
103 api,
104 watch,
105 })
106 }
107
108 pub async fn run(&mut self) -> color_eyre::Result<()> {
109 let mut tui = Tui::new()?
110 .tick_rate(self.tick_rate)
112 .frame_rate(self.frame_rate);
113 tui.enter()?;
114
115 let tx = self.action_tx.clone();
116 let cfg = self.config.clone();
117 let size = tui.size()?;
118 for component in self.iter_components_mut() {
119 component.register_action_handler(tx.clone())?;
120 component.register_config_handler(cfg.clone())?;
121 component.init(size)?;
122 }
123
124 let action_tx = self.action_tx.clone();
125 loop {
126 self.handle_events(&mut tui).await?;
127 self.handle_actions(&mut tui)?;
128 if self.should_suspend {
129 tui.suspend()?;
130 action_tx.send(Action::Resume)?;
131 action_tx.send(Action::ClearScreen)?;
132 tui.enter()?;
134 } else if self.should_quit {
135 tui.stop()?;
136 break;
137 }
138 }
139 self.watch.shutdown();
141 self.root_cancel.cancel();
142 tui.exit()?;
143 Ok(())
144 }
145
146 async fn handle_events(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
147 let Some(event) = tui.next_event().await else {
148 return Ok(());
149 };
150 let action_tx = self.action_tx.clone();
151 match event {
152 Event::Quit => action_tx.send(Action::Quit)?,
153 Event::Tick => action_tx.send(Action::Tick)?,
154 Event::Render => action_tx.send(Action::Render)?,
155 Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
156 Event::Key(key) => self.handle_key_event(key)?,
157 _ => {}
158 }
159 for component in self.iter_components_mut() {
160 if let Some(action) = component.handle_events(Some(event.clone()))? {
161 action_tx.send(action)?;
162 }
163 }
164 Ok(())
165 }
166
167 fn iter_components_mut(&mut self) -> impl Iterator<Item = &mut Box<dyn Component>> {
171 self.screens
172 .iter_mut()
173 .chain(std::iter::once(&mut self.command_log))
174 }
175
176 fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<()> {
177 let action_tx = self.action_tx.clone();
178 if matches!(key.code, crossterm::event::KeyCode::Tab) {
182 if !self.screens.is_empty() {
183 self.current_screen = (self.current_screen + 1) % self.screens.len();
184 debug!(
185 "switched to screen {}",
186 SCREEN_NAMES.get(self.current_screen).unwrap_or(&"?")
187 );
188 }
189 return Ok(());
190 }
191 let Some(keymap) = self.config.keybindings.0.get(&self.mode) else {
192 return Ok(());
193 };
194 match keymap.get(&vec![key]) {
195 Some(action) => {
196 info!("Got action: {action:?}");
197 action_tx.send(action.clone())?;
198 }
199 _ => {
200 self.last_tick_key_events.push(key);
203
204 if let Some(action) = keymap.get(&self.last_tick_key_events) {
206 info!("Got action: {action:?}");
207 action_tx.send(action.clone())?;
208 }
209 }
210 }
211 Ok(())
212 }
213
214 fn handle_actions(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
215 while let Ok(action) = self.action_rx.try_recv() {
216 if action != Action::Tick && action != Action::Render {
217 debug!("{action:?}");
218 }
219 match action {
220 Action::Tick => {
221 self.last_tick_key_events.drain(..);
222 }
223 Action::Quit => self.should_quit = true,
224 Action::Suspend => self.should_suspend = true,
225 Action::Resume => self.should_suspend = false,
226 Action::ClearScreen => tui.terminal.clear()?,
227 Action::Resize(w, h) => self.handle_resize(tui, w, h)?,
228 Action::Render => self.render(tui)?,
229 _ => {}
230 }
231 let tx = self.action_tx.clone();
232 for component in self.iter_components_mut() {
233 if let Some(action) = component.update(action.clone())? {
234 tx.send(action)?
235 };
236 }
237 }
238 Ok(())
239 }
240
241 fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> color_eyre::Result<()> {
242 tui.resize(Rect::new(0, 0, w, h))?;
243 self.render(tui)?;
244 Ok(())
245 }
246
247 fn render(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
248 let active = self.current_screen;
249 let tx = self.action_tx.clone();
250 let screens = &mut self.screens;
251 let command_log = &mut self.command_log;
252 tui.draw(|frame| {
253 use ratatui::layout::{Constraint, Layout};
254 use ratatui::style::{Color, Modifier, Style};
255 use ratatui::text::{Line, Span};
256 use ratatui::widgets::Paragraph;
257
258 let chunks = Layout::vertical([
259 Constraint::Length(1), Constraint::Min(0), Constraint::Length(8), ])
263 .split(frame.area());
264
265 let mut tabs = Vec::with_capacity(SCREEN_NAMES.len() * 2);
267 for (i, name) in SCREEN_NAMES.iter().enumerate() {
268 let style = if i == active {
269 Style::default()
270 .fg(Color::Black)
271 .bg(Color::Yellow)
272 .add_modifier(Modifier::BOLD)
273 } else {
274 Style::default().fg(Color::DarkGray)
275 };
276 tabs.push(Span::styled(format!(" {name} "), style));
277 tabs.push(Span::raw(" "));
278 }
279 tabs.push(Span::styled(
280 "Tab to switch",
281 Style::default().fg(Color::DarkGray),
282 ));
283 frame.render_widget(Paragraph::new(Line::from(tabs)), chunks[0]);
284
285 if let Some(screen) = screens.get_mut(active) {
287 if let Err(err) = screen.draw(frame, chunks[1]) {
288 let _ = tx.send(Action::Error(format!("Failed to draw screen: {err:?}")));
289 }
290 }
291 if let Err(err) = command_log.draw(frame, chunks[2]) {
293 let _ = tx.send(Action::Error(format!("Failed to draw log: {err:?}")));
294 }
295 })?;
296 Ok(())
297 }
298}