diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-15 03:58:07 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-15 04:02:14 +0300 |
| commit | 2c7c8beb079c5fd58a2e53efd2058df050ad9f55 (patch) | |
| tree | 6756f671dc728d915b1d6ebececa8cf7f70b66fb /src/app/mod.rs | |
| parent | 88370e1516182ab519d22a12fa82d5780a40fee6 (diff) | |
feat: add floating popup panes
refs #1125
Diffstat (limited to 'src/app/mod.rs')
| -rw-r--r-- | src/app/mod.rs | 187 |
1 files changed, 172 insertions, 15 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index 7391d7d3..a77f2536 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -13,6 +13,7 @@ mod config_io; mod creation; mod ids; mod input; +mod popup; mod runtime; mod runtime_mutations; mod session; @@ -659,6 +660,7 @@ impl App { pane_graphics_layers: std::collections::HashMap::new(), pane_graphics_streams: std::collections::HashMap::new(), pane_graphics_revision: 0, + popup_pane: None, plugin_command_logs: Vec::new(), next_plugin_command_log_id: 1, plugin_commands_in_flight: 0, @@ -1577,7 +1579,8 @@ impl App { let key_id = repeat_key_identity(&key); match key.kind { crossterm::event::KeyEventKind::Press => { - if self.state.mode == Mode::Terminal { + if self.state.popup_pane.is_some() || self.state.mode == Mode::Terminal + { self.suppressed_repeat_keys.remove(&key_id); self.handle_terminal_key_headless(key); } else { @@ -1586,7 +1589,8 @@ impl App { } } crossterm::event::KeyEventKind::Repeat => { - if self.state.mode == Mode::Terminal + if (self.state.popup_pane.is_some() + || self.state.mode == Mode::Terminal) && !self.suppressed_repeat_keys.contains(&key_id) { self.handle_terminal_key_headless(key); @@ -1600,7 +1604,7 @@ impl App { } } crate::raw_input::RawInputEvent::Mouse(mouse) => { - if self.state.mouse_capture { + if self.state.popup_pane.is_some() || self.state.mouse_capture { self.handle_mouse_event_headless(mouse); } else { self.state @@ -1608,7 +1612,8 @@ impl App { } } crate::raw_input::RawInputEvent::Paste(text) => { - if self.state.mode != Mode::Terminal { + if self.try_route_paste_to_popup(&text) { + } else if self.state.mode != Mode::Terminal { self.paste_into_active_text_input(&text); } else { if let Some(ws_idx) = self.state.active { @@ -1619,17 +1624,7 @@ impl App { ws_idx, focused, ) { - let _ = runtime.try_send_bytes(bytes::Bytes::from( - if runtime - .input_state() - .map(|s| s.bracketed_paste) - .unwrap_or(false) - { - format!("\x1b[200~{text}\x1b[201~") - } else { - text - }, - )); + let _ = runtime.try_send_paste(text); } } } @@ -4918,6 +4913,168 @@ last_pane = "prefix+tab" ); } + #[tokio::test] + async fn route_client_events_pastes_only_into_popup() { + let mut app = test_app(); + let mut workspace = Workspace::test_new("tiled"); + let focused = workspace.focused_pane_id().unwrap(); + let (tiled_runtime, mut tiled_rx) = TerminalRuntime::test_with_channel(80, 24); + workspace.tabs[0].runtimes.insert(focused, tiled_runtime); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + + let (popup_runtime, mut popup_rx) = TerminalRuntime::test_with_channel(40, 12); + app.install_test_popup_runtime(popup_runtime); + assert!(app + .state + .should_capture_host_mouse_from(&app.terminal_runtimes)); + + app.route_client_events( + vec![crate::raw_input::RawInputEvent::Paste("popup-only".into())], + true, + ); + + assert_eq!( + popup_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"popup-only") + ); + assert!(tiled_rx.try_recv().is_err()); + + app.route_client_events( + vec![raw_key( + KeyCode::Char('x'), + KeyModifiers::NONE, + KeyEventKind::Press, + )], + true, + ); + assert_eq!( + popup_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"x") + ); + assert!(tiled_rx.try_recv().is_err()); + + app.state.mode = Mode::Settings; + assert!( + app.handle_raw_input_event(raw_key( + KeyCode::Char('y'), + KeyModifiers::NONE, + KeyEventKind::Repeat, + )) + .await + ); + assert_eq!( + popup_rx.try_recv().unwrap(), + bytes::Bytes::from_static(b"y") + ); + assert!(tiled_rx.try_recv().is_err()); + } + + #[tokio::test] + async fn route_client_events_discards_paste_when_popup_runtime_is_missing() { + let mut app = test_app(); + let mut workspace = Workspace::test_new("tiled"); + let focused = workspace.focused_pane_id().unwrap(); + let (tiled_runtime, mut tiled_rx) = TerminalRuntime::test_with_channel(80, 24); + workspace.tabs[0].runtimes.insert(focused, tiled_runtime); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + let install_missing_popup = |app: &mut App| { + let popup_terminal_id = crate::terminal::TerminalId::alloc(); + app.state.terminals.insert( + popup_terminal_id.clone(), + crate::terminal::TerminalState::new( + popup_terminal_id.clone(), + std::path::PathBuf::from("/popup"), + ), + ); + app.state.popup_pane = Some(state::PopupPaneState { + pane_id: crate::layout::PaneId::alloc(), + terminal_id: popup_terminal_id, + width: None, + height: None, + }); + }; + install_missing_popup(&mut app); + + app.route_client_events( + vec![crate::raw_input::RawInputEvent::Paste("discard-me".into())], + true, + ); + + assert!(tiled_rx.try_recv().is_err()); + assert!(app.state.popup_pane.is_none()); + + install_missing_popup(&mut app); + assert!( + app.handle_raw_input_event(crate::raw_input::RawInputEvent::Paste( + "discard-monolithic".into(), + )) + .await + ); + assert!(tiled_rx.try_recv().is_err()); + assert!(app.state.popup_pane.is_none()); + } + + #[tokio::test] + async fn route_client_events_routes_popup_mouse_when_global_capture_is_disabled() { + let mut app = test_app(); + let mut workspace = Workspace::test_new("tiled"); + let focused = workspace.focused_pane_id().unwrap(); + let (tiled_runtime, mut tiled_rx) = TerminalRuntime::test_with_channel(80, 24); + tiled_runtime.test_process_pty_bytes(b"\x1b[?1000h\x1b[?1006h"); + workspace.tabs[0].runtimes.insert(focused, tiled_runtime); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.mouse_capture = false; + app.state.view.terminal_area = ratatui::layout::Rect::new(0, 0, 80, 24); + + let (popup_runtime, mut popup_rx) = TerminalRuntime::test_with_channel(40, 12); + popup_runtime.test_process_pty_bytes(b"\x1b[?1000h\x1b[?1006h"); + app.install_test_popup_runtime(popup_runtime); + let (_, inner) = + crate::ui::popup_pane_rects(&app.state, app.state.view.terminal_area).unwrap(); + + app.route_client_events( + vec![crate::raw_input::RawInputEvent::Mouse( + crossterm::event::MouseEvent { + kind: crossterm::event::MouseEventKind::Down( + crossterm::event::MouseButton::Left, + ), + column: inner.x, + row: inner.y, + modifiers: crossterm::event::KeyModifiers::NONE, + }, + )], + true, + ); + + assert!(popup_rx.try_recv().is_ok()); + assert!(tiled_rx.try_recv().is_err()); + + assert!( + app.handle_raw_input_event(crate::raw_input::RawInputEvent::Mouse( + crossterm::event::MouseEvent { + kind: crossterm::event::MouseEventKind::Down( + crossterm::event::MouseButton::Left, + ), + column: inner.x + 1, + row: inner.y, + modifiers: crossterm::event::KeyModifiers::NONE, + }, + )) + .await + ); + assert!(popup_rx.try_recv().is_ok()); + assert!(tiled_rx.try_recv().is_err()); + } + #[test] fn route_client_input_closes_release_notes_modal() { let mut app = test_app(); |
