Skip to main content

ez_tui/core/
app.rs

1use crate::core::tui::{ExitReason, Tui};
2use crate::inputs::legend::Legend;
3use crate::types::args::EzArgs;
4use crate::types::cpt_ids::{EzCptId, EzCptIds};
5use crate::types::event::EzMsg;
6use crate::types::ship_ids::{EzShipId, EzShipIds};
7use crate::types::state::EzState;
8use crate::utils::logs::LogSwitcher;
9use crate::{
10    AppArguments, EventClause, EzEvent, GlobalHotKeys, IoContext, NoClientState, Port, Result,
11    Sender, State, SubClause, SubId, Subscription, View,
12};
13use clap::Parser;
14use ratatui::layout::Rect;
15use std::collections::VecDeque;
16use tokio::sync::Mutex;
17use tokio::time::Instant;
18use tracing::{debug, error, info, trace, warn};
19
20/// Application is the struct that will hold all the parts of your TUI application.
21///
22/// It will hold
23/// * the [`Args`] given on the command line
24/// * the [`TUI`] - to manage the lifecycle and handle user inputs
25/// * the [`View`] - to draw your [`Component`]s
26/// * the [`Port`] - to handle external messages sent from [`Ship`]s
27/// * the [`Subscriptions`] - to register what [Event](s) your [`Component`]s and [`Ship`]s may accept
28/// * a copy of the [`Sender`] channel so we can send messages
29///
30/// # Example
31/// ```
32#[doc = include_str!("../../examples/basic/main.rs")]
33/// ```
34#[allow(missing_debug_implementations)]
35pub struct Application<CID, SID, CA, CS, CM>
36where
37    CID: EzCptIds,
38    SID: EzShipIds,
39    CA: EzArgs,
40    CS: EzState,
41    CM: EzMsg,
42{
43    args: AppArguments<CA>,
44    view: Option<View<CID, CA, CS, CM>>,
45    hot_keys: GlobalHotKeys,
46    port: Option<Port<CID, SID, CA, CS, CM>>,
47    subs: Vec<Subscription<CID, SID, CM>>,
48    tui: Tui<CID, CM>,
49    tx: Sender<EzEvent<CID, CM>>,
50    state: Mutex<State<CS>>,
51}
52
53/// Implementation for [`Application`]s that explicitly don't use a state (uses [`NoClientState`] instead)
54impl<CID, SID, CA, CM> Application<CID, SID, CA, NoClientState, CM>
55where
56    CID: EzCptIds,
57    SID: EzShipIds,
58    CA: EzArgs,
59    CM: EzMsg,
60{
61    /// Create a new [`Application`] instance by parsing the command line arguments and creating a [`Tui`] instance.
62    /// That application will not support any custom [`ClientState`] and will use the default [`NoClientState`].
63    /// If you need some global state for your business logic, you should use [`with_state`](Self::with_state) instead.
64    pub fn init(args: AppArguments<CA>, logger: LogSwitcher) -> Self {
65        Self::init_with_state(args, logger, NoClientState)
66    }
67}
68
69// public interface
70impl<CID, SID, CA, CS, CM> Application<CID, SID, CA, CS, CM>
71where
72    CID: EzCptIds,
73    SID: EzShipIds,
74    CA: EzArgs,
75    CS: EzState,
76    CM: EzMsg,
77{
78    /// Wrap the [`clap::Parser`] trait to parse the command line arguments nd initialize logging from them
79    #[must_use]
80    pub fn parse() -> (AppArguments<CA>, LogSwitcher) {
81        let args = AppArguments::parse();
82        let logger = LogSwitcher::initialize(args.lib.log_level);
83        (args, logger)
84    }
85    /// Set a [`View`] for this application. The view is the UI part of the application and holds all components rendered to the user.
86    #[must_use]
87    pub fn with_view(mut self, view: View<CID, CA, CS, CM>) -> Self {
88        self.view = Some(view);
89        self
90    }
91
92    /// Set a [`Port`] for this application. The port is the interface to the outside world; any data you expect to receive from an external source should be emited in a [`Ship`]. The port will holds all those ships.
93    #[must_use]
94    pub fn with_port(mut self, port: Port<CID, SID, CA, CS, CM>) -> Self {
95        self.port = Some(port);
96        self
97    }
98    /// Set the global key codes for this application.
99    #[must_use]
100    pub fn with_global_key_codes(mut self, hk: GlobalHotKeys) -> Self {
101        self.hot_keys = hk;
102        self
103    }
104
105    /// Same as `init` but with a custom [`ClientState`]
106    pub fn init_with_state(args: AppArguments<CA>, logger: LogSwitcher, client_state: CS) -> Self {
107        let tui = Tui::new(logger, args.lib.frame_rate, args.lib.tick_rate);
108        Self {
109            args,
110            tx: tui.get_tx(),
111            hot_keys: GlobalHotKeys::default(),
112            port: None,
113            view: None,
114            subs: vec![
115                Subscription::new(
116                    SubId::CPt(EzCptId::TickIndicator),
117                    EventClause::<CID, CM>::Lifecycle,
118                    SubClause::Always,
119                ),
120                Subscription::new(
121                    SubId::CPt(EzCptId::FrameIndicator),
122                    EventClause::<CID, CM>::Lifecycle,
123                    SubClause::Always,
124                ),
125            ],
126            state: Mutex::new(State::with_client(client_state)),
127            tui,
128        }
129    }
130
131    /// Get the original command line arguments
132    pub fn get_args(&self) -> AppArguments<CA> {
133        self.args.clone()
134    }
135
136    /// Get a clone of the [`Sender`] to send [Event](s)
137    pub fn clone_tx(&mut self) -> Sender<EzEvent<CID, CM>> {
138        self.tx.clone()
139    }
140
141    /// Start the application lifecycle
142    ///
143    /// # Panics
144    /// If any of those things fails to initialize:
145    ///  * The TUI cannot be entered
146    ///  * The channel to send events is already closed
147    pub async fn start(&mut self) {
148        self.tui.enter().expect("Failed to initialize TUI");
149        info!("TUI entered");
150
151        self.tx.send(EzEvent::Init).expect("Channel already closed");
152        if let Some(ref mut p) = self.port {
153            p.init(&self.tui.get_tx());
154        }
155
156        info!("Starting the application");
157        match self.run().await {
158            Ok(()) => {}
159            Err(err) => {
160                error!("Error during application lifecycle: {}", err);
161            }
162        }
163
164        self.tui.exit(&ExitReason::LifecycleEnded);
165    }
166
167    /// Subscribe a [`Component`] to a list of [Event](s) via a [`Sub`] struct.
168    /// A Sub describes what kind of event and in under what circumstances the component should be notified.
169    #[must_use]
170    pub fn subscribe_cpt(
171        mut self,
172        id: &EzCptId<CID>,
173        subs: Vec<(EventClause<CID, CM>, SubClause<CID, SID>)>,
174    ) -> Self {
175        for (ev, sub) in subs {
176            let subscription = Subscription::new(SubId::CPt(id.clone()), ev, sub);
177            if !self.cpt_subscribed(id, subscription.event()) {
178                self.subs.push(subscription);
179            }
180        }
181        self
182    }
183    /// Subscribe a [`Ship`] to a list of [Event](s) via a [`Sub`] struct.
184    /// A Sub describes what kind of event and in under what circumstances the ship should be notified.
185    #[must_use]
186    #[allow(clippy::needless_pass_by_value)]
187    pub fn subscribe_ship(
188        mut self,
189        id: EzShipId<SID>,
190        subs: Vec<(EventClause<CID, CM>, SubClause<CID, SID>)>,
191    ) -> Self {
192        for (ev, sub) in subs {
193            let subscription = Subscription::new(SubId::Ship(id.clone()), ev, sub);
194            if !self.port_subscribed(&id, subscription.event()) {
195                self.subs.push(subscription);
196            }
197        }
198        self
199    }
200}
201
202// Internal functions
203impl<CID, SID, CA, CS, CM> Application<CID, SID, CA, CS, CM>
204where
205    CID: EzCptIds,
206    SID: EzShipIds,
207    CA: EzArgs,
208    CS: EzState,
209    CM: EzMsg,
210{
211    async fn run(&mut self) -> Result<()> {
212        debug!("Initial drawing");
213        self.draw_if_needed()?;
214
215        loop {
216            trace!("[ LOOP # {} ]", self.state.get_mut().loop_cnt());
217            let event = self.tui.next().await?;
218
219            let start_time = Instant::now(); // Start timer after we waited to get the actual process time
220
221            let mut event_stack = VecDeque::new();
222            event_stack.push_back(event);
223            event_stack.extend(self.handle_init());
224            while let Some(event) = event_stack.pop_front() {
225                let new = self.handle_event(&event)?;
226                event_stack.extend(new);
227            }
228
229            if self.state.get_mut().should_quit() {
230                self.tui.exit(&ExitReason::UserRequested);
231                break;
232            }
233            if self.state.get_mut().should_redraw() {
234                self.draw_if_needed()?;
235            }
236            self.state.get_mut().log_loop(start_time);
237        }
238        Ok(())
239    }
240
241    fn handle_init(&mut self) -> VecDeque<EzEvent<CID, CM>> {
242        let mut event_stack = VecDeque::new();
243        if let Some(v) = &mut self.view {
244            event_stack.extend(v.init_cpts(self.state.get_mut()));
245        }
246        if let Some(p) = &mut self.port {
247            event_stack.extend(p.init_ships(self.state.get_mut()));
248        }
249        event_stack
250    }
251
252    fn draw_if_needed(&mut self) -> Result<()> {
253        self.state.get_mut().unflag_draw();
254        if let Some(ref mut v) = self.view {
255            let start_time = Instant::now();
256            let mut draw_error = None;
257            let draw_result = self.tui.draw(|f| {
258                v.draw(f).unwrap_or_else(|err| {
259                    draw_error = Some(err.clone());
260                });
261            });
262
263            self.state.get_mut().log_frame(start_time);
264            match (draw_result, draw_error) {
265                (Ok(_), None) => Ok(()),
266                (Ok(_), Some(original_error)) => {
267                    warn!("Frame rendered but error happened: {}", original_error);
268                    Ok(())
269                }
270                (Err(io_error), None) => Err(IoContext::Drawing.wrap_io(&io_error)),
271                (Err(io_error), Some(original_error)) => {
272                    error!("Next error is probably caused by {}", io_error);
273                    Err(original_error)
274                }
275            }
276        } else {
277            Ok(())
278        }
279    }
280
281    fn tick_if_needed(&mut self) -> Vec<EzEvent<CID, CM>> {
282        let mut ticked = false;
283        let mut messages = vec![];
284        let start_time = Instant::now();
285        if let Some(ref mut v) = self.view {
286            ticked = true;
287            messages.extend(v.tick_components(self.state.get_mut()));
288        }
289
290        if let Some(ref mut p) = self.port {
291            ticked = true;
292            messages.extend(p.tick(self.state.get_mut()));
293        }
294
295        if ticked {
296            self.state.get_mut().log_tick(start_time);
297        }
298
299        messages
300    }
301
302    fn handle_event(&mut self, event: &EzEvent<CID, CM>) -> Result<Vec<EzEvent<CID, CM>>> {
303        let mut messages = vec![];
304        if event == &EzEvent::None {
305            return Ok(vec![]);
306        } else if event == &EzEvent::Tick {
307            trace!("[ TICK # {} ]", self.state.get_mut().tick_cnt());
308            messages.extend(self.tick_if_needed());
309        } else if event == &EzEvent::Render {
310            trace!("[ FRAME # {} ]", self.state.get_mut().frame_cnt());
311            self.state.get_mut().initiate_draw();
312        } else if let EzEvent::Error(err) = event {
313            error!("Received error: '{}'", err);
314        } else if let EzEvent::Client(sub) = event {
315            debug!("Client event: {:?}", sub);
316        } else if let EzEvent::Quit = event {
317            warn!("Received quit event");
318            self.state.get_mut().quit();
319        }
320
321        // Actions to do if we have a view
322        if let Some(ref mut v) = self.view {
323            if let EzEvent::UpdateLegend(name, matchers) = event {
324                v.set_legend(Some(Legend::new(name.clone(), matchers.clone())));
325            } else if let EzEvent::ForceFocus(cid) = event {
326                v.focus(cid, true)?;
327            } else if let EzEvent::ForceBlur() = event {
328                v.blur()?;
329            }
330
331            // Forward event to focused component and handle any new event created
332            for msg in v.forward_event_to_focused_component(event.clone(), self.state.get_mut()) {
333                messages.push(msg);
334            }
335        }
336
337        // Forward event to all subscriptions and handle any new event created
338        for msg in self.forward_to_subscriptions(event) {
339            messages.push(msg);
340        }
341
342        // Resize the TUI if needed
343        if let EzEvent::WindowResize(w, h) = event {
344            let rect: Rect = Rect::new(0, 0, *w, *h);
345            self.tui
346                .resize(rect)
347                .map_err(|err| IoContext::TerminalInit.wrap_io(&err))?;
348        }
349
350        if messages.is_empty() {
351            if let EzEvent::Keyboard(key_pressed) = event {
352                if let Some(quit_hk) = &self.hot_keys.quit
353                    && quit_hk.matches(key_pressed)
354                {
355                    messages.push(EzEvent::Quit);
356                }
357                if let Some(ref mut v) = self.view {
358                    if let Some(nfocus_hk) = &self.hot_keys.next_focus
359                        && nfocus_hk.matches(key_pressed)
360                    {
361                        v.cycle_focus()?;
362                    } else if let Some(pfocus_hk) = &self.hot_keys.previous_focus
363                        && pfocus_hk.matches(key_pressed)
364                    {
365                        v.blur()?;
366                    }
367                }
368            }
369        }
370
371        Ok(messages)
372    }
373
374    fn forward_to_subscriptions(&mut self, ev: &EzEvent<CID, CM>) -> Vec<EzEvent<CID, CM>> {
375        let mut messages: Vec<EzEvent<CID, CM>> = Vec::new();
376
377        for sub in &self.subs {
378            // Component subscription
379            if let SubId::CPt(cid) = sub.target()
380                && let Some(ref mut v) = self.view
381                && v.focused() != Some(cid)
382            {
383                if !sub.forward(
384                    ev,
385                    |id, q| {
386                        if let SubId::CPt(cid) = id {
387                            v.query_cpt(cid, q).ok().flatten()
388                        } else {
389                            None
390                        }
391                    },
392                    |id| {
393                        if let SubId::CPt(cid) = id {
394                            v.mounted_internal(cid)
395                        } else {
396                            false
397                        }
398                    },
399                ) {
400                    continue;
401                }
402                messages.extend(v.forward_event_to_component(
403                    cid,
404                    ev.clone(),
405                    self.state.get_mut(),
406                ));
407            }
408
409            // Ship subscription
410            if let SubId::Ship(sid) = sub.target()
411                && let Some(ref mut p) = self.port
412            {
413                if !sub.forward(
414                    ev,
415                    |id, q| {
416                        if let SubId::Ship(sid) = id {
417                            p.query_ship(sid, q).ok().flatten()
418                        } else {
419                            None
420                        }
421                    },
422                    |id| {
423                        if let SubId::Ship(sid) = id {
424                            p.registered_internal(sid)
425                        } else {
426                            false
427                        }
428                    },
429                ) {
430                    continue;
431                }
432                messages.extend(p.forward(sid, ev.clone(), self.state.get_mut()));
433            }
434        }
435        messages
436    }
437}
438
439// Subscription logic
440impl<CID, SID, CA, CS, CM> Application<CID, SID, CA, CS, CM>
441where
442    CID: EzCptIds,
443    SID: EzShipIds,
444    CA: EzArgs,
445    CS: EzState,
446    CM: EzMsg,
447{
448    /// Set the state
449    pub fn set_state(&mut self, state: CS) {
450        self.state.get_mut().set_client(state);
451    }
452
453    /// Remoove all subscriptions for a given [`Component`]
454    pub fn unsubscribe_component(&mut self, id: &EzCptId<CID>) {
455        self.subs
456            .retain(|x| !matches!(x.target(), SubId::CPt(cid) if cid == id));
457    }
458
459    /// Remove all subscriptions for a given [`Ship`]
460    pub fn unsubscribe_ship(&mut self, id: &EzShipId<SID>) {
461        self.subs
462            .retain(|x| !matches!(x.target(), SubId::Ship(sid) if sid == id));
463    }
464
465    /// Check whether a [`Component`] is subscribed to a given [`EventClause`]
466    pub fn cpt_subscribed(&self, id: &EzCptId<CID>, clause: &EventClause<CID, CM>) -> bool {
467        self.subs
468            .iter()
469            .any(|s| matches!(s.target(), SubId::CPt(cid) if cid == id) && s.event() == clause)
470    }
471
472    /// Check whether a [`Ship`] is subscribed to a given [`EventClause`]
473    pub fn port_subscribed(&self, id: &EzShipId<SID>, clause: &EventClause<CID, CM>) -> bool {
474        self.subs
475            .iter()
476            .any(|s| matches!(s.target(), SubId::Ship(sid) if sid == id) && s.event() == clause)
477    }
478}