1pub mod ext;
2pub mod layout;
3pub mod span;
4pub mod theme;
5pub mod utils;
6pub mod widget;
7
8use std::collections::{HashSet, VecDeque};
9use std::hash::Hash;
10use std::rc::Rc;
11use std::time::Duration;
12
13use anyhow::Result;
14
15use ratatui::layout::{Alignment, Constraint, Flex, Position, Rect};
16use ratatui::prelude::*;
17use ratatui::text::{Span, Text};
18use ratatui::widgets::Cell;
19use ratatui::{Frame, Viewport};
20
21use tokio::sync::broadcast;
22use tokio::sync::mpsc::UnboundedReceiver;
23
24use tui_tree_widget::TreeItem;
25
26use crate::event::{Event, Key};
27use crate::store::Update;
28use crate::terminal::Terminal;
29use crate::ui::layout::Spacing;
30use crate::ui::theme::Theme;
31use crate::ui::widget::{AddContentFn, Borders, Column, Widget};
32use crate::{Interrupted, Share};
33
34const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
35
36pub trait Show<M> {
38 fn show(&self, ctx: &Context<M>, frame: &mut Frame) -> Result<()>;
39}
40
41#[derive(Default)]
42pub struct Frontend {}
43
44impl Frontend {
45 pub async fn run<S, M, R>(
46 self,
47 message_tx: broadcast::Sender<M>,
48 mut state_rx: UnboundedReceiver<S>,
49 mut event_rx: UnboundedReceiver<Event>,
50 mut interrupt_rx: broadcast::Receiver<Interrupted<R>>,
51 viewport: Viewport,
52 ) -> anyhow::Result<Interrupted<R>>
53 where
54 S: Update<M, Return = R> + Show<M>,
55 M: Share,
56 R: Share,
57 {
58 let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
59 let mut terminal = Terminal::try_from(viewport)?;
60
61 let mut state = state_rx.recv().await.unwrap();
62 let mut ctx = Context::default().with_sender(message_tx);
63
64 let result: anyhow::Result<Interrupted<R>> = loop {
65 tokio::select! {
66 _ = ticker.tick() => (),
68 Some(event) = event_rx.recv() => {
70 match event {
71 Event::Key(key) => {
72 log::debug!("Received key event: {key:?}");
73 ctx.store_input(event)
74 }
75 Event::Resize(x, y) => {
76 log::debug!("Received resize event: {x},{y}");
77 terminal.clear()?;
78 },
79 Event::Unknown => {
80 log::debug!("Received unknown event")
81 }
82 }
83 },
84 Some(s) = state_rx.recv() => {
86 state = s;
87 },
88 Ok(interrupted) = interrupt_rx.recv() => {
90 break Ok(interrupted);
91 }
92 }
93 terminal.draw(|frame| {
94 let ctx = ctx.clone().with_frame_size(frame.area());
95
96 if let Err(err) = state.show(&ctx, frame) {
97 log::error!("Drawing failed: {err}");
98 }
99 })?;
100
101 ctx.clear_inputs();
102 };
103 terminal.restore()?;
104
105 result
106 }
107}
108
109#[derive(Default, Debug)]
110pub struct Response {
111 pub changed: bool,
112}
113
114#[derive(Debug)]
115pub struct InnerResponse<R> {
116 pub inner: R,
118 pub response: Response,
120}
121
122impl<R> InnerResponse<R> {
123 #[inline]
124 pub fn new(inner: R, response: Response) -> Self {
125 Self { inner, response }
126 }
127}
128
129#[derive(Clone, Debug)]
131pub struct Context<M> {
132 inputs: VecDeque<Event>,
135 pub(crate) frame_size: Rect,
137 pub(crate) sender: Option<broadcast::Sender<M>>,
139}
140
141impl<M> Default for Context<M> {
142 fn default() -> Self {
143 Self {
144 inputs: VecDeque::default(),
145 frame_size: Rect::default(),
146 sender: None,
147 }
148 }
149}
150
151impl<M> Context<M> {
152 pub fn new(frame_size: Rect) -> Self {
153 Self {
154 frame_size,
155 ..Default::default()
156 }
157 }
158
159 pub fn with_inputs(mut self, inputs: VecDeque<Event>) -> Self {
160 self.inputs = inputs;
161 self
162 }
163
164 pub fn with_frame_size(mut self, frame_size: Rect) -> Self {
165 self.frame_size = frame_size;
166 self
167 }
168
169 pub fn with_sender(mut self, sender: broadcast::Sender<M>) -> Self {
170 self.sender = Some(sender);
171 self
172 }
173
174 pub fn frame_size(&self) -> Rect {
175 self.frame_size
176 }
177
178 pub fn store_input(&mut self, event: Event) {
179 self.inputs.push_back(event);
180 }
181
182 pub fn clear_inputs(&mut self) {
183 self.inputs.clear();
184 }
185}
186
187#[derive(Clone, Default, Debug)]
191pub enum Layout {
192 #[default]
193 None,
194 Wrapped {
195 internal: ratatui::layout::Layout,
196 },
197 Expandable3 {
198 left_only: bool,
199 },
200 Popup {
201 percent_x: u16,
202 percent_y: u16,
203 },
204}
205
206impl From<ratatui::layout::Layout> for Layout {
207 fn from(layout: ratatui::layout::Layout) -> Self {
208 Layout::Wrapped { internal: layout }
209 }
210}
211
212impl Layout {
213 pub fn len(&self) -> usize {
214 match self {
215 Layout::None => 0,
216 Layout::Wrapped { internal } => internal.split(Rect::default()).len(),
217 Layout::Expandable3 { left_only } => {
218 if *left_only {
219 1
220 } else {
221 3
222 }
223 }
224 Layout::Popup {
225 percent_x: _,
226 percent_y: _,
227 } => 1,
228 }
229 }
230
231 pub fn is_empty(&self) -> bool {
232 self.len() == 0
233 }
234
235 pub fn split(&self, area: Rect) -> Rc<[Rect]> {
236 match self {
237 Layout::None => Rc::new([]),
238 Layout::Wrapped { internal } => internal.split(area),
239 Layout::Expandable3 { left_only } => {
240 use ratatui::layout::Layout;
241
242 if *left_only {
243 [area].into()
244 } else if area.width <= 140 {
245 let [left, right] = Layout::horizontal([
246 Constraint::Percentage(50),
247 Constraint::Percentage(50),
248 ])
249 .areas(area);
250 let [right_top, right_bottom] =
251 Layout::vertical([Constraint::Percentage(60), Constraint::Percentage(40)])
252 .areas(right);
253
254 [left, right_top, right_bottom].into()
255 } else {
256 Layout::horizontal([
257 Constraint::Percentage(33),
258 Constraint::Percentage(33),
259 Constraint::Percentage(33),
260 ])
261 .split(area)
262 }
263 }
264 Layout::Popup {
265 percent_x,
266 percent_y,
267 } => {
268 use ratatui::layout::Layout;
269
270 let vertical =
271 Layout::vertical([Constraint::Percentage(*percent_y)]).flex(Flex::Center);
272 let horizontal =
273 Layout::horizontal([Constraint::Percentage(*percent_x)]).flex(Flex::Center);
274 let [area] = vertical.areas(area);
275 let [area] = horizontal.areas(area);
276
277 [area].into()
278 }
279 }
280 }
281}
282
283#[derive(Clone, Debug)]
287pub struct Ui<M> {
288 ctx: Context<M>,
290 theme: Theme,
292 area: Rect,
294 layout: Layout,
296 focus_area: Option<usize>,
298 has_focus: bool,
300 count: usize,
303}
304
305impl<M> Ui<M> {
306 pub fn has_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
307 self.has_focus
308 && self.is_area_focused()
309 && self.ctx.inputs.iter().any(|event| {
310 if let Event::Key(key) = event {
311 return f(*key);
312 }
313 false
314 })
315 }
316
317 pub fn has_global_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
318 self.has_focus
319 && self.ctx.inputs.iter().any(|event| {
320 if let Event::Key(key) = event {
321 return f(*key);
322 }
323 false
324 })
325 }
326
327 pub fn get_input(&mut self, f: impl Fn(Key) -> bool) -> Option<Key> {
328 if self.has_focus && self.is_area_focused() {
329 let matches = |&event| {
330 if let Event::Key(key) = event {
331 return f(key);
332 }
333 false
334 };
335
336 if let Some(Event::Key(key)) =
337 self.ctx.inputs.iter().find(|event| matches(event)).copied()
338 {
339 return Some(key);
340 }
341 None
342 } else {
343 None
344 }
345 }
346}
347
348impl<M> Default for Ui<M> {
349 fn default() -> Self {
350 Self {
351 theme: Theme::default(),
352 area: Rect::default(),
353 layout: Layout::default(),
354 focus_area: None,
355 has_focus: true,
356 count: 0,
357 ctx: Context::default(),
358 }
359 }
360}
361
362impl<M> Ui<M> {
363 pub fn new(area: Rect) -> Self {
364 Self {
365 area,
366 ..Default::default()
367 }
368 }
369
370 pub fn with_area(mut self, area: Rect) -> Self {
371 self.area = area;
372 self
373 }
374
375 pub fn with_layout(mut self, layout: Layout) -> Self {
376 self.layout = layout;
377 self
378 }
379
380 pub fn with_area_focus(mut self, focus: Option<usize>) -> Self {
381 self.focus_area = focus;
382 self
383 }
384
385 pub fn with_ctx(mut self, ctx: Context<M>) -> Self {
386 self.ctx = ctx;
387 self
388 }
389
390 pub fn with_focus(mut self) -> Self {
391 self.has_focus = true;
392 self
393 }
394
395 pub fn without_focus(mut self) -> Self {
396 self.has_focus = false;
397 self
398 }
399
400 pub fn with_theme(mut self, theme: Theme) -> Self {
401 self.theme = theme;
402 self
403 }
404
405 pub fn theme(&self) -> &Theme {
406 &self.theme
407 }
408
409 pub fn area(&self) -> Rect {
410 self.area
411 }
412
413 pub fn next_area(&mut self) -> Option<(Rect, bool)> {
414 let area_focus = self
415 .focus_area
416 .map(|focus| self.count == focus)
417 .unwrap_or(false);
418 let rect = self.layout.split(self.area).get(self.count).cloned();
419
420 self.count += 1;
421
422 rect.map(|rect| (rect, area_focus))
423 }
424
425 pub fn current_area(&mut self) -> Option<(Rect, bool)> {
426 let count = self.count.saturating_sub(1);
427
428 let area_focus = self.focus_area.map(|focus| count == focus).unwrap_or(false);
429 let rect = self.layout.split(self.area).get(self.count).cloned();
430
431 rect.map(|rect| (rect, area_focus))
432 }
433
434 pub fn is_area_focused(&self) -> bool {
435 let count = self.count.saturating_sub(1);
436 self.focus_area.map(|focus| count == focus).unwrap_or(false)
437 }
438
439 pub fn has_focus(&self) -> bool {
440 self.has_focus
441 }
442
443 pub fn count(&self) -> usize {
444 self.count
445 }
446
447 pub fn focus_next(&mut self) {
448 if self.focus_area.is_none() {
449 self.focus_area = Some(0);
450 } else {
451 self.focus_area = Some(self.focus_area.unwrap().saturating_add(1));
452 }
453 }
454
455 pub fn send_message(&self, message: M) {
456 if let Some(sender) = &self.ctx.sender {
457 let _ = sender.send(message);
458 }
459 }
460}
461
462impl<M> Ui<M>
463where
464 M: Clone,
465{
466 pub fn add(&mut self, frame: &mut Frame, widget: impl Widget) -> Response {
467 widget.ui(self, frame)
468 }
469
470 pub fn child_ui(&mut self, area: Rect, layout: impl Into<Layout>) -> Self {
471 Ui::default()
472 .with_area(area)
473 .with_layout(layout.into())
474 .with_ctx(self.ctx.clone())
475 .with_theme(self.theme.clone())
476 }
477
478 pub fn layout<R>(
479 &mut self,
480 layout: impl Into<Layout>,
481 focus: Option<usize>,
482 add_contents: impl FnOnce(&mut Self) -> R,
483 ) -> InnerResponse<R> {
484 self.layout_dyn(layout, focus, Box::new(add_contents))
485 }
486
487 pub fn layout_dyn<R>(
488 &mut self,
489 layout: impl Into<Layout>,
490 focus: Option<usize>,
491 add_contents: Box<AddContentFn<M, R>>,
492 ) -> InnerResponse<R> {
493 let (area, area_focus) = self.next_area().unwrap_or_default();
494
495 let mut child_ui = Ui {
496 has_focus: area_focus,
497 focus_area: focus,
498 ..self.child_ui(area, layout)
499 };
500
501 InnerResponse::new(add_contents(&mut child_ui), Response::default())
502 }
503}
504
505impl<M> Ui<M>
506where
507 M: Clone,
508{
509 pub fn container<R>(
510 &mut self,
511 layout: impl Into<Layout>,
512 focus: &mut Option<usize>,
513 add_contents: impl FnOnce(&mut Ui<M>) -> R,
514 ) -> InnerResponse<R> {
515 let (area, area_focus) = self.next_area().unwrap_or_default();
516
517 let layout: Layout = layout.into();
518 let len = layout.len();
519
520 let mut child_ui = Ui {
522 has_focus: area_focus,
523 focus_area: *focus,
524 ..self.child_ui(area, layout)
525 };
526
527 widget::Container::new(len, focus).show(&mut child_ui, add_contents)
528 }
529
530 pub fn popup<R>(
531 &mut self,
532 layout: impl Into<Layout>,
533 add_contents: impl FnOnce(&mut Ui<M>) -> R,
534 ) -> InnerResponse<R> {
535 let layout: Layout = layout.into();
536 let areas = layout.split(self.area());
537 let area = areas.first().cloned().unwrap_or(self.area());
538
539 let mut child_ui = self.child_ui(area, layout::fill());
540 child_ui.has_focus = true;
541
542 widget::Popup::default().show(&mut child_ui, add_contents)
543 }
544
545 pub fn label<'a>(&mut self, frame: &mut Frame, content: impl Into<Text<'a>>) -> Response {
546 widget::Label::new(content).ui(self, frame)
547 }
548
549 pub fn overline(&mut self, frame: &mut Frame) -> Response {
550 let overline = String::from("▔").repeat(256);
551 self.label(frame, Span::raw(overline).cyan())
552 }
553
554 pub fn separator(&mut self, frame: &mut Frame) -> Response {
555 let overline = String::from("─").repeat(256);
556 self.label(
557 frame,
558 Span::raw(overline).fg(self.theme.border_style.fg.unwrap_or_default()),
559 )
560 }
561
562 #[allow(clippy::too_many_arguments)]
563 pub fn table<'a, R, const W: usize>(
564 &mut self,
565 frame: &mut Frame,
566 selected: &mut Option<usize>,
567 items: &'a Vec<R>,
568 columns: Vec<Column<'a>>,
569 empty_message: Option<String>,
570 spacing: Spacing,
571 borders: Option<Borders>,
572 ) -> Response
573 where
574 R: ToRow<W> + Clone,
575 {
576 widget::Table::new(selected, items, columns, empty_message, borders)
577 .spacing(spacing)
578 .ui(self, frame)
579 }
580
581 pub fn tree<R, Id>(
582 &mut self,
583 frame: &mut Frame,
584 items: &'_ Vec<R>,
585 opened: &mut Option<HashSet<Vec<Id>>>,
586 selected: &mut Option<Vec<Id>>,
587 borders: Option<Borders>,
588 ) -> Response
589 where
590 R: ToTree<Id> + Clone,
591 Id: ToString + Clone + Eq + Hash,
592 {
593 widget::Tree::new(items, opened, selected, borders, false).ui(self, frame)
594 }
595
596 pub fn shortcuts(
597 &mut self,
598 frame: &mut Frame,
599 shortcuts: &[(&str, &str)],
600 divider: char,
601 alignment: Alignment,
602 ) -> Response {
603 widget::Shortcuts::new(shortcuts, divider, alignment).ui(self, frame)
604 }
605
606 pub fn column_bar(
607 &mut self,
608 frame: &mut Frame,
609 columns: Vec<Column<'_>>,
610 spacing: Spacing,
611 borders: Option<Borders>,
612 ) -> Response {
613 widget::ColumnBar::new(columns, spacing, borders).ui(self, frame)
614 }
615
616 pub fn text_view<'a>(
617 &mut self,
618 frame: &mut Frame,
619 text: impl Into<Text<'a>>,
620 scroll: &'a mut Position,
621 borders: Option<Borders>,
622 ) -> Response {
623 widget::TextView::new(text, None::<String>, scroll, borders).ui(self, frame)
624 }
625
626 pub fn text_view_with_footer<'a>(
627 &mut self,
628 frame: &mut Frame,
629 text: impl Into<Text<'a>>,
630 footer: impl Into<Text<'a>>,
631 scroll: &'a mut Position,
632 borders: Option<Borders>,
633 ) -> Response {
634 widget::TextView::new(text, Some(footer), scroll, borders).ui(self, frame)
635 }
636
637 pub fn centered_text_view<'a>(
638 &mut self,
639 frame: &mut Frame,
640 text: impl Into<Text<'a>>,
641 borders: Option<Borders>,
642 ) -> Response {
643 widget::CenteredTextView::new(text, borders).ui(self, frame)
644 }
645
646 pub fn text_edit_singleline(
647 &mut self,
648 frame: &mut Frame,
649 text: &mut String,
650 cursor: &mut usize,
651 label: Option<impl ToString>,
652 borders: Option<Borders>,
653 ) -> Response {
654 match label {
655 Some(label) => widget::TextEdit::new(text, cursor, borders)
656 .with_label(label)
657 .ui(self, frame),
658 _ => widget::TextEdit::new(text, cursor, borders).ui(self, frame),
659 }
660 }
661}
662
663pub trait ToRow<const W: usize> {
665 fn to_row(&self) -> [Cell<'_>; W];
666}
667
668pub trait ToTree<Id>
670where
671 Id: ToString,
672{
673 fn rows(&self) -> Vec<TreeItem<'_, Id>>;
674}
675
676#[derive(Clone, Debug)]
682pub struct BufferedValue<T>
683where
684 T: Clone,
685{
686 value: T,
687 buffer: Option<T>,
688}
689
690impl<T> BufferedValue<T>
691where
692 T: Clone,
693{
694 pub fn new(value: T) -> Self {
695 Self {
696 value,
697 buffer: None,
698 }
699 }
700
701 pub fn apply(&mut self) {
702 if let Some(buffer) = self.buffer.clone() {
703 self.value = buffer;
704 }
705 self.buffer = None;
706 }
707
708 pub fn reset(&mut self) {
709 self.buffer = None;
710 }
711
712 pub fn write(&mut self, value: T) {
713 self.buffer = Some(value);
714 }
715
716 pub fn read(&self) -> T {
717 if let Some(buffer) = self.buffer.clone() {
718 buffer
719 } else {
720 self.value.clone()
721 }
722 }
723}
724
725#[cfg(test)]
726mod test {
727 use super::*;
728
729 #[test]
730 fn state_value_read_should_succeed() {
731 let value = BufferedValue::new(0);
732 assert_eq!(value.read(), 0);
733 }
734
735 #[test]
736 fn state_value_read_buffer_should_succeed() {
737 let mut value = BufferedValue::new(0);
738 value.write(1);
739
740 assert_eq!(value.read(), 1);
741 }
742
743 #[test]
744 fn state_value_apply_should_succeed() {
745 let mut value = BufferedValue::new(0);
746
747 value.write(1);
748 assert_eq!(value.read(), 1);
749
750 value.apply();
751 assert_eq!(value.read(), 1);
752 }
753
754 #[test]
755 fn state_value_reset_should_succeed() {
756 let mut value = BufferedValue::new(0);
757
758 value.write(1);
759 assert_eq!(value.read(), 1);
760
761 value.reset();
762 assert_eq!(value.read(), 0);
763 }
764
765 #[test]
766 fn state_value_reset_after_apply_should_succeed() {
767 let mut value = BufferedValue::new(0);
768
769 value.write(1);
770 assert_eq!(value.read(), 1);
771
772 value.apply();
773 value.reset();
774 assert_eq!(value.read(), 1);
775 }
776}