Skip to main content

pito_tui/
screen.rs

1use std::any::Any;
2use std::borrow::Cow;
3use std::process::Command;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::mpsc::Sender;
7use std::time::Instant;
8
9use crossterm::event::{KeyEvent, MouseEvent};
10use pito_footer::{Confirm, Hint, InputBar, Notice, Tone};
11use ratatui::{Frame, layout::Rect, style::Style, text::Line};
12
13use crate::activity::{Activity, Change};
14use crate::command::{Heard, Run, Stream};
15use crate::copy::COPY_MAX;
16use crate::head::Focus;
17use crate::label::Span;
18use crate::layer::{Event as Layered, Modal, Takeover, Toast};
19use crate::layout::Layout;
20use crate::model::Item;
21use crate::palette::Palette;
22use crate::pick::Pick;
23use crate::wake::{Wake, Waker};
24use crate::words::Words;
25
26pub type Job<E> = Box<dyn FnOnce() -> E + Send + 'static>;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum Phase<'a> {
31    Ready,
32    Loading(Cow<'a, str>),
33    Trouble(Cow<'a, [Line<'a>]>),
34}
35
36pub trait Screen<E>: Any {
37    fn draw(&mut self, frame: &mut Frame, area: Rect, palette: &Palette);
38
39    fn moment(&mut self, _now: Instant) {}
40
41    fn phase(&self) -> Phase<'_> {
42        Phase::Ready
43    }
44
45    fn load(&mut self) -> Option<Job<E>> {
46        None
47    }
48
49    fn loaded(&mut self, _event: E, _cx: &mut Cx<'_, E>) {}
50
51    fn sources(&self) -> &[&str] {
52        &[]
53    }
54
55    fn cancel(&mut self) {}
56
57    fn start(&mut self, _cx: &mut Cx<'_, E>) {}
58
59    fn entered(&mut self, _cx: &mut Cx<'_, E>) {}
60
61    fn key(&mut self, _key: KeyEvent, _cx: &mut Cx<'_, E>) {}
62
63    fn paste(&mut self, _text: &str, _cx: &mut Cx<'_, E>) {}
64
65    fn mouse(&mut self, _mouse: MouseEvent, _area: Rect, _cx: &mut Cx<'_, E>) {}
66
67    fn event(&mut self, _event: E, _cx: &mut Cx<'_, E>) {}
68
69    fn answer(&mut self, _yes: bool, _cx: &mut Cx<'_, E>) {}
70
71    fn picked(&mut self, _choice: Option<usize>, _cx: &mut Cx<'_, E>) {}
72
73    fn heard(&mut self, _id: u64, _heard: Heard, _cx: &mut Cx<'_, E>) {}
74
75    fn layer(&mut self, _id: u64, _event: Layered, _cx: &mut Cx<'_, E>) {}
76
77    fn claim(&mut self, _key: KeyEvent, _cx: &mut Cx<'_, E>) -> bool {
78        false
79    }
80
81    fn back(&mut self, _cx: &mut Cx<'_, E>) {}
82
83    fn hints(&self) -> Vec<Hint<'_>> {
84        Vec::new()
85    }
86
87    fn facts(&self) -> Vec<(String, Style)> {
88        Vec::new()
89    }
90
91    fn status(&self) -> Option<(String, Style)> {
92        None
93    }
94
95    fn status_parts(&self, _room: u16) -> Vec<(String, Style)> {
96        self.status().into_iter().collect()
97    }
98
99    fn lead(&self, _room: u16) -> Vec<(String, Style)> {
100        Vec::new()
101    }
102
103    fn left(&self, _room: u16, _help: Option<&str>) -> Option<Vec<(String, Style)>> {
104        None
105    }
106
107    fn after_tabs(&self) -> Vec<(String, Style)> {
108        Vec::new()
109    }
110
111    fn caption(&self) -> Option<Vec<(String, Style)>> {
112        None
113    }
114
115    fn notice(&self) -> Option<Notice<'_>> {
116        None
117    }
118
119    fn legend(&self) -> Option<&str> {
120        None
121    }
122
123    fn typing(&self) -> bool {
124        false
125    }
126
127    fn input(&self) -> Option<InputBar<'_>> {
128        None
129    }
130
131    fn crumb(&self) -> Option<String> {
132        None
133    }
134
135    fn selected(&self) -> usize {
136        0
137    }
138
139    fn busy(&self) -> usize {
140        0
141    }
142
143    fn animating(&self) -> bool {
144        false
145    }
146
147    fn deadline(&self) -> Option<Instant> {
148        None
149    }
150
151    fn tick(&mut self, _cx: &mut Cx<'_, E>) {}
152}
153
154pub trait Band {
155    fn height(&self, room: u16, now: Instant) -> u16;
156
157    fn draw(&mut self, frame: &mut Frame, area: Rect, palette: &Palette, now: Instant);
158
159    fn deadline(&self, _now: Instant) -> Option<Instant> {
160        None
161    }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub(crate) struct Asking {
166    pub(crate) screen: usize,
167    pub(crate) question: Cow<'static, str>,
168    pub(crate) confirm: Confirm,
169}
170
171pub(crate) struct Picking {
172    pub(crate) screen: usize,
173    pub(crate) pick: Pick,
174}
175
176pub(crate) struct Opened {
177    pub(crate) screen: usize,
178    pub(crate) modal: Modal,
179}
180
181pub(crate) type Shown = Box<dyn Fn(Item) -> bool>;
182
183pub(crate) struct Outbox<E> {
184    pub(crate) sender: Sender<Wake<E>>,
185    pub(crate) jobs: Vec<(usize, u64, Job<E>)>,
186    pub(crate) epoch: Arc<AtomicU64>,
187    pub(crate) bump: bool,
188    pub(crate) copies: Vec<String>,
189    pub(crate) said: Option<(Cow<'static, str>, Tone)>,
190    pub(crate) asking: Option<Asking>,
191    pub(crate) picking: Option<Picking>,
192    pub(crate) go: Option<usize>,
193    pub(crate) changes: Vec<Change>,
194    pub(crate) runs: Vec<Run>,
195    pub(crate) stops: Vec<u64>,
196    pub(crate) quit: bool,
197    pub(crate) reload: Vec<usize>,
198    pub(crate) changed: Vec<String>,
199    pub(crate) palette: Option<Palette>,
200    pub(crate) words: Option<Words>,
201    pub(crate) hints: Option<Vec<Hint<'static>>>,
202    pub(crate) title: Option<Vec<Span>>,
203    pub(crate) layout: Option<Layout>,
204    pub(crate) role: Option<Option<Cow<'static, str>>>,
205    pub(crate) show: Option<Shown>,
206    pub(crate) group: Option<usize>,
207    pub(crate) toasts: Vec<Toast>,
208    pub(crate) modals: Vec<Opened>,
209    pub(crate) takeover: Option<(usize, Takeover)>,
210    pub(crate) gate: Option<bool>,
211}
212
213impl<E> Outbox<E> {
214    pub(crate) fn new(sender: Sender<Wake<E>>) -> Self {
215        Outbox {
216            sender,
217            jobs: Vec::new(),
218            epoch: Arc::new(AtomicU64::new(0)),
219            bump: false,
220            copies: Vec::new(),
221            said: None,
222            asking: None,
223            picking: None,
224            go: None,
225            changes: Vec::new(),
226            runs: Vec::new(),
227            stops: Vec::new(),
228            quit: false,
229            reload: Vec::new(),
230            changed: Vec::new(),
231            palette: None,
232            words: None,
233            hints: None,
234            title: None,
235            layout: None,
236            role: None,
237            show: None,
238            group: None,
239            toasts: Vec::new(),
240            modals: Vec::new(),
241            takeover: None,
242            gate: None,
243        }
244    }
245
246    pub(crate) fn stamp(&self) -> u64 {
247        self.epoch.load(Ordering::SeqCst)
248    }
249}
250
251pub struct Cx<'a, E> {
252    pub(crate) screen: usize,
253    pub(crate) now: Instant,
254    pub(crate) key: Option<KeyEvent>,
255    pub(crate) focus: Focus,
256    pub(crate) typing: bool,
257    pub(crate) out: &'a mut Outbox<E>,
258}
259
260impl<E: Send + 'static> Cx<'_, E> {
261    pub fn screen(&self) -> usize {
262        self.screen
263    }
264
265    pub fn now(&self) -> Instant {
266        self.now
267    }
268
269    pub fn key(&self) -> Option<KeyEvent> {
270        self.key
271    }
272
273    pub fn focus(&self) -> Focus {
274        self.focus
275    }
276
277    pub fn typing(&self) -> bool {
278        self.typing
279    }
280
281    pub fn detach(&mut self, work: impl FnOnce() -> E + Send + 'static) {
282        let stamp = self.out.stamp();
283        self.out.jobs.push((self.screen, stamp, Box::new(work)));
284    }
285
286    pub fn waker(&self) -> Waker<E> {
287        Waker::new(self.out.sender.clone(), self.screen).epoch(Arc::clone(&self.out.epoch))
288    }
289
290    pub fn epoch(&mut self) {
291        self.out.bump = true;
292    }
293
294    pub fn say(&mut self, text: impl Into<Cow<'static, str>>, tone: Tone) {
295        self.out.said = Some((text.into(), tone));
296    }
297
298    pub fn hush(&mut self) {
299        self.out.said = None;
300    }
301
302    pub fn confirm(&mut self, question: impl Into<Cow<'static, str>>) {
303        self.confirm_with(question, Confirm::new());
304    }
305
306    pub fn confirm_with(&mut self, question: impl Into<Cow<'static, str>>, confirm: Confirm) {
307        self.out.asking = Some(Asking {
308            screen: self.screen,
309            question: question.into(),
310            confirm,
311        });
312    }
313
314    pub fn pick(&mut self, pick: Pick) {
315        self.out.picking = Some(Picking {
316            screen: self.screen,
317            pick,
318        });
319    }
320
321    pub fn copy(&mut self, text: impl Into<String>) -> bool {
322        let text = text.into();
323        if text.len() > COPY_MAX {
324            return false;
325        }
326        self.out.copies.push(text);
327        true
328    }
329
330    pub fn go(&mut self, screen: usize) {
331        self.out.go = Some(screen);
332    }
333
334    pub fn activity(&mut self, activity: Activity) {
335        self.out.changes.push(Change::Put(activity));
336    }
337
338    pub fn activities(&mut self, activities: impl IntoIterator<Item = Activity>) {
339        let all = activities.into_iter().collect();
340        self.out.changes.push(Change::All(all));
341    }
342
343    pub fn run(&mut self, id: u64, label: impl Into<String>, command: Command) {
344        self.run_from(id, label, command, Stream::Stdout);
345    }
346
347    pub fn run_from(
348        &mut self,
349        id: u64,
350        label: impl Into<String>,
351        command: Command,
352        progress: Stream,
353    ) {
354        let activity = Activity::new(id, label, self.now);
355        self.out.changes.push(Change::Run(activity.clone()));
356        self.out.runs.push(Run {
357            screen: self.screen,
358            activity,
359            command,
360            progress,
361            keep: true,
362        });
363    }
364
365    pub fn stop(&mut self, id: u64) {
366        self.out.stops.push(id);
367    }
368
369    pub fn forget(&mut self, id: u64) {
370        for run in self.out.runs.iter_mut().filter(|run| run.activity.id == id) {
371            run.keep = false;
372        }
373        self.out.changes.push(Change::Forget(id));
374    }
375
376    pub fn band(&mut self, shown: bool) {
377        self.out.changes.push(Change::Band(shown));
378    }
379
380    pub fn quit(&mut self) {
381        self.out.quit = true;
382    }
383
384    pub fn reload(&mut self) {
385        self.out.reload.push(self.screen);
386    }
387
388    pub fn changed(&mut self, source: impl Into<String>) {
389        self.out.changed.push(source.into());
390    }
391
392    pub fn palette(&mut self, palette: Palette) {
393        self.out.palette = Some(palette);
394    }
395
396    pub fn words(&mut self, words: Words) {
397        self.out.words = Some(words);
398    }
399
400    pub fn hints(&mut self, hints: Vec<Hint<'static>>) {
401        self.out.hints = Some(hints);
402    }
403
404    pub fn title(&mut self, text: impl Into<Cow<'static, str>>) {
405        self.out.title = Some(vec![(text.into(), Style::new())]);
406    }
407
408    pub fn title_spans<T: Into<Cow<'static, str>>>(
409        &mut self,
410        spans: impl IntoIterator<Item = (T, Style)>,
411    ) {
412        let spans = spans
413            .into_iter()
414            .map(|(text, style)| (text.into(), style))
415            .collect();
416        self.out.title = Some(spans);
417    }
418
419    pub fn layout(&mut self, layout: Layout) {
420        self.out.layout = Some(layout);
421    }
422
423    pub fn role(&mut self, role: Option<Cow<'static, str>>) {
424        self.out.role = Some(role);
425    }
426
427    pub fn show(&mut self, shown: impl Fn(Item) -> bool + 'static) {
428        self.out.show = Some(Box::new(shown));
429    }
430
431    pub fn go_group(&mut self, group: usize) {
432        self.out.group = Some(group);
433    }
434
435    pub fn toast(&mut self, toast: Toast) {
436        self.out.toasts.push(toast);
437    }
438
439    pub fn open(&mut self, modal: Modal) {
440        self.out.modals.retain(|opened| opened.modal.id != modal.id);
441        self.out.modals.push(Opened {
442            screen: self.screen,
443            modal,
444        });
445    }
446
447    pub fn alert(
448        &mut self,
449        id: u64,
450        title: impl Into<Cow<'static, str>>,
451        lines: impl IntoIterator<Item = Line<'static>>,
452    ) {
453        self.open(Modal::alert(id, title, lines));
454    }
455
456    pub fn modal(&mut self, id: u64) -> Option<&mut Modal> {
457        self.out
458            .modals
459            .iter_mut()
460            .find(|opened| opened.modal.id == id)
461            .map(|opened| &mut opened.modal)
462    }
463
464    pub fn close(&mut self, id: u64) {
465        self.out.modals.retain(|opened| opened.modal.id != id);
466    }
467
468    pub fn takeover(&mut self, takeover: Takeover) {
469        self.out.takeover = Some((self.screen, takeover));
470    }
471
472    pub fn end_takeover(&mut self) {
473        self.out.takeover = None;
474    }
475
476    pub fn gate(&mut self, shut: bool) {
477        self.out.gate = Some(shut);
478    }
479}