1use std::io;
6use std::sync::mpsc::{self, Receiver};
7
8use katui_proto::input::{InputEvent, InputParser};
9use katui_scene::CellSize;
10
11use crate::app::{App, AppEvent, Control};
12use crate::events::{Signal, Waker};
13
14#[derive(Debug)]
16pub struct Headless<A: App> {
17 app: A,
18 size: CellSize,
19 parser: InputParser,
20 messages: Receiver<Signal<A::Message>>,
21 pending: Control,
22}
23
24impl<A: App> Headless<A>
25where
26 A::Message: Send + 'static,
27{
28 pub fn start(mut app: A, size: CellSize) -> Self {
30 let (sender, messages) = mpsc::channel();
31 app.start(Waker(sender));
32 Self {
33 app,
34 size,
35 parser: InputParser::default(),
36 messages,
37 pending: Control::Redraw,
38 }
39 }
40
41 pub fn type_bytes(&mut self, bytes: &[u8]) {
43 let mut events = self.parser.feed(bytes);
44 events.extend(self.parser.idle());
45 for event in events {
46 self.input(event);
47 }
48 }
49
50 pub fn input(&mut self, event: InputEvent) {
53 if !crate::app::is_terminal_answer(&event) {
54 self.update(AppEvent::Input(event));
55 }
56 }
57
58 pub fn deliver_messages(&mut self) {
60 while let Ok(signal) = self.messages.try_recv() {
61 if let Signal::Message(message) = signal {
62 self.update(AppEvent::Message(message));
63 }
64 }
65 }
66
67 pub fn resize(&mut self, size: CellSize) {
69 self.size = size;
70 self.update(AppEvent::Resized(size));
71 self.update(AppEvent::Repaint);
72 self.pending = crate::app::merge(self.pending, Control::Redraw);
73 }
74
75 pub fn frame(&mut self) -> io::Result<Option<Vec<u8>>> {
78 if !matches!(self.pending, Control::Redraw | Control::Animate) {
79 return Ok(None);
80 }
81 let mut bytes = Vec::new();
82 self.pending = self.app.draw(&mut bytes, self.size)?;
83 Ok(Some(bytes))
84 }
85
86 pub fn quit(&self) -> bool {
88 self.pending == Control::Quit
89 }
90
91 pub fn app(&self) -> &A {
93 &self.app
94 }
95
96 fn update(&mut self, event: AppEvent<A::Message>) {
97 let control = self.app.update(event);
98 self.pending = crate::app::merge(self.pending, control);
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use std::io;
105
106 use katui_proto::input::{InputEvent, Key};
107 use katui_scene::CellSize;
108
109 use super::Headless;
110 use crate::app::{App, AppEvent, Control};
111 use crate::events::Waker;
112
113 #[derive(Default)]
114 struct Counter {
115 typed: String,
116 waker: Option<Waker<u32>>,
117 total: u32,
118 }
119
120 impl App for Counter {
121 type Message = u32;
122
123 fn start(&mut self, waker: Waker<u32>) {
124 waker.send(5);
125 self.waker = Some(waker);
126 }
127
128 fn update(&mut self, event: AppEvent<u32>) -> Control {
129 match event {
130 AppEvent::Input(event) if event.is_quit_key() => Control::Quit,
131 AppEvent::Input(InputEvent::Key(Key::Char(letter), _)) => {
132 self.typed.push(letter);
133 Control::Redraw
134 }
135 AppEvent::Message(amount) => {
136 self.total += amount;
137 Control::Redraw
138 }
139 _ => Control::Wait,
140 }
141 }
142
143 fn draw(&mut self, out: &mut Vec<u8>, size: CellSize) -> io::Result<Control> {
144 out.extend(
145 format!(
146 "{}x{} {} {}",
147 size.columns, size.rows, self.typed, self.total
148 )
149 .bytes(),
150 );
151 Ok(Control::Wait)
152 }
153 }
154
155 fn size(columns: u16, rows: u16) -> CellSize {
156 CellSize { columns, rows }
157 }
158
159 #[test]
160 fn a_headless_app_types_receives_resizes_and_quits() {
161 let mut driver = Headless::start(Counter::default(), size(80, 24));
162 assert_eq!(
163 driver.frame().expect("drawing works"),
164 Some(b"80x24 0".to_vec())
165 );
166 assert_eq!(
167 driver.frame().expect("drawing works"),
168 None,
169 "a waiting app draws nothing"
170 );
171 driver.type_bytes(b"hi");
172 driver.deliver_messages();
173 assert_eq!(
174 driver.frame().expect("drawing works"),
175 Some(b"80x24 hi 5".to_vec())
176 );
177 driver.type_bytes(b"\x1b[?62;4c");
178 assert_eq!(
179 driver.frame().expect("drawing works"),
180 None,
181 "an answer is no key and draws nothing"
182 );
183 driver.resize(size(40, 10));
184 assert_eq!(
185 driver.frame().expect("drawing works"),
186 Some(b"40x10 hi 5".to_vec()),
187 "a resize redraws"
188 );
189 driver.type_bytes(b"!");
190 assert_eq!(
191 driver.frame().expect("drawing works"),
192 Some(b"40x10 hi! 5".to_vec())
193 );
194 driver.type_bytes(b"\x03");
195 assert!(driver.quit());
196 assert_eq!(driver.app().total, 5);
197 }
198}