1use std::fmt;
4use std::io;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7#[cfg(feature = "graphics")]
8use urushi_graphics::{GraphicsPreference, GraphicsSelection, select_graphics};
9use urushi_terminal::{
10 Command, CommandWriter, Event, EventSource, KeyboardEnhancementFlags, KeyboardEnhancementQuery,
11 Position, RawModeControl, SessionOptions, TerminalBackend, TerminalBackground,
12 TerminalCapabilities, TerminalOutput, TerminalQuery, TerminalSession, TerminalSize, WindowSize,
13};
14use urushi_tui::Screen;
15
16use super::application::Application;
17use super::core::{RuntimeCore, RuntimeError};
18use super::executor::{Clock, Executor, TokioClock, TokioExecutor};
19#[cfg(feature = "graphics")]
20use super::presentation::spawn_graphics_terminal;
21use super::presentation::{BlockingPresentation, BlockingPresentationError};
22use super::source::Surface;
23use super::sources::RuntimeSourceSpawner;
24use super::terminal_source::TerminalSourceSpawner;
25
26#[derive(Debug)]
28#[non_exhaustive]
29pub enum Error {
30 Terminal(io::Error),
32 Runtime(io::Error),
34}
35
36impl fmt::Display for Error {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::Terminal(error) => write!(formatter, "terminal runtime failed: {error}"),
40 Self::Runtime(error) => write!(formatter, "application runtime failed: {error}"),
41 }
42 }
43}
44
45impl std::error::Error for Error {
46 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
47 match self {
48 Self::Terminal(error) | Self::Runtime(error) => Some(error),
49 }
50 }
51}
52
53impl From<io::Error> for Error {
54 fn from(error: io::Error) -> Self {
55 Self::Terminal(error)
56 }
57}
58
59#[doc(hidden)]
61pub struct DefaultTerminal;
62
63pub struct Runtime<A, B = DefaultTerminal> {
74 application: A,
75 backend: B,
76 executor: Option<Arc<dyn Executor>>,
77 clock: Option<Arc<dyn Clock>>,
78 session: SessionOptions,
79 presentation: PresentationOptions,
80}
81
82#[derive(Clone, Copy, Debug, Default)]
83struct PresentationOptions {
84 #[cfg(feature = "graphics")]
85 graphics: GraphicsPreference,
86}
87
88impl<A> Runtime<A, DefaultTerminal> {
89 pub fn new(application: A) -> Self {
91 Self {
92 application,
93 backend: DefaultTerminal,
94 executor: None,
95 clock: None,
96 session: tui_session_options(),
97 presentation: PresentationOptions::default(),
98 }
99 }
100
101 #[cfg(feature = "crossterm")]
107 pub fn run(self) -> Result<A::Model, Error>
108 where
109 A: Application,
110 {
111 let Self {
112 application,
113 executor,
114 clock,
115 session,
116 presentation,
117 ..
118 } = self;
119 Runtime {
120 application,
121 backend: open_default_terminal()?,
122 executor,
123 clock,
124 session,
125 presentation,
126 }
127 .run()
128 }
129}
130
131#[cfg(all(feature = "crossterm", feature = "graphics", unix))]
132type ProductionTerminal = urushi_terminal::backend::native::NativeTerminal;
133
134#[cfg(all(feature = "crossterm", not(all(feature = "graphics", unix))))]
135type ProductionTerminal = urushi_terminal::backend::crossterm::CrosstermBackend<std::io::Stdout>;
136
137#[cfg(feature = "crossterm")]
145fn open_default_terminal() -> io::Result<ProductionTerminal> {
146 #[cfg(all(feature = "graphics", unix))]
147 {
148 ProductionTerminal::open()
149 }
150 #[cfg(not(all(feature = "graphics", unix)))]
151 {
152 Ok(ProductionTerminal::new(io::stdout()))
153 }
154}
155
156impl<A, B> Runtime<A, B> {
157 pub fn backend<U>(self, backend: U) -> Runtime<A, U>
159 where
160 U: TerminalBackend + Send + 'static,
161 {
162 Runtime {
163 application: self.application,
164 backend,
165 executor: self.executor,
166 clock: self.clock,
167 session: self.session,
168 presentation: self.presentation,
169 }
170 }
171
172 pub fn executor(mut self, executor: impl Executor) -> Self {
174 self.executor = Some(Arc::new(executor));
175 self
176 }
177
178 pub fn clock(mut self, clock: impl Clock) -> Self {
180 self.clock = Some(Arc::new(clock));
181 self
182 }
183
184 #[cfg(feature = "graphics")]
186 pub const fn graphics(mut self, preference: GraphicsPreference) -> Self {
187 self.presentation.graphics = preference;
188 self
189 }
190
191 pub const fn raw_mode(mut self, enabled: bool) -> Self {
193 self.session.raw_mode = enabled;
194 self
195 }
196
197 pub const fn alternate_screen(mut self, enabled: bool) -> Self {
199 self.session.alternate_screen = enabled;
200 self
201 }
202
203 pub const fn bracketed_paste(mut self, enabled: bool) -> Self {
205 self.session.bracketed_paste = enabled;
206 self
207 }
208
209 pub const fn focus_change(mut self, enabled: bool) -> Self {
211 self.session.focus_change = enabled;
212 self
213 }
214
215 pub const fn keyboard_enhancement(mut self, flags: Option<KeyboardEnhancementFlags>) -> Self {
217 self.session.keyboard_enhancement = flags;
218 self
219 }
220
221 pub const fn mouse(mut self, enabled: bool) -> Self {
223 self.session.mouse_capture = enabled;
224 self
225 }
226
227 pub const fn hide_cursor(mut self, enabled: bool) -> Self {
229 self.session.hide_cursor = enabled;
230 self
231 }
232}
233
234impl<A, B> Runtime<A, B>
235where
236 A: Application,
237 B: TerminalBackend + Send + 'static,
238{
239 pub fn run(self) -> Result<A::Model, Error> {
241 run_runtime(
242 self.application,
243 self.backend,
244 self.executor,
245 self.clock,
246 self.session,
247 self.presentation,
248 |shared, context, sources| {
249 let screen = Screen::new(shared, context.surface.size).map_err(Error::Terminal)?;
250 Ok(sized_presentation(screen, context, sources))
251 },
252 )
253 }
254}
255
256fn run_runtime<A, B>(
257 application: A,
258 backend: B,
259 executor: Option<Arc<dyn Executor>>,
260 clock: Option<Arc<dyn Clock>>,
261 session_options: SessionOptions,
262 presentation_options: PresentationOptions,
263 build_presentation: impl FnOnce(
264 SharedTerminal<B>,
265 PresentationContext,
266 Arc<TerminalSourceSpawner<SharedTerminal<B>, A::Message>>,
267 ) -> Result<BlockingPresentation, Error>,
268) -> Result<A::Model, Error>
269where
270 A: Application,
271 B: TerminalBackend + Send + 'static,
272{
273 #[cfg(not(feature = "graphics"))]
274 let _ = presentation_options;
275 let tokio = BackgroundRuntime::new()?;
276 let executor = executor.unwrap_or_else(|| {
277 Arc::new(TokioExecutor::new(tokio.handle().clone())) as Arc<dyn Executor>
278 });
279 let clock = clock.unwrap_or_else(|| Arc::new(TokioClock) as Arc<dyn Clock>);
280 let shared = SharedTerminal::new(backend);
281 let mut session_control = shared.clone();
282 let mut session = TerminalSession::enter(&mut session_control, session_options)
283 .map_err(|error| Error::Terminal(io::Error::other(error)))?;
284
285 let initial_window = match session.control_mut().window_size() {
286 Ok(size) => size,
287 Err(error) => {
288 return finish_with_restore(Err(Error::Terminal(error)), session.restore());
289 }
290 };
291 let initial_surface = Surface::from_window_size(initial_window);
292 #[cfg(feature = "graphics")]
293 let graphics = {
294 let capabilities = if presentation_options.graphics == GraphicsPreference::Text {
295 TerminalCapabilities::none()
296 } else {
297 match session.control_mut().terminal_capabilities() {
298 Ok(capabilities) => capabilities,
299 Err(error) => {
300 return finish_with_restore(Err(Error::Terminal(error)), session.restore());
301 }
302 }
303 };
304 match select_graphics(
305 presentation_options.graphics,
306 capabilities,
307 initial_surface.cell_pixels,
308 ) {
309 Ok(selection) => selection,
310 Err(error) => {
311 return finish_with_restore(
312 Err(Error::Terminal(io::Error::other(error))),
313 session.restore(),
314 );
315 }
316 }
317 };
318 let context = PresentationContext {
319 surface: initial_surface,
320 #[cfg(feature = "graphics")]
321 graphics,
322 };
323 let runtime_result = tokio.block_on(async move {
324 let fallback = Arc::new(RuntimeSourceSpawner::new(
325 Arc::clone(&executor),
326 Arc::clone(&clock),
327 ));
328 let sources = Arc::new(
329 TerminalSourceSpawner::new(shared.clone(), Arc::clone(&executor), fallback)
330 .map_err(Error::Terminal)?,
331 );
332 let presentation = build_presentation(shared, context, Arc::clone(&sources))?;
333 let core = RuntimeCore::new(application, executor, clock, sources, presentation)
334 .map_err(runtime_error)?;
335 core.run().await.map_err(runtime_error)
336 });
337
338 finish_with_restore(runtime_result, session.restore())
339}
340
341struct BackgroundRuntime(Option<tokio::runtime::Runtime>);
342
343impl BackgroundRuntime {
344 fn new() -> Result<Self, Error> {
345 tokio::runtime::Builder::new_current_thread()
346 .enable_all()
347 .build()
348 .map(|runtime| Self(Some(runtime)))
349 .map_err(Error::Runtime)
350 }
351
352 fn handle(&self) -> &tokio::runtime::Handle {
353 self.0.as_ref().expect("runtime is live").handle()
354 }
355
356 fn block_on<F: std::future::Future>(&self, future: F) -> F::Output {
357 self.0.as_ref().expect("runtime is live").block_on(future)
358 }
359}
360
361impl Drop for BackgroundRuntime {
362 fn drop(&mut self) {
363 if let Some(runtime) = self.0.take() {
364 runtime.shutdown_background();
365 }
366 }
367}
368
369#[derive(Clone, Copy, Debug)]
370struct PresentationContext {
371 surface: Surface,
372 #[cfg(feature = "graphics")]
373 graphics: GraphicsSelection,
374}
375
376fn sized_presentation<W, B, Message>(
377 screen: Screen<W>,
378 context: PresentationContext,
379 sources: Arc<TerminalSourceSpawner<SharedTerminal<B>, Message>>,
380) -> BlockingPresentation
381where
382 W: CommandWriter + Send + 'static,
383 B: TerminalBackend + Send + 'static,
384 Message: Send + 'static,
385{
386 #[cfg(feature = "graphics")]
387 {
388 spawn_graphics_terminal(screen, context.graphics, move || {
389 sources.presentation_surface(context.surface)
390 })
391 }
392 #[cfg(not(feature = "graphics"))]
393 {
394 BlockingPresentation::spawn_sized_terminal(screen, move || {
395 sources.presentation_surface(context.surface)
396 })
397 }
398}
399
400#[cfg(feature = "crossterm")]
402pub fn run<A>(application: A) -> Result<A::Model, Error>
403where
404 A: Application,
405{
406 Runtime::new(application).run()
407}
408
409fn runtime_error(error: RuntimeError<BlockingPresentationError>) -> Error {
410 match error {
411 RuntimeError::Terminal(error) => Error::Terminal(error),
412 RuntimeError::Presentation(error) => Error::Runtime(io::Error::other(error)),
413 }
414}
415
416fn finish_with_restore<Model>(
417 runtime: Result<Model, Error>,
418 restore: io::Result<()>,
419) -> Result<Model, Error> {
420 match (runtime, restore) {
421 (Ok(model), Ok(())) => Ok(model),
422 (Err(error), Ok(())) => Err(error),
423 (Ok(_), Err(error)) => Err(Error::Terminal(error)),
424 (Err(runtime), Err(restore)) => Err(Error::Terminal(io::Error::other(format!(
425 "{runtime}; terminal restoration also failed: {restore}"
426 )))),
427 }
428}
429
430const fn tui_session_options() -> SessionOptions {
431 SessionOptions {
432 raw_mode: true,
433 alternate_screen: true,
434 bracketed_paste: true,
435 focus_change: true,
436 keyboard_enhancement: Some(
437 KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
438 .union(KeyboardEnhancementFlags::REPORT_EVENT_TYPES),
439 ),
440 mouse_capture: false,
441 hide_cursor: true,
442 }
443}
444
445struct SharedTerminal<T> {
446 inner: Arc<Mutex<T>>,
447}
448
449impl<T> Clone for SharedTerminal<T> {
450 fn clone(&self) -> Self {
451 Self {
452 inner: Arc::clone(&self.inner),
453 }
454 }
455}
456
457impl<T> SharedTerminal<T> {
458 fn new(terminal: T) -> Self {
459 Self {
460 inner: Arc::new(Mutex::new(terminal)),
461 }
462 }
463
464 fn lock(&self) -> MutexGuard<'_, T> {
465 self.inner
466 .lock()
467 .unwrap_or_else(std::sync::PoisonError::into_inner)
468 }
469}
470
471impl<T: TerminalOutput> TerminalOutput for SharedTerminal<T> {
472 fn flush(&mut self) -> io::Result<()> {
473 self.lock().flush()
474 }
475}
476
477impl<T: CommandWriter> CommandWriter for SharedTerminal<T> {
478 fn write_command(&mut self, command: Command<'_>) -> io::Result<()> {
479 self.lock().write_command(command)
480 }
481}
482
483impl<T: EventSource> EventSource for SharedTerminal<T> {
484 fn read_event(&mut self) -> io::Result<Event> {
485 self.lock().read_event()
486 }
487
488 fn poll_event(&mut self) -> io::Result<Option<Event>> {
489 self.lock().poll_event()
490 }
491
492 fn poll_event_timeout(&mut self, timeout: std::time::Duration) -> io::Result<Option<Event>> {
493 let event = self.lock().poll_event()?;
494 if event.is_none() {
495 std::thread::sleep(timeout);
498 }
499 Ok(event)
500 }
501}
502
503impl<T: RawModeControl> RawModeControl for SharedTerminal<T> {
504 fn is_interactive(&self) -> bool {
505 self.lock().is_interactive()
506 }
507
508 fn enable_raw_mode(&mut self) -> io::Result<()> {
509 self.lock().enable_raw_mode()
510 }
511
512 fn disable_raw_mode(&mut self) -> io::Result<()> {
513 self.lock().disable_raw_mode()
514 }
515}
516
517impl<T: TerminalQuery> TerminalQuery for SharedTerminal<T> {
518 fn terminal_size(&mut self) -> io::Result<TerminalSize> {
519 self.lock().terminal_size()
520 }
521
522 fn cursor_position(&mut self) -> io::Result<Position> {
523 self.lock().cursor_position()
524 }
525
526 fn window_size(&mut self) -> io::Result<WindowSize> {
527 self.lock().window_size()
528 }
529
530 fn raw_mode_enabled(&mut self) -> io::Result<bool> {
531 self.lock().raw_mode_enabled()
532 }
533
534 fn terminal_capabilities(&mut self) -> io::Result<TerminalCapabilities> {
535 self.lock().terminal_capabilities()
536 }
537
538 fn terminal_background(&mut self) -> io::Result<Option<TerminalBackground>> {
539 self.lock().terminal_background()
540 }
541}
542
543impl<T: KeyboardEnhancementQuery> KeyboardEnhancementQuery for SharedTerminal<T> {
544 fn supports_keyboard_enhancement(&mut self) -> io::Result<bool> {
545 self.lock().supports_keyboard_enhancement()
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use std::collections::VecDeque;
552 use std::sync::Condvar;
553 use std::time::Duration;
554
555 use urushi::{TextStyle, View};
556 #[cfg(feature = "graphics")]
557 use urushi_terminal::TerminalGraphicsProtocols;
558 use urushi_terminal::{KeyCode, KeyEvent, PixelSize};
559
560 use super::*;
561 use crate::{Effect, Input, Subscription, Surface};
562
563 #[cfg(all(feature = "crossterm", feature = "graphics", unix))]
564 #[test]
565 fn graphics_default_uses_the_query_capable_native_connection() {
566 fn returns_native(
567 _open: fn() -> io::Result<urushi_terminal::backend::native::NativeTerminal>,
568 ) {
569 }
570
571 returns_native(open_default_terminal);
572 }
573
574 fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
575 mutex
576 .lock()
577 .unwrap_or_else(std::sync::PoisonError::into_inner)
578 }
579
580 #[derive(Default)]
581 struct ObservedTerminal {
582 events: VecDeque<Event>,
583 event_delay_polls: usize,
584 raw: bool,
585 alternate_screen: bool,
586 bracketed_paste: bool,
587 focus_change: bool,
588 keyboard_enhancement: bool,
589 cursor_visible: bool,
590 flushes: usize,
591 fail_window_query: bool,
592 window_pixels: Option<PixelSize>,
593 capabilities: Option<TerminalCapabilities>,
594 }
595
596 struct FakeTerminal {
597 observed: Arc<Mutex<ObservedTerminal>>,
598 size: TerminalSize,
599 }
600
601 impl TerminalOutput for FakeTerminal {
602 fn flush(&mut self) -> io::Result<()> {
603 lock(&self.observed).flushes += 1;
604 Ok(())
605 }
606 }
607
608 impl CommandWriter for FakeTerminal {
609 fn write_command(&mut self, command: Command<'_>) -> io::Result<()> {
610 let mut observed = lock(&self.observed);
611 match command {
612 Command::SetAlternateScreen(enabled) => observed.alternate_screen = enabled,
613 Command::SetBracketedPaste(enabled) => observed.bracketed_paste = enabled,
614 Command::SetFocusReporting(enabled) => observed.focus_change = enabled,
615 Command::PushKeyboardEnhancement(_) => observed.keyboard_enhancement = true,
616 Command::PopKeyboardEnhancement => observed.keyboard_enhancement = false,
617 Command::SetCursorVisible(visible) => observed.cursor_visible = visible,
618 _ => {}
619 }
620 Ok(())
621 }
622 }
623
624 impl EventSource for FakeTerminal {
625 fn read_event(&mut self) -> io::Result<Event> {
626 loop {
627 if let Some(event) = self.poll_event()? {
628 return Ok(event);
629 }
630 std::thread::yield_now();
631 }
632 }
633
634 fn poll_event(&mut self) -> io::Result<Option<Event>> {
635 let mut observed = lock(&self.observed);
636 if observed.event_delay_polls != 0 {
637 observed.event_delay_polls -= 1;
638 Ok(None)
639 } else {
640 Ok(observed.events.pop_front())
641 }
642 }
643
644 fn poll_event_timeout(&mut self, _timeout: Duration) -> io::Result<Option<Event>> {
645 let event = self.poll_event()?;
646 if event.is_none() {
647 std::thread::sleep(Duration::from_millis(1));
648 }
649 Ok(event)
650 }
651 }
652
653 impl RawModeControl for FakeTerminal {
654 fn is_interactive(&self) -> bool {
655 true
656 }
657
658 fn enable_raw_mode(&mut self) -> io::Result<()> {
659 lock(&self.observed).raw = true;
660 Ok(())
661 }
662
663 fn disable_raw_mode(&mut self) -> io::Result<()> {
664 lock(&self.observed).raw = false;
665 Ok(())
666 }
667 }
668
669 impl TerminalQuery for FakeTerminal {
670 fn terminal_size(&mut self) -> io::Result<TerminalSize> {
671 Ok(self.size)
672 }
673
674 fn cursor_position(&mut self) -> io::Result<Position> {
675 Ok(Position::new(0, 0))
676 }
677
678 fn window_size(&mut self) -> io::Result<WindowSize> {
679 let observed = lock(&self.observed);
680 if observed.fail_window_query {
681 return Err(io::Error::other("window query failed"));
682 }
683 Ok(WindowSize::new(self.size, observed.window_pixels))
684 }
685
686 fn raw_mode_enabled(&mut self) -> io::Result<bool> {
687 Ok(lock(&self.observed).raw)
688 }
689
690 fn terminal_capabilities(&mut self) -> io::Result<TerminalCapabilities> {
691 Ok(lock(&self.observed)
692 .capabilities
693 .unwrap_or_else(TerminalCapabilities::none))
694 }
695 }
696
697 impl KeyboardEnhancementQuery for FakeTerminal {
698 fn supports_keyboard_enhancement(&mut self) -> io::Result<bool> {
699 Ok(true)
700 }
701 }
702
703 struct ExampleApplication;
704
705 #[derive(Debug, Default, PartialEq, Eq)]
706 struct Model {
707 surface: Option<TerminalSize>,
708 effect_completed: bool,
709 }
710
711 enum Message {
712 Input(Input),
713 Surface(Surface),
714 EffectCompleted,
715 }
716
717 impl Application for ExampleApplication {
718 type Model = Model;
719 type Message = Message;
720
721 fn init(&self) -> (Self::Model, Effect<Self::Message>) {
722 (Model::default(), Effect::none())
723 }
724
725 fn update(&self, model: &mut Self::Model, message: Self::Message) -> Effect<Self::Message> {
726 match message {
727 Message::Surface(surface) => {
728 model.surface = Some(surface.size);
729 Effect::none()
730 }
731 Message::Input(Input::Key(_)) => Effect::perform(|| Message::EffectCompleted),
732 Message::Input(_) => Effect::none(),
733 Message::EffectCompleted => {
734 model.effect_completed = true;
735 Effect::shutdown()
736 }
737 }
738 }
739
740 fn view(&self, model: &Self::Model) -> View {
741 View::text(format!("done={}", model.effect_completed), TextStyle::new())
742 }
743
744 fn subscriptions(&self, _model: &Self::Model) -> Subscription<Self::Message> {
745 Subscription::batch([
746 Subscription::surface(Message::Surface),
747 Subscription::input(Message::Input),
748 ])
749 }
750 }
751
752 struct BlockingApplication {
753 started: std::sync::mpsc::Sender<()>,
754 exited: std::sync::mpsc::Sender<()>,
755 release: Arc<(Mutex<bool>, Condvar)>,
756 }
757
758 enum BlockingMessage {
759 Input,
760 Finished,
761 }
762
763 impl Application for BlockingApplication {
764 type Model = ();
765 type Message = BlockingMessage;
766
767 fn init(&self) -> (Self::Model, Effect<Self::Message>) {
768 let started = self.started.clone();
769 let exited = self.exited.clone();
770 let release = Arc::clone(&self.release);
771 let effect = Effect::perform(move || {
772 started.send(()).expect("test waits for effect start");
773 let (released, wake) = &*release;
774 let mut released = lock(released);
775 while !*released {
776 released = wake
777 .wait(released)
778 .unwrap_or_else(std::sync::PoisonError::into_inner);
779 }
780 exited.send(()).expect("test waits for effect exit");
781 BlockingMessage::Finished
782 });
783 ((), effect)
784 }
785
786 fn update(
787 &self,
788 _model: &mut Self::Model,
789 message: Self::Message,
790 ) -> Effect<Self::Message> {
791 match message {
792 BlockingMessage::Input => Effect::shutdown(),
793 BlockingMessage::Finished => Effect::none(),
794 }
795 }
796
797 fn view(&self, _model: &Self::Model) -> View {
798 View::text("blocking", TextStyle::new())
799 }
800
801 fn subscriptions(&self, _model: &Self::Model) -> Subscription<Self::Message> {
802 Subscription::input(|_| BlockingMessage::Input)
803 }
804 }
805
806 #[test]
807 fn public_runtime_wires_surface_input_effect_and_session_restoration() {
808 let observed = Arc::new(Mutex::new(ObservedTerminal {
809 events: VecDeque::from([Event::Key(KeyEvent::new(KeyCode::Enter))]),
810 event_delay_polls: 0,
811 cursor_visible: true,
812 ..ObservedTerminal::default()
813 }));
814 let size = TerminalSize::new(8, 2);
815 let terminal = FakeTerminal {
816 observed: Arc::clone(&observed),
817 size,
818 };
819
820 let model = Runtime::new(ExampleApplication)
821 .backend(terminal)
822 .run()
823 .expect("runtime completes");
824
825 assert_eq!(
826 model,
827 Model {
828 surface: Some(size),
829 effect_completed: true,
830 }
831 );
832 let observed = lock(&observed);
833 assert!(!observed.raw);
834 assert!(!observed.alternate_screen);
835 assert!(!observed.bracketed_paste);
836 assert!(!observed.focus_change);
837 assert!(!observed.keyboard_enhancement);
838 assert!(observed.cursor_visible);
839 assert!(observed.flushes >= 2);
840 }
841
842 #[cfg(feature = "graphics")]
843 #[test]
844 fn unavailable_explicit_graphics_preference_is_diagnostic_and_restores_the_session() {
845 let observed = Arc::new(Mutex::new(ObservedTerminal {
846 cursor_visible: true,
847 ..ObservedTerminal::default()
848 }));
849 let terminal = FakeTerminal {
850 observed: Arc::clone(&observed),
851 size: TerminalSize::new(8, 2),
852 };
853
854 let error = Runtime::new(ExampleApplication)
855 .backend(terminal)
856 .graphics(GraphicsPreference::Kitty)
857 .run()
858 .expect_err("an unsupported explicit protocol is rejected");
859
860 assert!(
861 error.to_string().contains(
862 "Kitty graphics were requested, but the terminal did not confirm support"
863 )
864 );
865 let observed = lock(&observed);
866 assert!(!observed.raw);
867 assert!(!observed.alternate_screen);
868 assert!(!observed.bracketed_paste);
869 assert!(!observed.focus_change);
870 assert!(!observed.keyboard_enhancement);
871 assert!(observed.cursor_visible);
872 }
873
874 #[cfg(feature = "graphics")]
875 #[test]
876 fn explicit_sixel_rejects_non_uniform_window_pixel_geometry() {
877 let observed = Arc::new(Mutex::new(ObservedTerminal {
878 cursor_visible: true,
879 window_pixels: Some(PixelSize::new(801, 480)),
880 capabilities: Some(
881 TerminalCapabilities::none()
882 .with_graphics_protocols(TerminalGraphicsProtocols::SIXEL),
883 ),
884 ..ObservedTerminal::default()
885 }));
886 let terminal = FakeTerminal {
887 observed: Arc::clone(&observed),
888 size: TerminalSize::new(80, 24),
889 };
890
891 let error = Runtime::new(ExampleApplication)
892 .backend(terminal)
893 .graphics(GraphicsPreference::Sixel)
894 .run()
895 .expect_err("non-uniform cell pixels cannot support Sixel");
896
897 assert!(error.to_string().contains(
898 "Sixel graphics were requested, but the terminal did not report uniform character-cell pixel geometry"
899 ));
900 let observed = lock(&observed);
901 assert!(!observed.raw);
902 assert!(!observed.alternate_screen);
903 assert!(observed.cursor_visible);
904 }
905
906 #[test]
907 fn shutdown_returns_while_started_blocking_work_finishes_in_the_background() {
908 let observed = Arc::new(Mutex::new(ObservedTerminal {
909 events: VecDeque::from([Event::Key(KeyEvent::new(KeyCode::Enter))]),
910 event_delay_polls: 2,
911 cursor_visible: true,
912 ..ObservedTerminal::default()
913 }));
914 let backend = FakeTerminal {
915 observed: Arc::clone(&observed),
916 size: TerminalSize::new(8, 2),
917 };
918 let (started_tx, started_rx) = std::sync::mpsc::channel();
919 let (exited_tx, exited_rx) = std::sync::mpsc::channel();
920 let release = Arc::new((Mutex::new(false), Condvar::new()));
921 let application = BlockingApplication {
922 started: started_tx,
923 exited: exited_tx,
924 release: Arc::clone(&release),
925 };
926 let (result_tx, result_rx) = std::sync::mpsc::channel();
927
928 std::thread::spawn(move || {
929 let result = Runtime::new(application).backend(backend).run();
930 result_tx.send(result).expect("test waits for run result");
931 });
932
933 started_rx
934 .recv_timeout(Duration::from_secs(1))
935 .expect("blocking effect starts");
936 result_rx
937 .recv_timeout(Duration::from_secs(1))
938 .expect("runtime returns without waiting for blocking effect")
939 .expect("shutdown succeeds");
940 assert!(!lock(&observed).raw);
941
942 let (released, wake) = &*release;
943 *lock(released) = true;
944 wake.notify_all();
945 exited_rx
946 .recv_timeout(Duration::from_secs(1))
947 .expect("background effect can finish after runtime return");
948 }
949
950 #[test]
951 fn setup_query_failure_restores_the_entered_session() {
952 let observed = Arc::new(Mutex::new(ObservedTerminal {
953 cursor_visible: true,
954 fail_window_query: true,
955 ..ObservedTerminal::default()
956 }));
957 let terminal = FakeTerminal {
958 observed: Arc::clone(&observed),
959 size: TerminalSize::new(8, 2),
960 };
961
962 let error = Runtime::new(ExampleApplication)
963 .backend(terminal)
964 .run()
965 .expect_err("window query failure ends setup");
966
967 assert!(error.to_string().contains("window query failed"));
968 let observed = lock(&observed);
969 assert!(!observed.raw);
970 assert!(!observed.alternate_screen);
971 assert!(!observed.bracketed_paste);
972 assert!(!observed.focus_change);
973 assert!(!observed.keyboard_enhancement);
974 assert!(observed.cursor_visible);
975 }
976}