1use std::{error::Error, fmt, io};
4
5use crate::{Command, CommandWriter, KeyboardEnhancementFlags, KeyboardEnhancementQuery};
6
7pub trait RawModeControl {
13 fn is_interactive(&self) -> bool;
14
15 fn enable_raw_mode(&mut self) -> io::Result<()>;
16 fn disable_raw_mode(&mut self) -> io::Result<()>;
17}
18
19impl<T: RawModeControl + ?Sized> RawModeControl for &mut T {
20 fn is_interactive(&self) -> bool {
21 T::is_interactive(self)
22 }
23
24 fn enable_raw_mode(&mut self) -> io::Result<()> {
25 T::enable_raw_mode(self)
26 }
27
28 fn disable_raw_mode(&mut self) -> io::Result<()> {
29 T::disable_raw_mode(self)
30 }
31}
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct SessionOptions {
39 pub raw_mode: bool,
41 pub alternate_screen: bool,
43 pub bracketed_paste: bool,
45 pub focus_change: bool,
47 pub keyboard_enhancement: Option<KeyboardEnhancementFlags>,
49 pub mouse_capture: bool,
51 pub hide_cursor: bool,
53}
54
55#[derive(Debug)]
57pub struct SessionError {
58 source: io::Error,
59 cleanup: Option<io::Error>,
60}
61
62impl SessionError {
63 pub fn source_error(&self) -> &io::Error {
65 &self.source
66 }
67
68 pub fn cleanup_error(&self) -> Option<&io::Error> {
70 self.cleanup.as_ref()
71 }
72
73 pub fn into_parts(self) -> (io::Error, Option<io::Error>) {
75 (self.source, self.cleanup)
76 }
77}
78
79impl fmt::Display for SessionError {
80 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(
82 formatter,
83 "failed to enter terminal session: {}",
84 self.source
85 )?;
86 if let Some(cleanup) = &self.cleanup {
87 write!(formatter, "; terminal restoration also failed: {cleanup}")?;
88 }
89 Ok(())
90 }
91}
92
93impl Error for SessionError {
94 fn source(&self) -> Option<&(dyn Error + 'static)> {
95 Some(&self.source)
96 }
97}
98
99pub struct TerminalSession<
107 'a,
108 C: RawModeControl + CommandWriter + KeyboardEnhancementQuery + ?Sized,
109> {
110 control: &'a mut C,
111 entered: Entered,
112 restored: bool,
113}
114
115#[derive(Default)]
116struct Entered {
117 raw_mode: bool,
118 alternate_screen: bool,
119 bracketed_paste: bool,
120 focus_change: bool,
121 keyboard_enhancement: bool,
122 mouse_capture: bool,
123 cursor_hidden: bool,
124}
125
126impl<'a, C> TerminalSession<'a, C>
127where
128 C: RawModeControl + CommandWriter + KeyboardEnhancementQuery + ?Sized,
129{
130 pub fn enter(control: &'a mut C, options: SessionOptions) -> Result<Self, SessionError> {
132 let mut session = Self {
133 control,
134 entered: Entered::default(),
135 restored: false,
136 };
137 if let Err(source) = session.acquire(options) {
138 let cleanup = session.restore().err();
139 return Err(SessionError { source, cleanup });
140 }
141 Ok(session)
142 }
143
144 pub fn control_mut(&mut self) -> &mut C {
146 self.control
147 }
148
149 pub fn restore(&mut self) -> io::Result<()> {
154 if self.restored {
155 return Ok(());
156 }
157 self.restored = true;
158 let mut first = None;
159 macro_rules! restore_command {
160 ($entered:ident, $command:expr) => {
161 if self.entered.$entered {
162 self.entered.$entered = false;
163 if let Err(error) = self.control.write_command($command) {
164 record_first(&mut first, error);
165 }
166 }
167 };
168 }
169 restore_command!(cursor_hidden, Command::SetCursorVisible(true));
170 restore_command!(mouse_capture, Command::SetMouseCapture(false));
171 restore_command!(keyboard_enhancement, Command::PopKeyboardEnhancement);
172 restore_command!(focus_change, Command::SetFocusReporting(false));
173 restore_command!(bracketed_paste, Command::SetBracketedPaste(false));
174 restore_command!(alternate_screen, Command::SetAlternateScreen(false));
175 if let Err(error) = self.control.flush() {
176 record_first(&mut first, error);
177 }
178 if self.entered.raw_mode {
179 self.entered.raw_mode = false;
180 if let Err(error) = self.control.disable_raw_mode() {
181 record_first(&mut first, error);
182 }
183 }
184 first.map_or(Ok(()), Err)
185 }
186
187 fn acquire(&mut self, options: SessionOptions) -> io::Result<()> {
188 if options.raw_mode {
189 self.entered.raw_mode = true;
190 self.control.enable_raw_mode()?;
191 }
192 if options.alternate_screen {
193 self.entered.alternate_screen = true;
194 self.control
195 .write_command(Command::SetAlternateScreen(true))?;
196 }
197 if options.bracketed_paste {
198 self.entered.bracketed_paste = true;
199 self.control
200 .write_command(Command::SetBracketedPaste(true))?;
201 }
202 if options.focus_change {
203 self.entered.focus_change = true;
204 self.control
205 .write_command(Command::SetFocusReporting(true))?;
206 }
207 if let Some(flags) = options.keyboard_enhancement
208 && self.control.supports_keyboard_enhancement()?
209 {
210 self.entered.keyboard_enhancement = true;
211 self.control
212 .write_command(Command::PushKeyboardEnhancement(flags))?;
213 }
214 if options.mouse_capture {
215 self.entered.mouse_capture = true;
216 self.control.write_command(Command::SetMouseCapture(true))?;
217 }
218 if options.hide_cursor {
219 self.entered.cursor_hidden = true;
220 self.control
221 .write_command(Command::SetCursorVisible(false))?;
222 }
223 self.control.flush()
224 }
225}
226
227impl<C> Drop for TerminalSession<'_, C>
228where
229 C: RawModeControl + CommandWriter + KeyboardEnhancementQuery + ?Sized,
230{
231 fn drop(&mut self) {
232 if !self.restored {
233 let _cleanup_error = self.restore();
234 }
235 }
236}
237
238fn record_first(first: &mut Option<io::Error>, error: io::Error) {
239 if first.is_none() {
240 *first = Some(error);
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::{Position, TerminalOutput, TerminalQuery, TerminalSize, WindowSize};
248 use std::panic::{AssertUnwindSafe, catch_unwind};
249
250 const ACQUIRE: [&str; 8] = [
251 "enable_raw",
252 "enter_alternate",
253 "enable_paste",
254 "enable_focus",
255 "enable_keyboard",
256 "enable_mouse",
257 "hide_cursor",
258 "flush",
259 ];
260 const RESTORE: [&str; 8] = [
261 "show_cursor",
262 "disable_mouse",
263 "disable_keyboard",
264 "disable_focus",
265 "disable_paste",
266 "leave_alternate",
267 "flush",
268 "disable_raw",
269 ];
270
271 #[derive(Default)]
272 struct RecordingControl {
273 calls: Vec<&'static str>,
274 fail_acquire: Option<&'static str>,
275 fail_restore: Vec<&'static str>,
276 }
277
278 impl RecordingControl {
279 fn call(&mut self, name: &'static str) -> io::Result<()> {
280 self.calls.push(name);
281 if self.fail_acquire == Some(name) || self.fail_restore.contains(&name) {
282 Err(io::Error::other(name))
283 } else {
284 Ok(())
285 }
286 }
287 }
288
289 impl RawModeControl for RecordingControl {
290 fn is_interactive(&self) -> bool {
291 true
292 }
293 fn enable_raw_mode(&mut self) -> io::Result<()> {
294 self.call("enable_raw")
295 }
296 fn disable_raw_mode(&mut self) -> io::Result<()> {
297 self.call("disable_raw")
298 }
299 }
300
301 impl CommandWriter for RecordingControl {
302 fn write_command(&mut self, command: Command<'_>) -> io::Result<()> {
303 let name = match command {
304 Command::SetAlternateScreen(true) => "enter_alternate",
305 Command::SetAlternateScreen(false) => "leave_alternate",
306 Command::SetBracketedPaste(true) => "enable_paste",
307 Command::SetBracketedPaste(false) => "disable_paste",
308 Command::SetFocusReporting(true) => "enable_focus",
309 Command::SetFocusReporting(false) => "disable_focus",
310 Command::PushKeyboardEnhancement(_) => "enable_keyboard",
311 Command::PopKeyboardEnhancement => "disable_keyboard",
312 Command::SetMouseCapture(true) => "enable_mouse",
313 Command::SetMouseCapture(false) => "disable_mouse",
314 Command::SetCursorVisible(false) => "hide_cursor",
315 Command::SetCursorVisible(true) => "show_cursor",
316 _ => panic!("unexpected session command: {command:?}"),
317 };
318 self.call(name)
319 }
320 }
321
322 impl TerminalQuery for RecordingControl {
323 fn terminal_size(&mut self) -> io::Result<TerminalSize> {
324 Ok(TerminalSize::new(80, 24))
325 }
326
327 fn cursor_position(&mut self) -> io::Result<Position> {
328 Ok(Position::new(0, 0))
329 }
330
331 fn window_size(&mut self) -> io::Result<WindowSize> {
332 Ok(WindowSize::new(TerminalSize::new(80, 24), None))
333 }
334
335 fn raw_mode_enabled(&mut self) -> io::Result<bool> {
336 Ok(false)
337 }
338 }
339
340 impl KeyboardEnhancementQuery for RecordingControl {
341 fn supports_keyboard_enhancement(&mut self) -> io::Result<bool> {
342 Ok(true)
343 }
344 }
345
346 impl TerminalOutput for RecordingControl {
347 fn flush(&mut self) -> io::Result<()> {
348 self.call("flush")
349 }
350 }
351
352 fn all_modes() -> SessionOptions {
353 SessionOptions {
354 raw_mode: true,
355 alternate_screen: true,
356 bracketed_paste: true,
357 focus_change: true,
358 keyboard_enhancement: Some(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES),
359 mouse_capture: true,
360 hide_cursor: true,
361 }
362 }
363
364 #[test]
365 fn explicit_restore_reverses_every_acquired_mode() {
366 let mut control = RecordingControl::default();
367 let mut session =
368 TerminalSession::enter(&mut control, all_modes()).expect("entry succeeds");
369 session.restore().expect("restoration succeeds");
370 drop(session);
371
372 assert_eq!(
373 control.calls,
374 [ACQUIRE.as_slice(), RESTORE.as_slice()].concat()
375 );
376 }
377
378 #[test]
379 fn every_partial_entry_failure_restores_the_mode_that_may_have_been_written() {
380 for (failed_index, failed) in ACQUIRE.into_iter().enumerate() {
381 let mut control = RecordingControl {
382 fail_acquire: Some(failed),
383 ..RecordingControl::default()
384 };
385 let error = match TerminalSession::enter(&mut control, all_modes()) {
386 Ok(_) => panic!("planned acquisition should fail"),
387 Err(error) => error,
388 };
389 assert_eq!(error.source_error().to_string(), failed);
390
391 let mut expected = ACQUIRE[..=failed_index].to_vec();
392 if failed == "flush" {
393 expected.extend_from_slice(&RESTORE);
394 } else {
395 let inverse_start = RESTORE.len() - failed_index - 2;
396 expected.extend_from_slice(&RESTORE[inverse_start..]);
397 }
398 assert_eq!(control.calls, expected, "failed at {failed}");
399 }
400 }
401
402 #[test]
403 fn restoration_reports_the_first_error_and_attempts_every_step() {
404 let mut control = RecordingControl::default();
405 let mut session =
406 TerminalSession::enter(&mut control, all_modes()).expect("entry succeeds");
407 session.control_mut().fail_restore = vec!["show_cursor", "disable_focus", "flush"];
408 let error = session
409 .restore()
410 .expect_err("restoration failure is reported");
411 assert_eq!(error.to_string(), "show_cursor");
412 drop(session);
413
414 assert_eq!(
415 control.calls,
416 [ACQUIRE.as_slice(), RESTORE.as_slice()].concat()
417 );
418 }
419
420 #[test]
421 fn panic_unwinding_runs_the_same_reverse_restoration() {
422 let mut control = RecordingControl::default();
423 let panic = catch_unwind(AssertUnwindSafe(|| {
424 let _session =
425 TerminalSession::enter(&mut control, all_modes()).expect("entry succeeds");
426 panic!("planned panic");
427 }));
428
429 assert!(panic.is_err());
430 assert_eq!(
431 control.calls,
432 [ACQUIRE.as_slice(), RESTORE.as_slice()].concat()
433 );
434 }
435}