1use muxr_config::MuxrConfig;
2use muxr_core::ClientMouseEvent;
3use muxr_core::ClientMouseEventPhase;
4use muxr_core::ClientMousePosition;
5use muxr_core::ClientRequest;
6
7use crate::copy_selection::SelectionInput;
8use crate::pane::focus::LocalMouseAction;
9use crate::renderer::ClientRenderer;
10use crate::renderer::FileOpenRelease;
11use crate::runtime::ClientInputSend;
12use crate::runtime::DroppableSendOutcome;
13
14pub async fn handle_mouse_input_action(
15 muxr_config: &MuxrConfig,
16 event: ClientMouseEvent,
17 input_sender: &tokio::sync::mpsc::Sender<ClientRequest>,
18 renderer: &mut ClientRenderer,
19) -> rootcause::Result<ClientInputSend> {
20 if matches!(self::alt_left_click_phase(event), Some(AltLeftClickPhase::Release))
21 && matches!(renderer.finish_file_open_capture(), FileOpenRelease::Consumed)
22 {
23 return Ok(ClientInputSend::Accepted);
24 }
25 let Some(position) = pane_position(muxr_config, event.position) else {
26 if let Some(request) = renderer.tab_focus_request_for_sidebar_click(event) {
27 if input_sender.send(request).await.is_err() {
28 return Ok(ClientInputSend::Closed);
29 }
30 return Ok(ClientInputSend::Accepted);
31 }
32 if renderer.mouse_capture_state() == crate::renderer::MouseCaptureState::Captured
35 && let Some(position) = pane_position_for_sidebar_drag(muxr_config, event.position)
36 && let Some(event) = renderer.mouse_request_for_event(ClientMouseEvent { position, ..event })
37 {
38 return send_mouse_request(input_sender, event).await;
39 }
40 let tab_bar_position = event.position;
43 match crate::pane::focus::LocalMouseAction::from_event(event) {
44 Some(LocalMouseAction::SelectionUpdate(_)) => {
45 if let Some(position) = pane_position_for_sidebar_drag(muxr_config, tab_bar_position) {
46 let scroll_request = renderer.set_selection_outside_edge_drag(position);
47 renderer.apply_selection_input_logical(SelectionInput::Update(position))?;
48 if let Some(request) = scroll_request {
49 return Ok(crate::runtime::send_edge_scroll_request(
50 input_sender,
51 renderer,
52 request,
53 ));
54 }
55 }
56 }
57 Some(LocalMouseAction::SelectionEnd(_)) => {
58 if let Some(position) = pane_position_for_sidebar_drag(muxr_config, tab_bar_position) {
59 renderer.apply_selection_input_logical(SelectionInput::End(position))?;
60 }
61 }
62 Some(LocalMouseAction::FocusAndSelectionStart(_)) | None => {}
63 }
64 return Ok(ClientInputSend::Accepted);
65 };
66 let event = ClientMouseEvent { position, ..event };
67 if crate::pane::scroll::MouseWheelEvent::from(event) == crate::pane::scroll::MouseWheelEvent::Wheel {
68 return send_mouse_request(input_sender, event).await;
69 }
70 if matches!(self::alt_left_click_phase(event), Some(AltLeftClickPhase::Press))
71 && let Some(request) = renderer.file_open_request(position)
72 {
73 if input_sender.send(request).await.is_err() {
74 return Ok(ClientInputSend::Closed);
75 }
76 renderer.begin_file_open_capture();
77 return Ok(ClientInputSend::Accepted);
78 }
79 if let Some(event) = renderer.mouse_request_for_event(event) {
80 return send_mouse_request(input_sender, event).await;
81 }
82
83 match crate::pane::focus::LocalMouseAction::from_event(event) {
84 Some(LocalMouseAction::FocusAndSelectionStart(position)) => {
85 if input_sender.send(ClientRequest::FocusPaneAt(position)).await.is_err() {
86 return Ok(ClientInputSend::Closed);
87 }
88 renderer.apply_selection_input_logical(SelectionInput::Start(position))?;
89 Ok(ClientInputSend::Accepted)
90 }
91 Some(LocalMouseAction::SelectionUpdate(position)) => {
92 let scroll_request = renderer.set_selection_edge_drag(position, None);
93 renderer.apply_selection_input_logical(SelectionInput::Update(position))?;
94 if let Some(request) = scroll_request {
95 return Ok(crate::runtime::send_edge_scroll_request(
96 input_sender,
97 renderer,
98 request,
99 ));
100 }
101 Ok(ClientInputSend::Accepted)
102 }
103 Some(LocalMouseAction::SelectionEnd(position)) => {
104 renderer.apply_selection_input_logical(SelectionInput::End(position))?;
105 Ok(ClientInputSend::Accepted)
106 }
107 None => Ok(ClientInputSend::Accepted),
108 }
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112enum AltLeftClickPhase {
113 Press,
114 Release,
115}
116
117const fn alt_left_click_phase(event: ClientMouseEvent) -> Option<AltLeftClickPhase> {
118 if event.button != 8 {
119 return None;
120 }
121 match event.phase {
122 ClientMouseEventPhase::Press => Some(AltLeftClickPhase::Press),
123 ClientMouseEventPhase::Release => Some(AltLeftClickPhase::Release),
124 }
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum MouseEventDrop {
129 Droppable,
130 Required,
131}
132
133impl From<ClientMouseEvent> for MouseEventDrop {
134 fn from(event: ClientMouseEvent) -> Self {
135 if event.button & 32 != 0
136 && !matches!(
137 crate::pane::scroll::MouseWheelEvent::from(event),
138 crate::pane::scroll::MouseWheelEvent::Wheel
139 )
140 {
141 Self::Droppable
142 } else {
143 Self::Required
144 }
145 }
146}
147
148async fn send_mouse_request(
149 input_sender: &tokio::sync::mpsc::Sender<ClientRequest>,
150 event: ClientMouseEvent,
151) -> rootcause::Result<ClientInputSend> {
152 if MouseEventDrop::from(event) == MouseEventDrop::Droppable {
153 return Ok(
154 match crate::runtime::send_droppable_request(input_sender, ClientRequest::Mouse(event)) {
155 DroppableSendOutcome::Closed => ClientInputSend::Closed,
156 DroppableSendOutcome::Dropped | DroppableSendOutcome::Sent => ClientInputSend::Accepted,
157 },
158 );
159 }
160 if input_sender.send(ClientRequest::Mouse(event)).await.is_err() {
161 return Ok(ClientInputSend::Closed);
162 }
163 Ok(ClientInputSend::Accepted)
164}
165
166fn pane_position(config: &MuxrConfig, position: ClientMousePosition) -> Option<ClientMousePosition> {
167 Some(ClientMousePosition {
168 row: position.row,
169 col: position.col.checked_sub(config.tab_bar.width)?,
170 })
171}
172
173const fn pane_position_for_sidebar_drag(
174 config: &MuxrConfig,
175 position: ClientMousePosition,
176) -> Option<ClientMousePosition> {
177 if position.col >= config.tab_bar.width {
178 return None;
179 }
180 Some(ClientMousePosition {
181 row: position.row,
182 col: 0,
183 })
184}
185
186#[cfg(test)]
187mod tests {
188 use muxr_core::LayoutSnapshot;
189 use muxr_core::PaneId;
190 use muxr_core::PaneRegionsSnapshot;
191 use muxr_core::PaneSnapshot;
192 use muxr_core::TabId;
193 use muxr_core::TabSnapshot;
194 use muxr_core::TerminalSize;
195 use rootcause::prelude::ResultExt;
196 use rootcause::report;
197 use test_that::prelude::*;
198
199 use super::*;
200 use crate::renderer::ClientRenderer;
201 use crate::renderer::test_helpers as renderer_test_helpers;
202 use crate::terminal::SynchronizedOutput;
203
204 #[test]
205 fn test_handle_mouse_input_action_when_plain_mouse_click_arrives_focuses_pane() -> rootcause::Result<()> {
206 self::runtime()?.block_on(async {
207 let config = MuxrConfig::default();
208 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
209 let mut renderer = ClientRenderer::with_synchronized_output(
210 self::layout_snapshot()?,
211 self::pane_regions_snapshot()?,
212 SynchronizedOutput::Csi,
213 );
214 assert_that!(
215 handle_mouse_input_action(
216 &config,
217 ClientMouseEvent {
218 button: 0,
219 phase: ClientMouseEventPhase::Press,
220 position: ClientMousePosition {
221 row: 0,
222 col: config.tab_bar.width.saturating_add(1)
223 }
224 },
225 &input_sender,
226 &mut renderer,
227 )
228 .await?,
229 eq(ClientInputSend::Accepted)
230 );
231
232 assert_that!(
233 input_receiver.recv().await,
234 eq(Some(ClientRequest::FocusPaneAt(ClientMousePosition { row: 0, col: 1 })))
235 );
236 Ok(())
237 })
238 }
239
240 #[test]
241 fn test_handle_mouse_input_action_when_alt_click_hits_file_path_sends_one_open_file_request()
242 -> rootcause::Result<()> {
243 self::runtime()?.block_on(async {
244 let config = MuxrConfig::default();
245 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(2);
246 let mut renderer = ClientRenderer::with_synchronized_output(
247 self::layout_snapshot()?,
248 self::file_link_pane_regions_snapshot()?,
249 SynchronizedOutput::Csi,
250 );
251 renderer.apply_render_logical(muxr_core::RenderUpdate::Baseline(self::file_link_render_baseline()?))?;
252 assert_that!(
253 handle_mouse_input_action(
254 &config,
255 ClientMouseEvent {
256 button: 8,
257 phase: ClientMouseEventPhase::Press,
258 position: ClientMousePosition {
259 row: 0,
260 col: config.tab_bar.width.saturating_add(5),
261 },
262 },
263 &input_sender,
264 &mut renderer,
265 )
266 .await?,
267 eq(ClientInputSend::Accepted)
268 );
269
270 assert_that!(
271 input_receiver.recv().await,
272 eq(Some(ClientRequest::OpenFile {
273 pane_id: PaneId::new(1)?,
274 path: "/tmp/foo.rs".to_owned(),
275 line: Some(42),
276 column: Some(7),
277 }))
278 );
279 assert_that!(
280 input_receiver.try_recv(),
281 err(matches_pattern!(tokio::sync::mpsc::error::TryRecvError::Empty))
282 );
283
284 let mut decoder = crate::input::InputDecoder::default();
285 let decoded_release = decoder.decode(b"\x1b[<8;10;5m");
286 let [crate::input::DecodedInput::Mouse(release)] = decoded_release.as_slice() else {
287 return Err(report!("expected decoded Alt-left release"));
288 };
289 assert_that!(
290 handle_mouse_input_action(&config, *release, &input_sender, &mut renderer).await?,
291 eq(ClientInputSend::Accepted)
292 );
293 assert_that!(
294 input_receiver.try_recv(),
295 err(matches_pattern!(tokio::sync::mpsc::error::TryRecvError::Empty))
296 );
297 Ok(())
298 })
299 }
300
301 #[test]
302 fn test_alt_left_click_phase_when_extra_modifiers_are_present_ignores_event() {
303 for button in [12, 24, 28] {
304 assert_that!(
305 alt_left_click_phase(ClientMouseEvent {
306 button,
307 phase: ClientMouseEventPhase::Press,
308 position: ClientMousePosition { row: 0, col: 0 },
309 }),
310 none()
311 );
312 }
313 }
314
315 #[test]
316 fn test_handle_mouse_input_action_when_tab_sidebar_is_clicked_focuses_tab() -> rootcause::Result<()> {
317 self::runtime()?.block_on(async {
318 let config = MuxrConfig::default();
319 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
320 let mut renderer = ClientRenderer::with_synchronized_output(
321 self::two_tab_layout()?,
322 self::pane_regions_snapshot()?,
323 SynchronizedOutput::Csi,
324 );
325 assert_that!(
326 handle_mouse_input_action(
327 &config,
328 ClientMouseEvent {
329 button: 0,
330 phase: ClientMouseEventPhase::Press,
331 position: ClientMousePosition { row: 3, col: 1 }
332 },
333 &input_sender,
334 &mut renderer,
335 )
336 .await?,
337 eq(ClientInputSend::Accepted)
338 );
339
340 assert_that!(
341 input_receiver.recv().await,
342 eq(Some(ClientRequest::FocusTab(TabId::new(2)?)))
343 );
344 Ok(())
345 })
346 }
347
348 #[test]
349 fn test_handle_mouse_input_action_when_selection_release_is_on_tab_sidebar_finalizes_selection()
350 -> rootcause::Result<()> {
351 self::runtime()?.block_on(async {
352 let config = MuxrConfig::default();
353 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
354 let mut renderer = ClientRenderer::with_synchronized_output(
355 self::layout_snapshot()?,
356 self::pane_regions_snapshot()?,
357 SynchronizedOutput::Csi,
358 );
359 renderer.apply_render_logical(muxr_core::RenderUpdate::Baseline(self::render_baseline()?))?;
360 assert_that!(
361 handle_mouse_input_action(
362 &config,
363 ClientMouseEvent {
364 button: 0,
365 phase: ClientMouseEventPhase::Press,
366 position: ClientMousePosition {
367 row: 0,
368 col: config.tab_bar.width.saturating_add(1)
369 }
370 },
371 &input_sender,
372 &mut renderer,
373 )
374 .await?,
375 eq(ClientInputSend::Accepted)
376 );
377 assert_that!(
378 handle_mouse_input_action(
379 &config,
380 ClientMouseEvent {
381 button: 0,
382 phase: ClientMouseEventPhase::Release,
383 position: ClientMousePosition { row: 0, col: 1 }
384 },
385 &input_sender,
386 &mut renderer,
387 )
388 .await?,
389 eq(ClientInputSend::Accepted)
390 );
391
392 assert_that!(
393 input_receiver.recv().await,
394 eq(Some(ClientRequest::FocusPaneAt(ClientMousePosition { row: 0, col: 1 })))
395 );
396 assert_that!(
397 renderer_test_helpers::selected_text(&renderer),
398 eq(Some("ab".to_owned()))
399 );
400 Ok(())
401 })
402 }
403
404 #[test]
405 fn test_handle_mouse_input_action_when_selection_drag_moves_into_tab_sidebar_clamps_to_left_edge()
406 -> rootcause::Result<()> {
407 self::runtime()?.block_on(async {
408 let config = MuxrConfig::default();
409 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(2);
410 let mut renderer = ClientRenderer::with_synchronized_output(
411 self::layout_snapshot()?,
412 self::pane_regions_snapshot()?,
413 SynchronizedOutput::Csi,
414 );
415 renderer.apply_render_logical(muxr_core::RenderUpdate::Baseline(self::render_baseline()?))?;
416 assert_that!(
417 handle_mouse_input_action(
418 &config,
419 ClientMouseEvent {
420 button: 0,
421 phase: ClientMouseEventPhase::Press,
422 position: ClientMousePosition {
423 row: 0,
424 col: config.tab_bar.width.saturating_add(1)
425 }
426 },
427 &input_sender,
428 &mut renderer,
429 )
430 .await?,
431 eq(ClientInputSend::Accepted)
432 );
433 assert_that!(
434 handle_mouse_input_action(
435 &config,
436 ClientMouseEvent {
437 button: 32,
438 phase: ClientMouseEventPhase::Press,
439 position: ClientMousePosition { row: 0, col: 0 }
440 },
441 &input_sender,
442 &mut renderer,
443 )
444 .await?,
445 eq(ClientInputSend::Accepted)
446 );
447
448 assert_that!(
449 self::recv_client_request(&mut input_receiver).await?,
450 eq(Some(ClientRequest::FocusPaneAt(ClientMousePosition { row: 0, col: 1 })))
451 );
452 assert_that!(
453 input_receiver.try_recv(),
454 err(matches_pattern!(tokio::sync::mpsc::error::TryRecvError::Empty))
455 );
456 assert_that!(
457 renderer_test_helpers::selected_text(&renderer),
458 eq(Some("ab".to_owned()))
459 );
460 Ok(())
461 })
462 }
463
464 #[test]
465 fn test_handle_mouse_input_action_when_pane_tracks_mouse_forwards_mouse_to_server() -> rootcause::Result<()> {
466 self::runtime()?.block_on(async {
467 let config = MuxrConfig::default();
468 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
469 let mut renderer = ClientRenderer::with_synchronized_output(
470 self::layout_snapshot()?,
471 self::mouse_tracking_pane_regions_snapshot()?,
472 SynchronizedOutput::Csi,
473 );
474 assert_that!(
475 handle_mouse_input_action(
476 &config,
477 ClientMouseEvent {
478 button: 0,
479 phase: ClientMouseEventPhase::Press,
480 position: ClientMousePosition {
481 row: 0,
482 col: config.tab_bar.width.saturating_add(1)
483 }
484 },
485 &input_sender,
486 &mut renderer,
487 )
488 .await?,
489 eq(ClientInputSend::Accepted)
490 );
491
492 assert_that!(
493 input_receiver.recv().await,
494 eq(Some(ClientRequest::Mouse(ClientMouseEvent {
495 button: 0,
496 phase: ClientMouseEventPhase::Press,
497 position: ClientMousePosition { row: 0, col: 1 }
498 })))
499 );
500 Ok(())
501 })
502 }
503
504 #[test]
505 fn test_handle_mouse_input_action_when_pane_receives_wheel_forwards_mouse_to_server() -> rootcause::Result<()> {
506 self::runtime()?.block_on(async {
507 let config = MuxrConfig::default();
508 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
509 let mut renderer = ClientRenderer::with_synchronized_output(
510 self::layout_snapshot()?,
511 self::pane_regions_snapshot()?,
512 SynchronizedOutput::Csi,
513 );
514 let event = ClientMouseEvent {
515 button: 64,
516 phase: ClientMouseEventPhase::Press,
517 position: ClientMousePosition {
518 row: 0,
519 col: config.tab_bar.width.saturating_add(1),
520 },
521 };
522
523 assert_that!(
524 handle_mouse_input_action(&config, event, &input_sender, &mut renderer).await?,
525 eq(ClientInputSend::Accepted)
526 );
527
528 assert_that!(
529 input_receiver.recv().await,
530 eq(Some(ClientRequest::Mouse(ClientMouseEvent {
531 position: ClientMousePosition { row: 0, col: 1 },
532 ..event
533 })))
534 );
535 Ok(())
536 })
537 }
538
539 #[test]
540 fn test_handle_mouse_input_action_when_pane_wheel_request_queue_is_full_waits_for_queue_space()
541 -> rootcause::Result<()> {
542 self::runtime()?.block_on(async {
543 let config = MuxrConfig::default();
544 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(1);
545 assert_that!(input_sender.try_send(ClientRequest::Pong), ok(eq(())));
546 let mut renderer = ClientRenderer::with_synchronized_output(
547 self::layout_snapshot()?,
548 self::pane_regions_snapshot()?,
549 SynchronizedOutput::Csi,
550 );
551 let event = ClientMouseEvent {
552 button: 64,
553 phase: ClientMouseEventPhase::Press,
554 position: ClientMousePosition {
555 row: 0,
556 col: config.tab_bar.width.saturating_add(1),
557 },
558 };
559 let handle = handle_mouse_input_action(&config, event, &input_sender, &mut renderer);
560 tokio::pin!(handle);
561
562 tokio::select! {
563 result = &mut handle => {
564 return Err(report!("muxr wheel request did not wait for queue space").attach(format!("{result:?}")));
565 }
566 () = tokio::time::sleep(std::time::Duration::from_millis(50)) => {}
567 }
568
569 assert_that!(input_receiver.recv().await, eq(Some(ClientRequest::Pong)));
570 assert_that!(handle.await?, eq(ClientInputSend::Accepted));
571 assert_that!(input_receiver.recv().await, eq(Some(ClientRequest::Mouse(ClientMouseEvent {
572 position: ClientMousePosition { row: 0, col: 1 },
573 ..event
574 }))));
575 Ok(())
576 })
577 }
578
579 #[test]
580 fn test_handle_mouse_input_action_when_tracking_drag_crosses_pane_routes_to_pressed_pane() -> rootcause::Result<()>
581 {
582 self::runtime()?.block_on(async {
583 let config = MuxrConfig::default();
584 let (input_sender, mut input_receiver) = tokio::sync::mpsc::channel(4);
585 let mut renderer = ClientRenderer::with_synchronized_output(
586 self::layout_snapshot()?,
587 self::split_mouse_tracking_pane_regions_snapshot()?,
588 SynchronizedOutput::Csi,
589 );
590 let events = [
591 ClientMouseEvent {
592 button: 0,
593 phase: ClientMouseEventPhase::Press,
594 position: ClientMousePosition {
595 row: 0,
596 col: config.tab_bar.width.saturating_add(1),
597 },
598 },
599 ClientMouseEvent {
600 button: 32,
601 phase: ClientMouseEventPhase::Press,
602 position: ClientMousePosition {
603 row: 0,
604 col: config.tab_bar.width.saturating_add(3),
605 },
606 },
607 ClientMouseEvent {
608 button: 0,
609 phase: ClientMouseEventPhase::Release,
610 position: ClientMousePosition { row: 0, col: 1 },
611 },
612 ClientMouseEvent {
613 button: 32,
614 phase: ClientMouseEventPhase::Press,
615 position: ClientMousePosition {
616 row: 0,
617 col: config.tab_bar.width.saturating_add(3),
618 },
619 },
620 ];
621 for event in events {
622 assert_that!(
623 handle_mouse_input_action(&config, event, &input_sender, &mut renderer).await?,
624 eq(ClientInputSend::Accepted)
625 );
626 }
627
628 assert_that!(
629 self::recv_client_request(&mut input_receiver).await?,
630 eq(Some(ClientRequest::Mouse(ClientMouseEvent {
631 button: 0,
632 phase: ClientMouseEventPhase::Press,
633 position: ClientMousePosition { row: 0, col: 1 }
634 })))
635 );
636 assert_that!(
637 self::recv_client_request(&mut input_receiver).await?,
638 eq(Some(ClientRequest::Mouse(ClientMouseEvent {
639 button: 32,
640 phase: ClientMouseEventPhase::Press,
641 position: ClientMousePosition { row: 0, col: 1 }
642 })))
643 );
644 assert_that!(
645 self::recv_client_request(&mut input_receiver).await?,
646 eq(Some(ClientRequest::Mouse(ClientMouseEvent {
647 button: 0,
648 phase: ClientMouseEventPhase::Release,
649 position: ClientMousePosition { row: 0, col: 0 }
650 })))
651 );
652 assert_that!(
653 input_receiver.try_recv(),
654 err(matches_pattern!(tokio::sync::mpsc::error::TryRecvError::Empty))
655 );
656 Ok(())
657 })
658 }
659
660 async fn recv_client_request(
661 input_receiver: &mut tokio::sync::mpsc::Receiver<ClientRequest>,
662 ) -> rootcause::Result<Option<ClientRequest>> {
663 Ok(
664 tokio::time::timeout(std::time::Duration::from_secs(1), input_receiver.recv())
665 .await
666 .context("timed out waiting for muxr client request")?,
667 )
668 }
669
670 fn layout_snapshot() -> rootcause::Result<LayoutSnapshot> {
671 let active_tab = TabId::new(1)?;
672 let active_pane = PaneId::new(1)?;
673 let pane = PaneSnapshot {
674 tracked_process_state: muxr_core::TrackedProcessState::None,
675 cwd: "/tmp".to_owned(),
676 cmd_label: None,
677 focus_seq: 1,
678 id: active_pane,
679 title: "shell".to_owned(),
680 };
681 let tab = TabSnapshot::new(active_tab, "default", active_pane, vec![pane])?;
682 LayoutSnapshot::new(active_tab, vec![tab])
683 }
684
685 fn pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
686 PaneRegionsSnapshot::new(vec![muxr_core::PaneRegionSnapshot::new(
687 muxr_core::PaneId::new(1)?,
688 0,
689 0,
690 2,
691 1,
692 muxr_core::PaneMouseMode::None,
693 0,
694 )?])
695 }
696
697 fn file_link_pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
698 PaneRegionsSnapshot::new(vec![muxr_core::PaneRegionSnapshot::new(
699 muxr_core::PaneId::new(1)?,
700 0,
701 0,
702 16,
703 1,
704 muxr_core::PaneMouseMode::PressRelease,
705 0,
706 )?])
707 }
708
709 fn two_tab_layout() -> rootcause::Result<LayoutSnapshot> {
710 LayoutSnapshot::new(
711 TabId::new(1)?,
712 vec![
713 TabSnapshot::new(
714 TabId::new(1)?,
715 "default",
716 PaneId::new(1)?,
717 vec![PaneSnapshot {
718 tracked_process_state: muxr_core::TrackedProcessState::None,
719 cwd: "/tmp/tab-1".to_owned(),
720 cmd_label: None,
721 focus_seq: 1,
722 id: PaneId::new(1)?,
723 title: "shell".to_owned(),
724 }],
725 )?,
726 TabSnapshot::new(
727 TabId::new(2)?,
728 "tab 2",
729 PaneId::new(2)?,
730 vec![PaneSnapshot {
731 tracked_process_state: muxr_core::TrackedProcessState::None,
732 cwd: "/tmp/tab-2".to_owned(),
733 cmd_label: None,
734 focus_seq: 1,
735 id: PaneId::new(2)?,
736 title: "shell".to_owned(),
737 }],
738 )?,
739 ],
740 )
741 }
742
743 fn mouse_tracking_pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
744 PaneRegionsSnapshot::new(vec![muxr_core::PaneRegionSnapshot::new(
745 muxr_core::PaneId::new(1)?,
746 0,
747 0,
748 2,
749 1,
750 muxr_core::PaneMouseMode::ButtonMotion,
751 0,
752 )?])
753 }
754
755 fn split_mouse_tracking_pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
756 PaneRegionsSnapshot::new(vec![
757 muxr_core::PaneRegionSnapshot::new(
758 muxr_core::PaneId::new(1)?,
759 0,
760 0,
761 2,
762 1,
763 muxr_core::PaneMouseMode::ButtonMotion,
764 0,
765 )?,
766 muxr_core::PaneRegionSnapshot::new(
767 muxr_core::PaneId::new(2)?,
768 2,
769 0,
770 2,
771 1,
772 muxr_core::PaneMouseMode::None,
773 0,
774 )?,
775 ])
776 }
777
778 fn render_baseline() -> rootcause::Result<muxr_core::RenderBaseline> {
779 muxr_core::RenderBaseline::new(
780 1,
781 TerminalSize::new(2, 1)?,
782 muxr_core::RenderCursor {
783 row: 0,
784 col: 1,
785 shape: muxr_core::RenderCursorShape::Default,
786 visibility: muxr_core::RenderCursorVisibility::Visible,
787 },
788 vec![muxr_core::RenderRowSpan::new(
789 0,
790 0,
791 vec![self::render_cell("a"), self::render_cell("b")],
792 )?],
793 )
794 }
795
796 fn file_link_render_baseline() -> rootcause::Result<muxr_core::RenderBaseline> {
797 let text = "/tmp/foo.rs:42:7";
798 muxr_core::RenderBaseline::new(
799 1,
800 TerminalSize::new(16, 1)?,
801 muxr_core::RenderCursor {
802 row: 0,
803 col: 0,
804 shape: muxr_core::RenderCursorShape::Default,
805 visibility: muxr_core::RenderCursorVisibility::Visible,
806 },
807 vec![muxr_core::RenderRowSpan::new(
808 0,
809 0,
810 text.chars().map(|ch| render_cell(&ch.to_string())).collect(),
811 )?],
812 )
813 }
814
815 fn render_cell(text: &str) -> muxr_core::RenderCell {
816 muxr_core::RenderCell::narrow(text, muxr_core::RenderStyle::default())
817 }
818
819 fn runtime() -> rootcause::Result<tokio::runtime::Runtime> {
820 Ok(tokio::runtime::Runtime::new().context("failed to build muxr client mouse test runtime")?)
821 }
822}