Skip to main content

ftui_backend/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = "Backend traits for FrankenTUI: platform abstraction for input, presentation, and time."]
3#![doc = ""]
4#![doc = "This crate defines the boundary between the ftui runtime and platform-specific"]
5#![doc = "implementations (native terminal via `ftui-tty`, WASM via `ftui-web`)."]
6#![doc = ""]
7#![doc = "See ADR-008 for the design rationale."]
8
9use core::time::Duration;
10
11use ftui_core::event::Event;
12use ftui_core::terminal_capabilities::TerminalCapabilities;
13use ftui_render::buffer::Buffer;
14use ftui_render::diff::BufferDiff;
15
16/// Terminal feature toggles that backends must support.
17///
18/// These map to terminal modes that are enabled/disabled at session start/end.
19/// Backends translate these into platform-specific escape sequences or API calls.
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub struct BackendFeatures {
22    /// SGR mouse capture (CSI ? 1000;1002;1006 h/l on native).
23    pub mouse_capture: bool,
24    /// Bracketed paste mode (CSI ? 2004 h/l on native).
25    pub bracketed_paste: bool,
26    /// Focus-in/focus-out reporting (CSI ? 1004 h/l on native).
27    pub focus_events: bool,
28    /// Kitty keyboard protocol (CSI > 15 u on native).
29    pub kitty_keyboard: bool,
30}
31
32/// Monotonic clock abstraction.
33///
34/// Native backends use `std::time::Instant`; WASM backends use `performance.now()`.
35/// The runtime never calls `Instant::now()` directly — all time flows through this trait.
36pub trait BackendClock {
37    /// Returns elapsed time since an unspecified epoch, monotonically increasing.
38    fn now_mono(&self) -> Duration;
39}
40
41/// Event source abstraction: terminal size queries, feature toggles, and event I/O.
42///
43/// This is the input half of the backend boundary. The runtime polls this for
44/// canonical `Event` values without knowing whether they come from crossterm,
45/// raw Unix reads, or DOM events.
46pub trait BackendEventSource {
47    /// Platform-specific error type.
48    type Error: core::fmt::Debug + core::fmt::Display;
49
50    /// Query current terminal dimensions (columns, rows).
51    fn size(&self) -> Result<(u16, u16), Self::Error>;
52
53    /// Enable or disable terminal features (mouse, paste, focus, kitty keyboard).
54    ///
55    /// Backends must track current state and only emit escape sequences for changes.
56    fn set_features(&mut self, features: BackendFeatures) -> Result<(), Self::Error>;
57
58    /// Poll for an available event, returning `true` if one is ready.
59    ///
60    /// Must not block longer than `timeout`. Returns `Ok(false)` on timeout.
61    fn poll_event(&mut self, timeout: Duration) -> Result<bool, Self::Error>;
62
63    /// Read the next available event, or `None` if none is ready.
64    ///
65    /// Call after `poll_event` returns `true`, or speculatively.
66    fn read_event(&mut self) -> Result<Option<Event>, Self::Error>;
67}
68
69/// Presentation abstraction: UI rendering and log output.
70///
71/// This is the output half of the backend boundary. The runtime hands a `Buffer`
72/// (and optional `BufferDiff`) to the presenter, which emits platform-specific
73/// output (ANSI escape sequences on native, DOM mutations on web).
74pub trait BackendPresenter {
75    /// Platform-specific error type.
76    type Error: core::fmt::Debug + core::fmt::Display;
77
78    /// Terminal capabilities detected by this backend.
79    fn capabilities(&self) -> &TerminalCapabilities;
80
81    /// Write a log line to the scrollback region (inline mode) or stderr.
82    fn write_log(&mut self, text: &str) -> Result<(), Self::Error>;
83
84    /// Present a UI frame.
85    ///
86    /// - `buf`: the full rendered buffer for this frame.
87    /// - `diff`: optional pre-computed diff (backends may recompute if `None`).
88    /// - `full_repaint_hint`: if `true`, the backend should skip diffing and repaint everything.
89    fn present_ui(
90        &mut self,
91        buf: &Buffer,
92        diff: Option<&BufferDiff>,
93        full_repaint_hint: bool,
94    ) -> Result<(), Self::Error>;
95
96    /// Optional: release resources held by the presenter (e.g., grapheme pool compaction).
97    fn gc(&mut self) {}
98}
99
100/// Unified backend combining clock, event source, and presenter.
101///
102/// The `Program` runtime is generic over this trait. Concrete implementations:
103/// - `ftui-tty`: native Unix/macOS terminal (and eventually Windows).
104/// - `ftui-web`: WASM + DOM + WebGPU renderer.
105pub trait Backend {
106    /// Platform-specific error type shared across sub-traits.
107    type Error: core::fmt::Debug + core::fmt::Display;
108
109    /// Clock implementation.
110    type Clock: BackendClock;
111
112    /// Event source implementation.
113    type Events: BackendEventSource<Error = Self::Error>;
114
115    /// Presenter implementation.
116    type Presenter: BackendPresenter<Error = Self::Error>;
117
118    /// Access the monotonic clock.
119    fn clock(&self) -> &Self::Clock;
120
121    /// Access the event source (mutable for polling/reading).
122    fn events(&mut self) -> &mut Self::Events;
123
124    /// Access the presenter (mutable for rendering).
125    fn presenter(&mut self) -> &mut Self::Presenter;
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use core::fmt;
132    use ftui_core::terminal_capabilities::TerminalCapabilities;
133
134    // -----------------------------------------------------------------------
135    // BackendFeatures tests
136    // -----------------------------------------------------------------------
137
138    #[test]
139    fn backend_features_default_all_false() {
140        let f = BackendFeatures::default();
141        assert!(!f.mouse_capture);
142        assert!(!f.bracketed_paste);
143        assert!(!f.focus_events);
144        assert!(!f.kitty_keyboard);
145    }
146
147    #[test]
148    fn backend_features_equality() {
149        let a = BackendFeatures {
150            mouse_capture: true,
151            bracketed_paste: false,
152            focus_events: true,
153            kitty_keyboard: false,
154        };
155        let b = BackendFeatures {
156            mouse_capture: true,
157            bracketed_paste: false,
158            focus_events: true,
159            kitty_keyboard: false,
160        };
161        assert_eq!(a, b);
162    }
163
164    #[test]
165    fn backend_features_inequality() {
166        let a = BackendFeatures::default();
167        let b = BackendFeatures {
168            mouse_capture: true,
169            ..BackendFeatures::default()
170        };
171        assert_ne!(a, b);
172    }
173
174    #[test]
175    fn backend_features_clone() {
176        let a = BackendFeatures {
177            mouse_capture: true,
178            bracketed_paste: true,
179            focus_events: true,
180            kitty_keyboard: true,
181        };
182        let b = a;
183        assert_eq!(a, b);
184    }
185
186    #[test]
187    fn backend_features_debug() {
188        let f = BackendFeatures::default();
189        let debug = format!("{f:?}");
190        assert!(debug.contains("BackendFeatures"));
191        assert!(debug.contains("mouse_capture"));
192    }
193
194    // -----------------------------------------------------------------------
195    // Mock implementations for trait testing
196    // -----------------------------------------------------------------------
197
198    struct TestClock {
199        elapsed: Duration,
200    }
201
202    impl BackendClock for TestClock {
203        fn now_mono(&self) -> Duration {
204            self.elapsed
205        }
206    }
207
208    #[derive(Debug)]
209    struct TestError(String);
210
211    impl fmt::Display for TestError {
212        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213            write!(f, "TestError: {}", self.0)
214        }
215    }
216
217    struct TestEventSource {
218        features: BackendFeatures,
219        events: Vec<Event>,
220    }
221
222    impl BackendEventSource for TestEventSource {
223        type Error = TestError;
224
225        fn size(&self) -> Result<(u16, u16), Self::Error> {
226            Ok((80, 24))
227        }
228
229        fn set_features(&mut self, features: BackendFeatures) -> Result<(), Self::Error> {
230            self.features = features;
231            Ok(())
232        }
233
234        fn poll_event(&mut self, _timeout: Duration) -> Result<bool, Self::Error> {
235            Ok(!self.events.is_empty())
236        }
237
238        fn read_event(&mut self) -> Result<Option<Event>, Self::Error> {
239            Ok(if self.events.is_empty() {
240                None
241            } else {
242                Some(self.events.remove(0))
243            })
244        }
245    }
246
247    struct TestPresenter {
248        caps: TerminalCapabilities,
249        logs: Vec<String>,
250        present_count: usize,
251        gc_count: usize,
252    }
253
254    impl BackendPresenter for TestPresenter {
255        type Error = TestError;
256
257        fn capabilities(&self) -> &TerminalCapabilities {
258            &self.caps
259        }
260
261        fn write_log(&mut self, text: &str) -> Result<(), Self::Error> {
262            self.logs.push(text.to_owned());
263            Ok(())
264        }
265
266        fn present_ui(
267            &mut self,
268            _buf: &Buffer,
269            _diff: Option<&BufferDiff>,
270            _full_repaint_hint: bool,
271        ) -> Result<(), Self::Error> {
272            self.present_count += 1;
273            Ok(())
274        }
275
276        fn gc(&mut self) {
277            self.gc_count += 1;
278        }
279    }
280
281    struct TestBackend {
282        clock: TestClock,
283        events: TestEventSource,
284        presenter: TestPresenter,
285    }
286
287    impl Backend for TestBackend {
288        type Error = TestError;
289        type Clock = TestClock;
290        type Events = TestEventSource;
291        type Presenter = TestPresenter;
292
293        fn clock(&self) -> &Self::Clock {
294            &self.clock
295        }
296
297        fn events(&mut self) -> &mut Self::Events {
298            &mut self.events
299        }
300
301        fn presenter(&mut self) -> &mut Self::Presenter {
302            &mut self.presenter
303        }
304    }
305
306    fn make_test_backend() -> TestBackend {
307        TestBackend {
308            clock: TestClock {
309                elapsed: Duration::from_millis(42),
310            },
311            events: TestEventSource {
312                features: BackendFeatures::default(),
313                events: Vec::new(),
314            },
315            presenter: TestPresenter {
316                caps: TerminalCapabilities::default(),
317                logs: Vec::new(),
318                present_count: 0,
319                gc_count: 0,
320            },
321        }
322    }
323
324    // -----------------------------------------------------------------------
325    // BackendClock tests
326    // -----------------------------------------------------------------------
327
328    #[test]
329    fn clock_returns_elapsed() {
330        let clock = TestClock {
331            elapsed: Duration::from_secs(5),
332        };
333        assert_eq!(clock.now_mono(), Duration::from_secs(5));
334    }
335
336    #[test]
337    fn clock_zero_duration() {
338        let clock = TestClock {
339            elapsed: Duration::ZERO,
340        };
341        assert_eq!(clock.now_mono(), Duration::ZERO);
342    }
343
344    // -----------------------------------------------------------------------
345    // BackendEventSource tests
346    // -----------------------------------------------------------------------
347
348    #[test]
349    fn event_source_size() {
350        let src = TestEventSource {
351            features: BackendFeatures::default(),
352            events: Vec::new(),
353        };
354        assert_eq!(src.size().unwrap(), (80, 24));
355    }
356
357    #[test]
358    fn event_source_set_features() {
359        let mut src = TestEventSource {
360            features: BackendFeatures::default(),
361            events: Vec::new(),
362        };
363        let features = BackendFeatures {
364            mouse_capture: true,
365            bracketed_paste: true,
366            focus_events: false,
367            kitty_keyboard: false,
368        };
369        src.set_features(features).unwrap();
370        assert!(src.features.mouse_capture);
371        assert!(src.features.bracketed_paste);
372    }
373
374    #[test]
375    fn event_source_poll_empty() {
376        let mut src = TestEventSource {
377            features: BackendFeatures::default(),
378            events: Vec::new(),
379        };
380        assert!(!src.poll_event(Duration::from_millis(10)).unwrap());
381    }
382
383    #[test]
384    fn event_source_read_none_when_empty() {
385        let mut src = TestEventSource {
386            features: BackendFeatures::default(),
387            events: Vec::new(),
388        };
389        assert!(src.read_event().unwrap().is_none());
390    }
391
392    #[test]
393    fn event_source_poll_with_events() {
394        let mut src = TestEventSource {
395            features: BackendFeatures::default(),
396            events: vec![Event::Focus(true)],
397        };
398        assert!(src.poll_event(Duration::from_millis(10)).unwrap());
399    }
400
401    #[test]
402    fn event_source_read_drains_events() {
403        let mut src = TestEventSource {
404            features: BackendFeatures::default(),
405            events: vec![Event::Focus(true), Event::Focus(false)],
406        };
407        let e1 = src.read_event().unwrap();
408        assert!(e1.is_some());
409        let e2 = src.read_event().unwrap();
410        assert!(e2.is_some());
411        let e3 = src.read_event().unwrap();
412        assert!(e3.is_none());
413    }
414
415    // -----------------------------------------------------------------------
416    // BackendPresenter tests
417    // -----------------------------------------------------------------------
418
419    #[test]
420    fn presenter_capabilities() {
421        let p = TestPresenter {
422            caps: TerminalCapabilities::default(),
423            logs: Vec::new(),
424            present_count: 0,
425            gc_count: 0,
426        };
427        let _caps = p.capabilities();
428    }
429
430    #[test]
431    fn presenter_write_log() {
432        let mut p = TestPresenter {
433            caps: TerminalCapabilities::default(),
434            logs: Vec::new(),
435            present_count: 0,
436            gc_count: 0,
437        };
438        p.write_log("hello").unwrap();
439        p.write_log("world").unwrap();
440        assert_eq!(p.logs.len(), 2);
441        assert_eq!(p.logs[0], "hello");
442        assert_eq!(p.logs[1], "world");
443    }
444
445    #[test]
446    fn presenter_present_ui() {
447        let mut p = TestPresenter {
448            caps: TerminalCapabilities::default(),
449            logs: Vec::new(),
450            present_count: 0,
451            gc_count: 0,
452        };
453        let buf = Buffer::new(10, 5);
454        p.present_ui(&buf, None, false).unwrap();
455        p.present_ui(&buf, None, true).unwrap();
456        assert_eq!(p.present_count, 2);
457    }
458
459    #[test]
460    fn presenter_gc() {
461        let mut p = TestPresenter {
462            caps: TerminalCapabilities::default(),
463            logs: Vec::new(),
464            present_count: 0,
465            gc_count: 0,
466        };
467        p.gc();
468        p.gc();
469        assert_eq!(p.gc_count, 2);
470    }
471
472    // -----------------------------------------------------------------------
473    // Unified Backend tests
474    // -----------------------------------------------------------------------
475
476    #[test]
477    fn backend_clock_access() {
478        let backend = make_test_backend();
479        assert_eq!(backend.clock().now_mono(), Duration::from_millis(42));
480    }
481
482    #[test]
483    fn backend_events_access() {
484        let mut backend = make_test_backend();
485        let size = backend.events().size().unwrap();
486        assert_eq!(size, (80, 24));
487    }
488
489    #[test]
490    fn backend_presenter_access() {
491        let mut backend = make_test_backend();
492        let buf = Buffer::new(10, 5);
493        backend.presenter().present_ui(&buf, None, false).unwrap();
494        assert_eq!(backend.presenter.present_count, 1);
495    }
496
497    #[test]
498    fn backend_full_cycle() {
499        let mut backend = make_test_backend();
500
501        // Clock
502        let _now = backend.clock().now_mono();
503
504        // Features
505        backend
506            .events()
507            .set_features(BackendFeatures {
508                mouse_capture: true,
509                ..BackendFeatures::default()
510            })
511            .unwrap();
512        assert!(backend.events.features.mouse_capture);
513
514        // Present
515        let buf = Buffer::new(80, 24);
516        backend.presenter().write_log("frame start").unwrap();
517        backend.presenter().present_ui(&buf, None, false).unwrap();
518        backend.presenter().gc();
519
520        assert_eq!(backend.presenter.logs.len(), 1);
521        assert_eq!(backend.presenter.present_count, 1);
522        assert_eq!(backend.presenter.gc_count, 1);
523    }
524
525    // -----------------------------------------------------------------------
526    // Error type tests
527    // -----------------------------------------------------------------------
528
529    #[test]
530    fn test_error_display() {
531        let err = TestError("something failed".into());
532        assert_eq!(format!("{err}"), "TestError: something failed");
533    }
534
535    #[test]
536    fn test_error_debug() {
537        let err = TestError("oops".into());
538        let debug = format!("{err:?}");
539        assert!(debug.contains("oops"));
540    }
541}