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