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}