1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use std::{io::Write, pin::Pin};
use crossterm::{
cursor::{Hide, Show},
event::{Event as CEvent, EventStream},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, size, Clear, ClearType,
EnterAlternateScreen, LeaveAlternateScreen,
},
ExecutableCommand, QueueableCommand,
};
use futures::{channel::mpsc, prelude::*};
use crate::{
event::{Cause, Event},
Am, Amv, CommandBuf, Gui, Printer, View,
};
/// Event loop control flow
#[derive(PartialEq, Eq)]
pub enum ControlFlow {
/// Pause the event loop until new events are available
Wait,
/// Shut down the event loop
Exit,
}
/// Runtime for a Glyph User Interface application
pub struct Runtime<T, A> {
events_tx: mpsc::Sender<Event<T>>,
events_rx: mpsc::Receiver<Event<T>>,
app: A,
}
impl<T, A> Runtime<T, A>
where
T: Send + 'static,
A: Gui<Event = T>,
{
/// Create a new runtime
///
/// `spawn` must be be able to spawn a future on the async runtime being
/// used. For example, `|task| tokio::spawn(task)` will work.
pub fn new<S, H>(app: A, spawn: S) -> Self
where
S: FnOnce(Pin<Box<dyn Future<Output = ()> + Send>>) -> H,
{
let (events_tx, events_rx) = mpsc::channel(16);
let task = {
let events_tx = events_tx.clone();
async {
EventStream::new()
.map(|e| match e {
Ok(CEvent::Resize(w, h)) => {
Ok(Event::Resize((w, h).into()))
}
Ok(CEvent::Key(key)) => Ok(Event::Key(
crate::event::keyboard_types::from_crossterm(key),
)),
_ => Ok(Event::New(Cause::WaitOver)),
})
.forward(events_tx)
.await
.unwrap();
}
};
spawn(Box::pin(task));
Self {
events_tx,
events_rx,
app,
}
}
/// Run the event loop
///
/// This will also initialize the terminal for rendering views. In
/// particular, raw mode is enabled then an alternate screen is entered and
/// then cleared. Note however that the location of the cursor is undefined.
/// When the event loop is destroyed, the alternate screen is exited and raw
/// mode is disabled.
pub async fn run(mut self) {
enable_raw_mode().unwrap();
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, Clear(ClearType::All), Hide,)
.unwrap();
self.events_tx.send(Event::New(Cause::Init)).await.unwrap();
let cmd_queues: Amv<Am<CommandBuf>> = Default::default();
let new_cmd_queue = || {
let cmd_queue: Am<CommandBuf> = Default::default();
cmd_queues.lock().push(cmd_queue.clone());
cmd_queue
};
let cmd_queue = new_cmd_queue();
let mut printer =
Printer::new(size().unwrap(), cmd_queue, &new_cmd_queue);
while let Some(event) = self.events_rx.next().await {
// The root view always has focus
let messages = self.app.view().event(&event, true);
for message in messages {
self.app.update(message);
}
let control_flow = self.app.control_flow();
if control_flow == ControlFlow::Exit {
break;
}
let _resized = if let Event::Resize(new_size) = event {
printer.set_size(new_size);
true
} else {
false
};
let view = self.app.view();
stdout.execute(Clear(ClearType::All)).unwrap();
// The root view always has focus
view.draw(&printer, true);
let cmd_queues = cmd_queues.lock();
let mut queue_cmds = |changes_cursor: bool| {
for cmd_queue in cmd_queues
.iter()
.filter(|x| x.lock().changes_cursor == changes_cursor)
{
let mut cmd_queue = cmd_queue.lock();
for cmd in cmd_queue.cmds.iter() {
stdout.queue(cmd).unwrap();
}
cmd_queue.cmds.clear();
}
};
queue_cmds(false);
// Make sure that command buffers (I mean, there should only be one)
// that want to show the cursor at a certain location are queued
// last, because otherwise the cursor will probably (it depends on
// the contents of the screen) show in the wrong place.
queue_cmds(true);
stdout.flush().unwrap();
}
execute!(stdout, LeaveAlternateScreen, Show).unwrap();
disable_raw_mode().unwrap();
}
}