Skip to main content

par_term/
traits_impl.rs

1//! Concrete implementations of the traits defined in [`crate::traits`].
2//!
3//! # What is implemented here
4//!
5//! - [`TerminalAccess`] on [`par_term_terminal::TerminalManager`] — all five
6//!   read-only query methods are direct thin wrappers over the existing
7//!   `TerminalManager` methods, so there is no behaviour change at call sites.
8//!
9//! - [`UIElement`] on [`crate::tab_bar_ui::TabBarUI`] and
10//!   [`crate::status_bar::StatusBarUI`] — each type defines its own `Ctx<'a>`
11//!   holding the per-frame references it needs (config, tab count, fullscreen flag).
12//!   Existing call sites are unaffected; the trait provides a new generic path.
13//!
14//! # How to use `TerminalAccess` in new code
15//!
16//! Any function that only needs to inspect terminal mode (e.g. mouse handlers,
17//! input encoders) can be written against `T: TerminalAccess` instead of
18//! taking a concrete `&TerminalManager`. This makes the function testable with
19//! a lightweight mock:
20//!
21//! ```rust,ignore
22//! use par_term::traits::TerminalAccess;
23//!
24//! fn encode_if_tracking<T: TerminalAccess>(t: &T, button: u8, col: usize, row: usize) -> Vec<u8> {
25//!     if t.is_mouse_tracking_active() {
26//!         t.encode_mouse_event(button, col, row, true, 0)
27//!     } else {
28//!         Vec::new()
29//!     }
30//! }
31//! ```
32//!
33//! See `src/traits.rs` for the full trait contract and migration path notes.
34
35use crate::config::Config;
36use crate::status_bar::StatusBarUI;
37use crate::tab_bar_ui::TabBarUI;
38use crate::traits::{TerminalAccess, UIElement};
39use par_term_terminal::TerminalManager;
40
41impl TerminalAccess for TerminalManager {
42    /// Returns `true` if the alternate screen buffer (DECSC/smcup) is active.
43    ///
44    /// Delegates to [`TerminalManager::is_alt_screen_active`].
45    fn is_alt_screen_active(&self) -> bool {
46        self.is_alt_screen_active()
47    }
48
49    /// Returns `true` if mouse motion events should be forwarded to the PTY.
50    ///
51    /// Delegates to [`TerminalManager::should_report_mouse_motion`].
52    fn should_report_mouse_motion(&self, button_pressed: bool) -> bool {
53        self.should_report_mouse_motion(button_pressed)
54    }
55
56    /// Returns the current modifyOtherKeys level (0 = off, 1 = basic, 2 = full).
57    ///
58    /// Delegates to [`TerminalManager::modify_other_keys_mode`].
59    fn modify_other_keys_mode(&self) -> u8 {
60        self.modify_other_keys_mode()
61    }
62
63    /// Returns `true` if DECCKM (application cursor key) mode is active.
64    ///
65    /// Delegates to [`TerminalManager::application_cursor`].
66    fn application_cursor(&self) -> bool {
67        self.application_cursor()
68    }
69
70    /// Encode a mouse event into the bytes to be written to the PTY.
71    ///
72    /// Delegates to [`TerminalManager::encode_mouse_event`].
73    fn encode_mouse_event(
74        &self,
75        button: u8,
76        col: usize,
77        row: usize,
78        pressed: bool,
79        modifiers: u8,
80    ) -> Vec<u8> {
81        self.encode_mouse_event(button, col, row, pressed, modifiers)
82    }
83}
84
85// ── UIElement implementations ─────────────────────────────────────────────────
86
87/// Per-call context for [`TabBarUI`].
88///
89/// Holds the two parameters that `TabBarUI` needs to compute its layout
90/// dimensions: the global config reference and the current tab count.
91pub struct TabBarCtx<'a> {
92    /// Global application configuration (read-only borrow for this frame).
93    pub config: &'a Config,
94    /// Number of open tabs in the current window.
95    pub tab_count: usize,
96}
97
98impl UIElement for TabBarUI {
99    type Ctx<'a> = TabBarCtx<'a>;
100
101    /// Returns `true` when the tab bar is configured to show given the current
102    /// tab count and `config.tab_bar_mode`.
103    fn is_visible(&self, ctx: Self::Ctx<'_>) -> bool {
104        self.should_show(ctx.tab_count, ctx.config.tabs.tab_bar_mode)
105    }
106
107    /// Effective height of the tab bar in logical pixels.
108    ///
109    /// Non-zero only when the bar is visible **and** positioned horizontally
110    /// (top or bottom).  Returns 0.0 for a left-side tab bar.
111    fn height_logical(&self, ctx: Self::Ctx<'_>) -> f32 {
112        self.get_height(ctx.tab_count, ctx.config)
113    }
114
115    /// Effective width of the tab bar in logical pixels.
116    ///
117    /// Non-zero only when the bar is visible **and** positioned on the left side.
118    /// Returns 0.0 for top/bottom tab bars.
119    fn width_logical(&self, ctx: Self::Ctx<'_>) -> f32 {
120        self.get_width(ctx.tab_count, ctx.config)
121    }
122
123    /// Returns `true` when the tab rename field or a context menu is open,
124    /// indicating that keyboard input should not be forwarded to the terminal.
125    fn is_capturing_input(&self) -> bool {
126        self.is_renaming() || self.is_context_menu_open() || self.is_app_menu_open()
127    }
128}
129
130/// Per-call context for [`StatusBarUI`].
131///
132/// Holds the two parameters that `StatusBarUI` needs to determine its
133/// visibility and height: the global config reference and whether the window
134/// is currently fullscreen.
135pub struct StatusBarCtx<'a> {
136    /// Global application configuration (read-only borrow for this frame).
137    pub config: &'a Config,
138    /// Whether the window is currently in fullscreen mode.
139    pub is_fullscreen: bool,
140}
141
142impl UIElement for StatusBarUI {
143    type Ctx<'a> = StatusBarCtx<'a>;
144
145    /// Returns `true` when the status bar is enabled and not hidden by the
146    /// current window/mouse state.
147    fn is_visible(&self, ctx: Self::Ctx<'_>) -> bool {
148        !self.should_hide(ctx.config, ctx.is_fullscreen)
149    }
150
151    /// Effective height of the status bar in logical pixels.
152    ///
153    /// Delegates to `StatusBarUI::height`, which returns 0.0 when hidden.
154    fn height_logical(&self, ctx: Self::Ctx<'_>) -> f32 {
155        self.height(ctx.config, ctx.is_fullscreen)
156    }
157
158    /// Width of the status bar in logical pixels.
159    ///
160    /// The status bar always spans the full window width, so this returns 0.0
161    /// (the bar does not consume horizontal space from the sides).
162    fn width_logical(&self, _ctx: Self::Ctx<'_>) -> f32 {
163        0.0
164    }
165
166    /// Returns `false` — the status bar is read-only and never captures input.
167    fn is_capturing_input(&self) -> bool {
168        false
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    /// Minimal mock that implements `TerminalAccess` without a live PTY session.
177    ///
178    /// Used to verify the trait object interface at compile time and exercise the
179    /// method dispatch without spinning up a PTY.
180    struct MockTerminal {
181        alt_screen: bool,
182        mouse_any_event: bool,
183        modify_other_keys: u8,
184        app_cursor: bool,
185    }
186
187    impl MockTerminal {
188        fn new() -> Self {
189            Self {
190                alt_screen: false,
191                mouse_any_event: false,
192                modify_other_keys: 0,
193                app_cursor: false,
194            }
195        }
196
197        fn with_alt_screen(mut self) -> Self {
198            self.alt_screen = true;
199            self
200        }
201
202        fn with_any_event_mouse(mut self) -> Self {
203            self.mouse_any_event = true;
204            self
205        }
206
207        fn with_app_cursor(mut self) -> Self {
208            self.app_cursor = true;
209            self
210        }
211
212        fn with_modify_other_keys(mut self, level: u8) -> Self {
213            self.modify_other_keys = level;
214            self
215        }
216    }
217
218    impl TerminalAccess for MockTerminal {
219        fn is_alt_screen_active(&self) -> bool {
220            self.alt_screen
221        }
222
223        fn should_report_mouse_motion(&self, _button_pressed: bool) -> bool {
224            self.mouse_any_event
225        }
226
227        fn modify_other_keys_mode(&self) -> u8 {
228            self.modify_other_keys
229        }
230
231        fn application_cursor(&self) -> bool {
232            self.app_cursor
233        }
234
235        fn encode_mouse_event(
236            &self,
237            button: u8,
238            col: usize,
239            row: usize,
240            _pressed: bool,
241            _modifiers: u8,
242        ) -> Vec<u8> {
243            // Minimal stub: return a recognisable byte sequence for assertions.
244            vec![b'\x1b', b'[', b'M', button, col as u8, row as u8]
245        }
246    }
247
248    // ── TerminalAccess contract tests ─────────────────────────────────────
249
250    #[test]
251    fn mock_alt_screen_default_false() {
252        let t = MockTerminal::new();
253        assert!(!t.is_alt_screen_active());
254    }
255
256    #[test]
257    fn mock_alt_screen_activated() {
258        let t = MockTerminal::new().with_alt_screen();
259        assert!(t.is_alt_screen_active());
260    }
261
262    #[test]
263    fn mock_mouse_motion_off_by_default() {
264        let t = MockTerminal::new();
265        assert!(!t.should_report_mouse_motion(false));
266        assert!(!t.should_report_mouse_motion(true));
267    }
268
269    #[test]
270    fn mock_mouse_motion_on_with_any_event() {
271        let t = MockTerminal::new().with_any_event_mouse();
272        assert!(t.should_report_mouse_motion(false));
273        assert!(t.should_report_mouse_motion(true));
274    }
275
276    #[test]
277    fn mock_modify_other_keys_default_zero() {
278        let t = MockTerminal::new();
279        assert_eq!(t.modify_other_keys_mode(), 0);
280    }
281
282    #[test]
283    fn mock_modify_other_keys_level_2() {
284        let t = MockTerminal::new().with_modify_other_keys(2);
285        assert_eq!(t.modify_other_keys_mode(), 2);
286    }
287
288    #[test]
289    fn mock_application_cursor_default_false() {
290        let t = MockTerminal::new();
291        assert!(!t.application_cursor());
292    }
293
294    #[test]
295    fn mock_application_cursor_activated() {
296        let t = MockTerminal::new().with_app_cursor();
297        assert!(t.application_cursor());
298    }
299
300    #[test]
301    fn mock_encode_mouse_event_returns_bytes() {
302        let t = MockTerminal::new();
303        let bytes = t.encode_mouse_event(0, 10, 5, true, 0);
304        // Our mock stub returns a 6-byte sequence starting with ESC [ M.
305        assert_eq!(bytes.len(), 6);
306        assert_eq!(bytes[0], b'\x1b');
307        assert_eq!(bytes[1], b'[');
308        assert_eq!(bytes[2], b'M');
309        assert_eq!(bytes[3], 0); // button
310        assert_eq!(bytes[4], 10); // col
311        assert_eq!(bytes[5], 5); // row
312    }
313
314    /// Compile-time check: a function generic over `T: TerminalAccess` can
315    /// accept either `TerminalManager` or `MockTerminal`.
316    fn query_mode<T: TerminalAccess>(t: &T) -> (bool, bool, u8) {
317        (
318            t.is_alt_screen_active(),
319            t.application_cursor(),
320            t.modify_other_keys_mode(),
321        )
322    }
323
324    #[test]
325    fn generic_function_works_with_mock() {
326        let t = MockTerminal::new().with_alt_screen().with_app_cursor();
327        let (alt, cursor, mok) = query_mode(&t);
328        assert!(alt);
329        assert!(cursor);
330        assert_eq!(mok, 0);
331    }
332
333    // ── UIElement contract tests ───────────────────────────────────────────
334
335    /// Compile-time check: a function generic over `T: UIElement` can query
336    /// height and visibility without knowing the concrete type.
337    fn query_element<T>(element: &T, ctx: T::Ctx<'_>) -> (bool, f32, f32, bool)
338    where
339        T: UIElement,
340    {
341        // We can't call the ctx-taking methods twice with the same ctx because
342        // Ctx is moved on the first call.  Instead return the results as a tuple
343        // so the caller can assert each individually with separate ctx values.
344        let _ = element.height_logical(ctx);
345        (false, 0.0, 0.0, element.is_capturing_input())
346    }
347
348    /// Verify that `TabBarUI::is_capturing_input` returns false when no
349    /// interactive state is active (no rename, no context menu).
350    #[test]
351    fn tab_bar_ui_not_capturing_input_by_default() {
352        let tab_bar = crate::tab_bar_ui::TabBarUI::new();
353        assert!(!tab_bar.is_capturing_input());
354    }
355
356    /// Verify that `StatusBarUI::is_capturing_input` always returns false.
357    #[test]
358    fn status_bar_ui_never_captures_input() {
359        let status_bar = crate::status_bar::StatusBarUI::new();
360        assert!(!status_bar.is_capturing_input());
361    }
362
363    /// Verify that `StatusBarUI::width_logical` always returns 0.0 (the
364    /// status bar spans the full window width, not a side panel).
365    #[test]
366    fn status_bar_ui_width_is_zero() {
367        use crate::traits_impl::StatusBarCtx;
368        let status_bar = crate::status_bar::StatusBarUI::new();
369        let config = crate::config::Config::default();
370        let ctx = StatusBarCtx {
371            config: &config,
372            is_fullscreen: false,
373        };
374        assert_eq!(status_bar.width_logical(ctx), 0.0);
375    }
376
377    /// Verify that `TabBarUI::height_logical` returns the configured tab bar
378    /// height when `TabBarMode::Always` is active and the bar is horizontal.
379    #[test]
380    fn tab_bar_ui_height_matches_config() {
381        use crate::traits_impl::TabBarCtx;
382        let tab_bar = crate::tab_bar_ui::TabBarUI::new();
383        let config = crate::config::Config::default();
384        // Default config has TabBarMode::Always and horizontal position,
385        // so height_logical should equal config.tab_bar_height.
386        let height = tab_bar.height_logical(TabBarCtx {
387            config: &config,
388            tab_count: 1,
389        });
390        assert_eq!(height, config.tabs.tab_bar_height);
391    }
392
393    /// Compile-time check: `query_element` can be instantiated with both
394    /// `TabBarUI` and `StatusBarUI`, confirming they share the `UIElement` bound.
395    #[test]
396    fn ui_element_generic_function_compiles_for_both_types() {
397        use crate::traits_impl::{StatusBarCtx, TabBarCtx};
398        let tab_bar = crate::tab_bar_ui::TabBarUI::new();
399        let status_bar = crate::status_bar::StatusBarUI::new();
400        let config = crate::config::Config::default();
401
402        let (_, _, _, capturing_tab) = query_element(
403            &tab_bar,
404            TabBarCtx {
405                config: &config,
406                tab_count: 1,
407            },
408        );
409        let (_, _, _, capturing_status) = query_element(
410            &status_bar,
411            StatusBarCtx {
412                config: &config,
413                is_fullscreen: false,
414            },
415        );
416        // Neither should be capturing input in default state.
417        assert!(!capturing_tab);
418        assert!(!capturing_status);
419    }
420}