1mod remote_pane;
2
3pub use remote_pane::RemotePane;
4
5use std::io::{self, Write, stdout};
6use std::sync::Arc;
7use std::time::Duration;
8
9use crossterm::QueueableCommand;
10use crossterm::cursor::{Hide, Show};
11use crossterm::event as crossterm_event;
12use crossterm::terminal::{
13 EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
14};
15use muxio_tokio_mpsc_adapter::ChannelCallerExt;
16use muxio_tokio_rpc_ipc_client::{RpcCallPrebuffered, RpcIpcClient, RpcServiceCallerInterface};
17use portable_pty::PtySize;
18use term_session_muxio_service_definitions::{
19 STREAM_INPUT_METHOD_ID, SUBSCRIBE_OUTPUT_METHOD_ID, Spawn,
20};
21use term_wm_core::events::{Event, KeyKind};
22use term_wm_pty_engine::Pane;
23use term_wm_pty_engine::clipboard::{Clipboard, extract_osc52_text};
24use term_wm_pty_engine::input_encoding::{key_to_bytes, mouse_event_to_bytes};
25use term_wm_pty_engine::signal::install_sigint_handler;
26use tokio::sync::mpsc;
27use vt100::{MouseProtocolEncoding, MouseProtocolMode};
28
29#[cfg(target_os = "windows")]
32const INITIAL_WAIT_ITERS: usize = 60;
33#[cfg(not(target_os = "windows"))]
34const INITIAL_WAIT_ITERS: usize = 20;
35
36struct TerminalGuard;
37
38impl Drop for TerminalGuard {
39 fn drop(&mut self) {
40 let _ = stdout().queue(crossterm::event::DisableMouseCapture);
41 let _ = stdout().queue(Show);
42 let _ = stdout().queue(LeaveAlternateScreen);
43 let _ = disable_raw_mode();
44 let _ = stdout().flush();
45 }
46}
47
48pub fn run_session(socket_path: &str) -> io::Result<()> {
54 let rt =
55 tokio::runtime::Runtime::new().map_err(|e| io::Error::other(format!("runtime: {e}")))?;
56
57 let client: Arc<RpcIpcClient> = rt
59 .block_on(RpcIpcClient::new(socket_path))
60 .map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, format!("{e:?}")))?;
61
62 let (push_tx, push_rx) = mpsc::unbounded_channel::<Vec<u8>>();
64
65 let (term_cols, term_rows) = crossterm::terminal::size()?;
67
68 rt.block_on(async {
70 Spawn::call(&*client, (None, term_cols, term_rows))
71 .await
72 .map_err(|e| io::Error::other(format!("spawn: {e:?}")))
73 })?;
74
75 let writer = rt.block_on(async {
77 let (_, mut reader) = client
80 .open_channel(SUBSCRIBE_OUTPUT_METHOD_ID, 0)
81 .await
82 .map_err(|e| io::Error::other(format!("subscribe: {e:?}")))?;
83
84 rt.spawn(async move {
88 while let Some(chunk) = reader.recv().await {
89 match chunk {
90 Ok(data) => {
91 let _ = push_tx.send(data);
92 }
93 Err(_) => break,
94 }
95 }
96 });
97
98 let (writer, _) = client
101 .open_channel(STREAM_INPUT_METHOD_ID, 0)
102 .await
103 .map_err(|e| io::Error::other(format!("stream input: {e:?}")))?;
104
105 Ok::<_, io::Error>(writer)
106 })?;
107
108 let input_writer = Box::new(move |data: &[u8]| -> io::Result<()> {
109 writer
110 .send(data.to_vec())
111 .map_err(|e| io::Error::other(e.to_string()))?;
112 Ok(())
113 });
114
115 let mut pane = RemotePane::new(
116 1u64,
117 client.clone(),
118 rt.handle().clone(),
119 term_cols,
120 term_rows,
121 push_rx,
122 input_writer,
123 );
124
125 for _ in 0..INITIAL_WAIT_ITERS {
127 pane.drain_pushes();
128 let parser = pane.shared_parser();
129 let parser = parser.lock().unwrap();
130 if !parser.screen().contents_formatted().is_empty() {
131 break;
132 }
133 drop(parser);
134 std::thread::sleep(Duration::from_millis(50));
135 }
136
137 enable_raw_mode()?;
138 let mut out = stdout();
139 out.queue(EnterAlternateScreen)?;
140 out.queue(Hide)?;
141 out.queue(crossterm::event::EnableMouseCapture)?;
142 out.flush()?;
143 let _guard = TerminalGuard;
144
145 let mut clipboard = Clipboard::new();
146 let sigint = install_sigint_handler()?;
147
148 {
150 let parser = pane.shared_parser();
151 let parser = parser.lock().unwrap();
152 let screen = parser.screen();
153 let data = screen.contents_formatted();
154 out.write_all(&data)?;
155 out.flush()?;
156 }
157
158 let mut prev_content: Option<Vec<u8>> = None;
159
160 loop {
161 let frame_start = std::time::Instant::now();
162
163 pane.drain_pushes();
165
166 let current_content = {
168 let parser = pane.shared_parser();
169 let parser = parser.lock().unwrap();
170 parser.screen().contents_formatted()
171 };
172 let has_new_data = prev_content.as_deref() != Some(¤t_content);
173
174 if has_new_data && let Some(text) = extract_osc52_text(¤t_content) {
176 let _ = clipboard.set(&text);
177 }
178
179 if !client.is_connected() {
181 return Err(io::Error::other("connection to session server lost"));
182 }
183
184 if sigint.received() {
186 sigint.ack();
187 let _ = pane.write_bytes(&[0x03]);
188 }
189
190 let had_input = if crossterm_event::poll(Duration::from_millis(4))? {
192 let crossterm_evt = crossterm_event::read()?;
193 let evt = match crossterm_evt {
195 crossterm_event::Event::Key(key) => Event::Key(term_wm_core::events::KeyEvent {
196 code: match key.code {
197 crossterm_event::KeyCode::Char(c) => term_wm_core::events::KeyCode::Char(c),
198 crossterm_event::KeyCode::Enter => term_wm_core::events::KeyCode::Enter,
199 crossterm_event::KeyCode::Tab => term_wm_core::events::KeyCode::Tab,
200 crossterm_event::KeyCode::Backspace => {
201 term_wm_core::events::KeyCode::Backspace
202 }
203 crossterm_event::KeyCode::Esc => term_wm_core::events::KeyCode::Esc,
204 crossterm_event::KeyCode::Left => term_wm_core::events::KeyCode::Left,
205 crossterm_event::KeyCode::Right => term_wm_core::events::KeyCode::Right,
206 crossterm_event::KeyCode::Up => term_wm_core::events::KeyCode::Up,
207 crossterm_event::KeyCode::Down => term_wm_core::events::KeyCode::Down,
208 crossterm_event::KeyCode::Home => term_wm_core::events::KeyCode::Home,
209 crossterm_event::KeyCode::End => term_wm_core::events::KeyCode::End,
210 crossterm_event::KeyCode::PageUp => term_wm_core::events::KeyCode::PageUp,
211 crossterm_event::KeyCode::PageDown => {
212 term_wm_core::events::KeyCode::PageDown
213 }
214 crossterm_event::KeyCode::Delete => term_wm_core::events::KeyCode::Delete,
215 crossterm_event::KeyCode::Insert => term_wm_core::events::KeyCode::Insert,
216 crossterm_event::KeyCode::F(n) => term_wm_core::events::KeyCode::F(n),
217 _ => continue,
218 },
219 modifiers: term_wm_core::events::KeyModifiers {
220 shift: key.modifiers.contains(crossterm_event::KeyModifiers::SHIFT),
221 control: key
222 .modifiers
223 .contains(crossterm_event::KeyModifiers::CONTROL),
224 alt: key.modifiers.contains(crossterm_event::KeyModifiers::ALT),
225 },
226 kind: match key.kind {
227 crossterm_event::KeyEventKind::Press => KeyKind::Press,
228 crossterm_event::KeyEventKind::Repeat => KeyKind::Repeat,
229 crossterm_event::KeyEventKind::Release => KeyKind::Release,
230 },
231 }),
232 crossterm_event::Event::Mouse(mouse) => {
233 Event::Mouse(term_wm_core::events::MouseEvent {
234 kind: match mouse.kind {
235 crossterm_event::MouseEventKind::Down(btn) => {
236 term_wm_core::events::MouseEventKind::Press(match btn {
237 crossterm_event::MouseButton::Left => {
238 term_wm_core::events::MouseButton::Left
239 }
240 crossterm_event::MouseButton::Right => {
241 term_wm_core::events::MouseButton::Right
242 }
243 crossterm_event::MouseButton::Middle => {
244 term_wm_core::events::MouseButton::Middle
245 }
246 })
247 }
248 crossterm_event::MouseEventKind::Up(btn) => {
249 term_wm_core::events::MouseEventKind::Release(match btn {
250 crossterm_event::MouseButton::Left => {
251 term_wm_core::events::MouseButton::Left
252 }
253 crossterm_event::MouseButton::Right => {
254 term_wm_core::events::MouseButton::Right
255 }
256 crossterm_event::MouseButton::Middle => {
257 term_wm_core::events::MouseButton::Middle
258 }
259 })
260 }
261 crossterm_event::MouseEventKind::Drag(btn) => {
262 term_wm_core::events::MouseEventKind::Drag(match btn {
263 crossterm_event::MouseButton::Left => {
264 term_wm_core::events::MouseButton::Left
265 }
266 crossterm_event::MouseButton::Right => {
267 term_wm_core::events::MouseButton::Right
268 }
269 crossterm_event::MouseButton::Middle => {
270 term_wm_core::events::MouseButton::Middle
271 }
272 })
273 }
274 crossterm_event::MouseEventKind::Moved => {
275 term_wm_core::events::MouseEventKind::Moved
276 }
277 crossterm_event::MouseEventKind::ScrollUp => {
278 term_wm_core::events::MouseEventKind::ScrollUp
279 }
280 crossterm_event::MouseEventKind::ScrollDown => {
281 term_wm_core::events::MouseEventKind::ScrollDown
282 }
283 crossterm_event::MouseEventKind::ScrollLeft => {
284 term_wm_core::events::MouseEventKind::ScrollLeft
285 }
286 crossterm_event::MouseEventKind::ScrollRight => {
287 term_wm_core::events::MouseEventKind::ScrollRight
288 }
289 },
290 modifiers: term_wm_core::events::KeyModifiers {
291 shift: mouse
292 .modifiers
293 .contains(crossterm_event::KeyModifiers::SHIFT),
294 control: mouse
295 .modifiers
296 .contains(crossterm_event::KeyModifiers::CONTROL),
297 alt: mouse.modifiers.contains(crossterm_event::KeyModifiers::ALT),
298 },
299 column: mouse.column,
300 row: mouse.row,
301 })
302 }
303 crossterm_event::Event::Resize(w, h) => Event::Resize(w, h),
304 crossterm_event::Event::FocusGained => Event::FocusGained,
305 crossterm_event::Event::FocusLost => Event::FocusLost,
306 crossterm_event::Event::Paste(text) => Event::Paste(text),
307 };
308 match evt {
309 Event::Key(ref key)
310 if key.kind == KeyKind::Press || key.kind == KeyKind::Repeat =>
311 {
312 let pty_key = term_wm_pty_engine::input_encoding::KeyEvent {
314 code: match key.code {
315 term_wm_core::events::KeyCode::Char(c) => {
316 term_wm_pty_engine::input_encoding::KeyCode::Char(c)
317 }
318 term_wm_core::events::KeyCode::Enter => {
319 term_wm_pty_engine::input_encoding::KeyCode::Enter
320 }
321 term_wm_core::events::KeyCode::Tab => {
322 term_wm_pty_engine::input_encoding::KeyCode::Tab
323 }
324 term_wm_core::events::KeyCode::Backspace => {
325 term_wm_pty_engine::input_encoding::KeyCode::Backspace
326 }
327 term_wm_core::events::KeyCode::Esc => {
328 term_wm_pty_engine::input_encoding::KeyCode::Esc
329 }
330 term_wm_core::events::KeyCode::Left => {
331 term_wm_pty_engine::input_encoding::KeyCode::Left
332 }
333 term_wm_core::events::KeyCode::Right => {
334 term_wm_pty_engine::input_encoding::KeyCode::Right
335 }
336 term_wm_core::events::KeyCode::Up => {
337 term_wm_pty_engine::input_encoding::KeyCode::Up
338 }
339 term_wm_core::events::KeyCode::Down => {
340 term_wm_pty_engine::input_encoding::KeyCode::Down
341 }
342 term_wm_core::events::KeyCode::Home => {
343 term_wm_pty_engine::input_encoding::KeyCode::Home
344 }
345 term_wm_core::events::KeyCode::End => {
346 term_wm_pty_engine::input_encoding::KeyCode::End
347 }
348 term_wm_core::events::KeyCode::PageUp => {
349 term_wm_pty_engine::input_encoding::KeyCode::PageUp
350 }
351 term_wm_core::events::KeyCode::PageDown => {
352 term_wm_pty_engine::input_encoding::KeyCode::PageDown
353 }
354 term_wm_core::events::KeyCode::Delete => {
355 term_wm_pty_engine::input_encoding::KeyCode::Delete
356 }
357 term_wm_core::events::KeyCode::Insert => {
358 term_wm_pty_engine::input_encoding::KeyCode::Insert
359 }
360 term_wm_core::events::KeyCode::F(n) => {
361 term_wm_pty_engine::input_encoding::KeyCode::F(n)
362 }
363 _ => continue,
364 },
365 modifiers: term_wm_pty_engine::input_encoding::KeyModifiers {
366 shift: key.modifiers.shift,
367 control: key.modifiers.control,
368 alt: key.modifiers.alt,
369 },
370 };
371 let bytes = key_to_bytes(&pty_key);
372 if !bytes.is_empty() {
373 let _ = pane.write_bytes(&bytes);
374 }
375 true
376 }
377 Event::Mouse(ref mouse) => {
378 let mouse_active = {
379 let parser = pane.shared_parser();
380 let parser = parser.lock().unwrap();
381 parser.screen().mouse_protocol_mode() != MouseProtocolMode::None
382 };
383 if mouse_active {
384 let pty_mouse = term_wm_pty_engine::input_encoding::MouseEvent {
386 kind: match mouse.kind {
387 term_wm_core::events::MouseEventKind::Press(btn) => term_wm_pty_engine::input_encoding::MouseEventKind::Press(match btn {
388 term_wm_core::events::MouseButton::Left => term_wm_pty_engine::input_encoding::MouseButton::Left,
389 term_wm_core::events::MouseButton::Right => term_wm_pty_engine::input_encoding::MouseButton::Right,
390 term_wm_core::events::MouseButton::Middle => term_wm_pty_engine::input_encoding::MouseButton::Middle,
391 }),
392 term_wm_core::events::MouseEventKind::Release(btn) => term_wm_pty_engine::input_encoding::MouseEventKind::Release(match btn {
393 term_wm_core::events::MouseButton::Left => term_wm_pty_engine::input_encoding::MouseButton::Left,
394 term_wm_core::events::MouseButton::Right => term_wm_pty_engine::input_encoding::MouseButton::Right,
395 term_wm_core::events::MouseButton::Middle => term_wm_pty_engine::input_encoding::MouseButton::Middle,
396 }),
397 term_wm_core::events::MouseEventKind::Drag(btn) => term_wm_pty_engine::input_encoding::MouseEventKind::Drag(match btn {
398 term_wm_core::events::MouseButton::Left => term_wm_pty_engine::input_encoding::MouseButton::Left,
399 term_wm_core::events::MouseButton::Right => term_wm_pty_engine::input_encoding::MouseButton::Right,
400 term_wm_core::events::MouseButton::Middle => term_wm_pty_engine::input_encoding::MouseButton::Middle,
401 }),
402 term_wm_core::events::MouseEventKind::Moved => term_wm_pty_engine::input_encoding::MouseEventKind::Moved,
403 term_wm_core::events::MouseEventKind::ScrollUp => term_wm_pty_engine::input_encoding::MouseEventKind::ScrollUp,
404 term_wm_core::events::MouseEventKind::ScrollDown => term_wm_pty_engine::input_encoding::MouseEventKind::ScrollDown,
405 term_wm_core::events::MouseEventKind::ScrollLeft => term_wm_pty_engine::input_encoding::MouseEventKind::ScrollLeft,
406 term_wm_core::events::MouseEventKind::ScrollRight => term_wm_pty_engine::input_encoding::MouseEventKind::ScrollRight,
407 },
408 modifiers: term_wm_pty_engine::input_encoding::KeyModifiers {
409 shift: mouse.modifiers.shift,
410 control: mouse.modifiers.control,
411 alt: mouse.modifiers.alt,
412 },
413 column: mouse.column,
414 row: mouse.row,
415 };
416 let bytes = mouse_event_to_bytes(&pty_mouse, MouseProtocolEncoding::Sgr);
417 if !bytes.is_empty() {
418 let _ = pane.write_bytes(&bytes);
419 }
420 }
421 mouse_active
422 }
423 Event::Resize(w, h) => {
424 let size = PtySize {
425 rows: h,
426 cols: w,
427 pixel_width: 0,
428 pixel_height: 0,
429 };
430 prev_content = None;
431 let _ = pane.resize(size);
432 true
433 }
434 _ => false,
435 }
436 } else {
437 false
438 };
439
440 if has_new_data || prev_content.is_none() {
442 let parser = pane.shared_parser();
443 let parser = parser.lock().unwrap();
444 let screen = parser.screen();
445 let (rows, cols) = screen.size();
446
447 let diff = match &prev_content {
448 Some(prev) => {
449 let mut prev_parser = vt100::Parser::new(rows, cols, 0);
450 prev_parser.process(prev);
451 screen.contents_diff(prev_parser.screen())
452 }
453 None => screen.contents_formatted(),
454 };
455
456 if !diff.is_empty() {
457 out.write_all(&diff)?;
458 out.flush()?;
459 }
460
461 prev_content = Some(screen.contents_formatted());
462 }
463
464 if pane.has_exited() {
466 return Ok(());
467 }
468
469 if !has_new_data && !had_input {
471 let elapsed = frame_start.elapsed();
472 if elapsed < Duration::from_millis(8) {
473 std::thread::sleep(Duration::from_millis(8) - elapsed);
474 }
475 }
476 }
477}