1use std::io;
6use std::time::{Duration, Instant};
7
8use katui_present::cells;
9use katui_present::session;
10use katui_proto::kitty;
11
12use crate::RtError;
13
14pub struct Session<W: io::Write> {
17 out: Option<W>,
18}
19
20impl<W: io::Write> Session<W> {
21 pub fn enter(out: W) -> Result<Self, RtError> {
23 let mut session = Self { out: Some(out) };
24 if let Some(out) = session.out.as_mut() {
25 session::enter(out)?;
26 }
27 Ok(session)
28 }
29
30 pub fn leave(mut self) -> io::Result<W> {
32 let out = self.out.take();
33 let Some(mut out) = out else {
34 unreachable!("only leave takes the writer and it runs once");
35 };
36 session::leave(&mut out)?;
37 Ok(out)
38 }
39}
40
41impl<W: io::Write> Drop for Session<W> {
42 fn drop(&mut self) {
43 if let Some(out) = &mut self.out {
45 let _ = session::leave(out);
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, Default)]
53pub struct Teardown {
54 pub graphics: bool,
56 pub home: bool,
58 pub clear: bool,
60}
61
62impl Teardown {
63 pub(crate) fn finish<W: io::Write>(self, out: &mut W) -> io::Result<()> {
66 out.write_all(b"\x1b[?2026l\x1b[0m")?;
67 cells::show_cursor(out)?;
68 if self.graphics {
69 out.write_all(&kitty::Delete::AllVisible.encode())?;
70 }
71 if self.home {
72 out.write_all(b"\x1b[H")?;
73 }
74 if self.clear {
75 out.write_all(b"\x1b[2J")?;
76 }
77 out.flush()
78 }
79}
80
81pub struct Stage<W: io::Write> {
86 out: Option<W>,
87 teardown: Teardown,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ResizeEvent {
93 Resized(katui_scene::CellSize),
95 Settled,
98}
99
100const SETTLE_REPAINTS: [Duration; 2] = [Duration::from_millis(250), Duration::from_millis(1000)];
104
105#[derive(Debug)]
107pub struct ResizeWatch {
108 size: katui_scene::CellSize,
109 changed_at: Option<Instant>,
110 repaints_done: usize,
111}
112
113impl ResizeWatch {
114 #[must_use]
116 pub fn new(size: katui_scene::CellSize) -> Self {
117 Self {
118 size,
119 changed_at: None,
120 repaints_done: 0,
121 }
122 }
123
124 pub fn observe(
126 &mut self,
127 size: Option<katui_scene::CellSize>,
128 at: Instant,
129 ) -> Option<ResizeEvent> {
130 if let Some(new_size) = size
131 && (new_size.columns, new_size.rows) != (self.size.columns, self.size.rows)
132 && new_size.columns > 0
133 && new_size.rows > 0
134 {
135 self.size = new_size;
136 self.changed_at = Some(at);
137 self.repaints_done = 0;
138 return Some(ResizeEvent::Resized(new_size));
139 }
140 let waited = at.duration_since(self.changed_at?);
141 let due = *SETTLE_REPAINTS.get(self.repaints_done)?;
142 if waited < due {
143 return None;
144 }
145 self.repaints_done += 1;
146 Some(ResizeEvent::Settled)
147 }
148}
149
150impl<W: io::Write> Stage<W> {
151 pub fn enter(mut out: W, teardown: Teardown) -> Result<Self, RtError> {
153 cells::hide_cursor(&mut out)?;
154 Ok(Self {
155 out: Some(out),
156 teardown,
157 })
158 }
159
160 pub fn writer(&mut self) -> &mut W {
162 let Some(out) = self.out.as_mut() else {
163 unreachable!("only leave takes the writer and it runs once");
164 };
165 out
166 }
167
168 pub fn leave(mut self) -> io::Result<W> {
170 let teardown = self.teardown;
171 let Some(mut out) = self.out.take() else {
172 unreachable!("only leave takes the writer and it runs once");
173 };
174 teardown.finish(&mut out)?;
175 Ok(out)
176 }
177}
178
179impl<W: io::Write> Drop for Stage<W> {
180 fn drop(&mut self) {
181 if let Some(out) = &mut self.out {
184 let _ = self.teardown.finish(out);
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use std::io;
192
193 use std::time::{Duration, Instant};
194
195 use super::{ResizeEvent, ResizeWatch, Session, Stage, Teardown};
196 #[derive(Clone)]
199 struct Recorder(std::rc::Rc<std::cell::RefCell<Vec<u8>>>);
200
201 impl io::Write for Recorder {
202 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
203 self.0.borrow_mut().extend_from_slice(bytes);
204 Ok(bytes.len())
205 }
206
207 fn flush(&mut self) -> io::Result<()> {
208 Ok(())
209 }
210 }
211
212 #[test]
213 fn a_session_leaves_on_drop() {
214 let log = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
215 let session = Session::enter(Recorder(log.clone())).expect("recorder cannot fail");
216 drop(session);
217 assert_eq!(
218 String::from_utf8_lossy(&log.borrow()),
219 "\x1b[?1049h\x1b[?25l\x1b[?25h\x1b[?1049l",
220 "drop leaves the screen"
221 );
222 }
223
224 #[test]
225 fn leave_after_use_returns_the_writer_with_the_leave_bytes() {
226 let session = Session::enter(Vec::new()).expect("writing to a Vec cannot fail");
227 let bytes = session.leave().expect("writing to a Vec cannot fail");
228 assert_eq!(
229 String::from_utf8_lossy(&bytes),
230 "\x1b[?1049h\x1b[?25l\x1b[?25h\x1b[?1049l"
231 );
232 }
233
234 #[test]
235 fn a_failing_writer_fails_the_enter() {
236 struct Broken;
237 impl io::Write for Broken {
238 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
239 Err(io::Error::other("broken"))
240 }
241 fn flush(&mut self) -> io::Result<()> {
242 Ok(())
243 }
244 }
245 assert!(Session::enter(Broken).is_err());
246 }
247
248 #[test]
249 fn a_full_stage_restores_everything_it_owes() {
250 let stage = Stage::enter(
251 Vec::new(),
252 Teardown {
253 graphics: true,
254 home: true,
255 clear: true,
256 },
257 )
258 .expect("writing to a Vec cannot fail");
259 let bytes = stage.leave().expect("writing to a Vec cannot fail");
260 assert_eq!(
261 String::from_utf8_lossy(&bytes),
262 "\x1b[?25l\x1b[?2026l\x1b[0m\x1b[?25h\x1b_Ga=d,d=a,q=2\x1b\\\x1b[H\x1b[2J",
263 "hide on enter; sync off, styles, cursor, graphics, home, clear on leave"
264 );
265 }
266
267 #[test]
268 fn a_bare_stage_only_resets_the_basics() {
269 let stage =
270 Stage::enter(Vec::new(), Teardown::default()).expect("writing to a Vec cannot fail");
271 let bytes = stage.leave().expect("writing to a Vec cannot fail");
272 assert_eq!(
273 String::from_utf8_lossy(&bytes),
274 "\x1b[?25l\x1b[?2026l\x1b[0m\x1b[?25h",
275 "no graphics, home or clear owed"
276 );
277 }
278
279 #[test]
280 fn a_stage_left_unleft_still_restores_on_drop() {
281 let log = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
282 let stage =
283 Stage::enter(Recorder(log.clone()), Teardown::default()).expect("recorder cannot fail");
284 drop(stage);
285 assert_eq!(
286 String::from_utf8_lossy(&log.borrow()),
287 "\x1b[?25l\x1b[?2026l\x1b[0m\x1b[?25h",
288 "drop restores when leave never ran"
289 );
290 }
291
292 fn size(columns: u16, rows: u16) -> katui_scene::CellSize {
293 katui_scene::CellSize { columns, rows }
294 }
295
296 #[test]
297 fn a_resize_reports_once_then_repaints_as_it_settles() {
298 let start = Instant::now();
299 let mut watch = ResizeWatch::new(size(80, 24));
300 assert_eq!(watch.observe(Some(size(80, 24)), start), None);
301 let later = |millis| start + Duration::from_millis(millis);
302 assert_eq!(
303 watch.observe(Some(size(100, 30)), start),
304 Some(ResizeEvent::Resized(size(100, 30)))
305 );
306 assert_eq!(watch.observe(Some(size(100, 30)), later(100)), None);
307 assert_eq!(
308 watch.observe(Some(size(100, 30)), later(300)),
309 Some(ResizeEvent::Settled)
310 );
311 assert_eq!(watch.observe(Some(size(100, 30)), later(400)), None);
312 assert_eq!(
313 watch.observe(Some(size(100, 30)), later(1100)),
314 Some(ResizeEvent::Settled)
315 );
316 assert_eq!(watch.observe(Some(size(100, 30)), later(5000)), None);
317 }
318
319 #[test]
320 fn a_new_resize_restarts_the_settling() {
321 let start = Instant::now();
322 let mut watch = ResizeWatch::new(size(80, 24));
323 watch.observe(Some(size(100, 30)), start);
324 let dragged = start + Duration::from_millis(200);
325 watch.observe(Some(size(110, 30)), dragged);
326 assert_eq!(
327 watch.observe(Some(size(110, 30)), start + Duration::from_millis(300)),
328 None,
329 "the clock runs from the last change"
330 );
331 assert_eq!(
332 watch.observe(Some(size(110, 30)), start + Duration::from_millis(500)),
333 Some(ResizeEvent::Settled)
334 );
335 }
336
337 #[test]
338 fn a_zero_or_unreadable_size_is_ignored() {
339 let mut watch = ResizeWatch::new(size(80, 24));
340 let now = Instant::now();
341 assert_eq!(watch.observe(None, now), None);
342 assert_eq!(watch.observe(Some(size(0, 0)), now), None);
343 }
344}