1use std::io;
9use std::path::PathBuf;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::mpsc;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14
15use crossterm::event::{Event, KeyEventKind};
16use ratatui::Terminal;
17
18use crate::app::Editor;
19use crate::config::Config;
20use crate::config_io::DirectoryContext;
21use crate::server::capture_backend::{
24 terminal_setup_sequences, terminal_teardown_sequences, CaptureBackend,
25};
26use crate::server::input_parser::InputParser;
27use crate::server::ipc::{ServerConnection, ServerListener, SocketPaths, StreamWrapper};
28use crate::server::protocol::{
29 ClientControl, ServerControl, ServerHello, TermSize, VersionMismatch, PROTOCOL_VERSION,
30};
31use crate::view::color_support::ColorCapability;
32
33pub struct EditorServerConfig {
35 pub working_dir: PathBuf,
37 pub session_name: Option<String>,
39 pub idle_timeout: Option<Duration>,
41 pub editor_config: Config,
43 pub dir_context: DirectoryContext,
45 pub plugins_enabled: bool,
47 pub init_enabled: bool,
49 pub startup_authority: Option<crate::services::authority::Authority>,
56 pub workspace_trust: Arc<crate::services::workspace_trust::WorkspaceTrust>,
61 pub env_provider: Arc<crate::services::env_provider::EnvProvider>,
65 pub session_keepalive: Option<Box<dyn std::any::Any + Send>>,
72}
73
74pub struct EditorServer {
76 config: EditorServerConfig,
77 listener: ServerListener,
78 clients: Vec<ConnectedClient>,
79 editor: Option<Editor>,
80 terminal: Option<Terminal<CaptureBackend>>,
81 last_client_activity: Instant,
82 shutdown: Arc<AtomicBool>,
83 term_size: TermSize,
85 last_input_client: Option<usize>,
87 next_wait_id: u64,
89 waiting_clients: std::collections::HashMap<u64, u64>,
91 current_authority: crate::services::authority::Authority,
98 workspace_trust: Arc<crate::services::workspace_trust::WorkspaceTrust>,
104 env_provider: Arc<crate::services::env_provider::EnvProvider>,
106 #[allow(dead_code)]
112 session_keepalive: Option<Box<dyn std::any::Any + Send>>,
113}
114
115struct ClientDataWriter {
122 sender: mpsc::SyncSender<Vec<u8>>,
123 pipe_broken: Arc<AtomicBool>,
124}
125
126impl ClientDataWriter {
127 fn new(data: StreamWrapper, client_id: u64) -> Self {
129 let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(16);
131 let pipe_broken = Arc::new(AtomicBool::new(false));
132 let pipe_broken_clone = pipe_broken.clone();
133
134 std::thread::Builder::new()
135 .name(format!("client-{}-writer", client_id))
136 .spawn(move || {
137 while let Ok(buf) = rx.recv() {
138 if let Err(e) = data.write_all(&buf) {
139 tracing::debug!("Client {} writer pipe error: {}", client_id, e);
140 pipe_broken_clone.store(true, Ordering::Relaxed);
141 break;
142 }
143 if let Err(e) = data.flush() {
144 tracing::debug!("Client {} writer flush error: {}", client_id, e);
145 pipe_broken_clone.store(true, Ordering::Relaxed);
146 break;
147 }
148 }
149 tracing::debug!("Client {} writer thread exiting", client_id);
150 })
151 .expect("Failed to spawn client writer thread");
152
153 Self {
154 sender: tx,
155 pipe_broken,
156 }
157 }
158
159 fn try_write(&self, data: &[u8]) -> bool {
162 self.sender.try_send(data.to_vec()).is_ok()
163 }
164
165 fn is_broken(&self) -> bool {
167 self.pipe_broken.load(Ordering::Relaxed)
168 }
169}
170
171struct ConnectedClient {
173 conn: ServerConnection,
174 data_writer: ClientDataWriter,
176 term_size: TermSize,
177 env: std::collections::HashMap<String, Option<String>>,
178 id: u64,
179 input_parser: InputParser,
180 needs_full_render: bool,
182 wait_id: Option<u64>,
184}
185
186impl EditorServer {
187 pub fn new(mut config: EditorServerConfig) -> io::Result<Self> {
189 let socket_paths = if let Some(ref name) = config.session_name {
190 SocketPaths::for_session_name(name)?
191 } else {
192 SocketPaths::for_working_dir(&config.working_dir)?
193 };
194
195 let listener = ServerListener::bind(socket_paths)?;
196
197 let pid = std::process::id();
199 if let Err(e) = listener.paths().write_pid(pid) {
200 tracing::warn!("Failed to write PID file: {}", e);
201 }
202
203 let workspace_trust = Arc::clone(&config.workspace_trust);
209 let env_provider = Arc::clone(&config.env_provider);
210
211 let current_authority = config.startup_authority.take().unwrap_or_else(|| {
216 crate::services::authority::Authority::local(
217 Arc::clone(&workspace_trust),
218 Arc::clone(&env_provider),
219 )
220 });
221 let session_keepalive = config.session_keepalive.take();
222
223 Ok(Self {
224 config,
225 listener,
226 clients: Vec::new(),
227 editor: None,
228 terminal: None,
229 last_client_activity: Instant::now(),
230 shutdown: Arc::new(AtomicBool::new(false)),
231 term_size: TermSize::new(80, 24), last_input_client: None,
233 next_wait_id: 1,
234 waiting_clients: std::collections::HashMap::new(),
235 current_authority,
236 workspace_trust,
237 env_provider,
238 session_keepalive,
239 })
240 }
241
242 pub fn shutdown_handle(&self) -> Arc<AtomicBool> {
244 self.shutdown.clone()
245 }
246
247 pub fn socket_paths(&self) -> &SocketPaths {
249 self.listener.paths()
250 }
251
252 pub fn editor(&self) -> Option<&Editor> {
254 self.editor.as_ref()
255 }
256
257 pub fn editor_mut(&mut self) -> Option<&mut Editor> {
259 self.editor.as_mut()
260 }
261
262 pub fn run(&mut self) -> io::Result<()> {
264 tracing::info!("Editor server starting for {:?}", self.config.working_dir);
265
266 let mut next_client_id = 1u64;
267 let mut needs_render = true;
268 let mut last_render = Instant::now();
269 const FRAME_DURATION: Duration = Duration::from_millis(16); loop {
272 if self.shutdown.load(Ordering::SeqCst) {
274 tracing::info!("Shutdown requested");
275 break;
276 }
277
278 if let Some(timeout) = self.config.idle_timeout {
280 if self.clients.is_empty() && self.last_client_activity.elapsed() > timeout {
281 tracing::info!("Idle timeout reached, shutting down");
282 break;
283 }
284 }
285
286 tracing::debug!("[server] main loop: calling accept()");
288 match self.listener.accept() {
289 Ok(Some(conn)) => {
290 let cursor_style = self
292 .editor
293 .as_ref()
294 .map(|e| e.config().editor.cursor_style)
295 .unwrap_or(self.config.editor_config.editor.cursor_style);
296 match self.handle_new_connection(conn, next_client_id, cursor_style) {
297 Ok(client) => {
298 tracing::info!("Client {} connected", client.id);
299
300 if self.editor.is_none() {
302 self.term_size = client.term_size;
304 self.initialize_editor()?;
305 } else if self.clients.is_empty() {
306 if self.term_size != client.term_size {
308 self.term_size = client.term_size;
309 self.update_terminal_size()?;
310 }
311 }
312 self.clients.push(client);
315 self.last_client_activity = Instant::now();
316 next_client_id += 1;
317 needs_render = true;
318 }
319 Err(e) => {
320 tracing::warn!("Failed to complete handshake: {}", e);
321 }
322 }
323 }
324 Ok(None) => {}
325 Err(e) => {
326 tracing::error!("Accept error: {}", e);
327 }
328 }
329
330 tracing::debug!("[server] main loop: calling process_clients");
332 let (input_events, resize_occurred, input_source) = self.process_clients()?;
333 if let Some(idx) = input_source {
334 self.last_input_client = Some(idx);
335 }
336 if !input_events.is_empty() {
337 tracing::debug!(
338 "[server] process_clients returned {} events",
339 input_events.len()
340 );
341 }
342
343 if let Some(ref mut editor) = self.editor {
351 if editor.should_quit() {
352 let pending_authority = editor.take_pending_authority();
353 let pending_keepalive = editor.take_pending_keepalive();
354 let restart_dir = editor.take_restart_dir();
355 if pending_authority.is_some() || restart_dir.is_some() {
356 tracing::info!(
357 "Session rebuild requested (authority={}, dir={})",
358 pending_authority.is_some(),
359 restart_dir.is_some()
360 );
361 if let Err(e) =
362 self.rebuild_editor(restart_dir, pending_authority, pending_keepalive)
363 {
364 tracing::error!("Session rebuild failed, shutting down: {}", e);
365 self.shutdown.store(true, Ordering::SeqCst);
366 continue;
367 }
368 needs_render = true;
369 continue;
370 }
371 tracing::info!("Editor requested quit");
372 self.shutdown.store(true, Ordering::SeqCst);
373 continue;
374 }
375 }
376
377 let detach_requested = self
379 .editor
380 .as_ref()
381 .map(|e| e.should_detach())
382 .unwrap_or(false);
383 if detach_requested {
384 if let Some(idx) = self.last_input_client.take() {
386 if idx < self.clients.len() {
387 tracing::info!("Client {} requested detach", self.clients[idx].id);
388 let client = self.clients.remove(idx);
389 let teardown = terminal_teardown_sequences();
390 #[allow(clippy::let_underscore_must_use)]
392 let _ = client.data_writer.try_write(&teardown);
393 let quit_msg = serde_json::to_string(&ServerControl::Quit {
394 reason: "Detached".to_string(),
395 })
396 .unwrap_or_default();
397 #[allow(clippy::let_underscore_must_use)]
399 let _ = client.conn.write_control(&quit_msg);
400 }
401 } else {
402 tracing::info!("Detach requested but no input source, detaching all");
404 self.disconnect_all_clients("Detached")?;
405 }
406 if let Some(ref mut editor) = self.editor {
408 editor.clear_detach();
409 }
410 continue;
411 }
412
413 let suspend_requested = self
419 .editor
420 .as_mut()
421 .map(|e| e.take_suspend_request())
422 .unwrap_or(false);
423 if suspend_requested {
424 if let Some(idx) = self.last_input_client {
425 if idx < self.clients.len() {
426 let client_id = self.clients[idx].id;
427 tracing::info!("Client {} requested suspend", client_id);
428 let suspend_msg = serde_json::to_string(&ServerControl::SuspendClient)
429 .unwrap_or_default();
430 #[allow(clippy::let_underscore_must_use)]
432 let _ = self.clients[idx].conn.write_control(&suspend_msg);
433 self.clients[idx].needs_full_render = true;
437 }
438 } else {
439 tracing::warn!("Suspend requested but no input source; ignoring");
440 }
441 continue;
442 }
443
444 if resize_occurred {
446 self.update_terminal_size()?;
447 needs_render = true;
448 }
449
450 if !input_events.is_empty() {
452 self.last_client_activity = Instant::now();
453 for event in input_events {
454 if self.handle_event(event)? {
455 needs_render = true;
456 }
457 }
458 }
459
460 if let Some(ref mut editor) = self.editor {
462 if editor.process_async_messages() {
463 needs_render = true;
464 }
465 if editor.process_pending_file_opens() {
466 needs_render = true;
467 }
468
469 for wait_id in editor.take_completed_waits() {
471 if let Some(client_id) = self.waiting_clients.remove(&wait_id) {
472 if let Some(client) = self.clients.iter_mut().find(|c| c.id == client_id) {
474 let msg = serde_json::to_string(&ServerControl::WaitComplete)
475 .unwrap_or_default();
476 #[allow(clippy::let_underscore_must_use)]
477 let _ = client.conn.write_control(&msg);
478 client.wait_id = None;
479 }
480 }
481 }
482
483 if let Some(cb) = editor.take_pending_clipboard() {
485 let msg = serde_json::to_string(&ServerControl::SetClipboard {
486 text: cb.text,
487 use_osc52: cb.use_osc52,
488 use_system_clipboard: cb.use_system_clipboard,
489 })
490 .unwrap_or_default();
491 for client in &mut self.clients {
492 #[allow(clippy::let_underscore_must_use)]
493 let _ = client.conn.write_control(&msg);
494 }
495 }
496
497 if editor.check_mouse_hover_timer() {
498 needs_render = true;
499 }
500
501 if editor.active_window().animations.is_active() {
509 needs_render = true;
510 }
511 }
512
513 if needs_render && last_render.elapsed() >= FRAME_DURATION {
515 self.render_and_broadcast()?;
516 last_render = Instant::now();
517 needs_render = false;
518 }
519
520 std::thread::sleep(Duration::from_millis(5));
522 }
523
524 if let Some(ref mut editor) = self.editor {
527 if editor.config().editor.auto_save_enabled {
529 match editor.save_all_on_exit() {
530 Ok(count) if count > 0 => {
531 tracing::info!("Auto-saved {} buffer(s) on exit", count);
532 }
533 Ok(_) => {}
534 Err(e) => {
535 tracing::warn!("Failed to auto-save on exit: {}", e);
536 }
537 }
538 }
539
540 if let Err(e) = editor.end_recovery_session() {
543 tracing::warn!("Failed to end recovery session: {}", e);
544 }
545 if let Err(e) = editor.save_all_windows_workspaces() {
546 tracing::warn!("Failed to save workspaces: {}", e);
547 } else {
548 tracing::debug!("Workspaces saved successfully");
549 }
550 }
551
552 self.disconnect_all_clients("Server shutting down")?;
554
555 Ok(())
556 }
557
558 fn build_editor_instance(&mut self) -> io::Result<(Editor, Terminal<CaptureBackend>)> {
562 let backend = CaptureBackend::new(self.term_size.cols, self.term_size.rows);
563 let terminal = Terminal::new(backend)
564 .map_err(|e| io::Error::other(format!("Failed to create terminal: {}", e)))?;
565
566 let color_capability = ColorCapability::TrueColor; let mut editor = Editor::with_working_dir_opts(
573 self.config.editor_config.clone(),
574 self.term_size.cols,
575 self.term_size.rows,
576 Some(self.config.working_dir.clone()),
577 self.config.dir_context.clone(),
578 self.config.plugins_enabled,
579 color_capability,
580 std::mem::replace(
586 &mut self.current_authority,
587 crate::services::authority::Authority::local(
588 std::sync::Arc::clone(&self.workspace_trust),
589 std::sync::Arc::clone(&self.env_provider),
590 ),
591 ),
592 false,
593 )
594 .map_err(|e| io::Error::other(format!("Failed to create editor: {}", e)))?;
595
596 editor.load_init_script(self.config.init_enabled);
598
599 editor.set_session_mode(true);
601
602 let session_display_name = self.config.session_name.clone().unwrap_or_else(|| {
604 self.config
606 .working_dir
607 .file_name()
608 .and_then(|n| n.to_str())
609 .map(|s| s.to_string())
610 .unwrap_or_else(|| "session".to_string())
611 });
612 editor.set_session_name(Some(session_display_name));
613
614 Ok((editor, terminal))
615 }
616
617 pub fn initialize_editor(&mut self) -> io::Result<()> {
624 let (mut editor, terminal) = self.build_editor_instance()?;
625
626 match editor.try_restore_workspace() {
629 Ok(true) => {
630 tracing::info!("Session workspace restored successfully");
631 }
632 Ok(false) => {
633 tracing::debug!("No previous session workspace found");
634 }
635 Err(e) => {
636 tracing::warn!("Failed to restore session workspace: {}", e);
637 }
638 }
639
640 if editor.has_recovery_files().unwrap_or(false) {
642 tracing::info!("Recovery files found for session, recovering...");
643 match editor.recover_all_buffers() {
644 Ok(count) if count > 0 => {
645 tracing::info!("Recovered {} buffer(s) for session", count);
646 }
647 Ok(_) => {
648 tracing::info!("No buffers to recover for session");
649 }
650 Err(e) => {
651 tracing::warn!("Failed to recover session buffers: {}", e);
652 }
653 }
654 }
655
656 if let Err(e) = editor.start_recovery_session() {
658 tracing::warn!("Failed to start recovery session: {}", e);
659 }
660
661 self.terminal = Some(terminal);
662 self.editor = Some(editor);
663
664 self.maybe_prompt_workspace_trust();
665
666 tracing::info!(
667 "Editor initialized with size {}x{}",
668 self.term_size.cols,
669 self.term_size.rows
670 );
671
672 Ok(())
673 }
674
675 fn maybe_prompt_workspace_trust(&mut self) {
679 if let Some(editor) = self.editor.as_mut() {
680 editor.maybe_prompt_workspace_trust();
681 }
682 }
683
684 pub(crate) fn rebuild_editor(
696 &mut self,
697 new_working_dir: Option<PathBuf>,
698 new_authority: Option<crate::services::authority::Authority>,
699 new_keepalive: Option<Box<dyn std::any::Any + Send>>,
700 ) -> io::Result<()> {
701 if let Some(ref mut editor) = self.editor {
705 if editor.config().editor.auto_save_enabled {
706 if let Err(e) = editor.save_all_on_exit() {
707 tracing::warn!("Rebuild: failed to auto-save on exit: {}", e);
708 }
709 }
710 if let Err(e) = editor.end_recovery_session() {
711 tracing::warn!("Rebuild: failed to end recovery session: {}", e);
712 }
713 if let Err(e) = editor.save_all_windows_workspaces() {
714 tracing::warn!("Rebuild: failed to save workspaces: {}", e);
715 }
716 }
717
718 if new_authority.is_none() {
724 if let Some(ref mut editor) = self.editor {
725 self.current_authority = editor.take_active_authority();
726 }
727 }
728
729 self.editor = None;
732 self.terminal = None;
733
734 if let Some(dir) = new_working_dir {
736 tracing::info!("Rebuild: switching working dir to {}", dir.display());
737 self.config.working_dir = dir;
738 self.workspace_trust
742 .set_root(Some(self.config.working_dir.clone()));
743 self.workspace_trust.set_store(Some(
744 crate::services::workspace_trust::TrustStore::for_project_dir(
745 &self
746 .config
747 .dir_context
748 .project_state_dir(&self.config.working_dir),
749 ),
750 ));
751 self.env_provider.clear();
754 }
755 if let Some(auth) = new_authority {
756 tracing::info!(
757 "Rebuild: installing authority with label {:?}",
758 auth.display_label
759 );
760 self.current_authority = auth;
761 }
762 if let Some(keepalive) = new_keepalive {
768 self.session_keepalive = Some(keepalive);
769 }
770
771 let (mut editor, terminal) = self.build_editor_instance()?;
772
773 match editor.try_restore_workspace() {
777 Ok(true) => tracing::info!("Rebuild: workspace restored"),
778 Ok(false) => tracing::debug!("Rebuild: no workspace to restore"),
779 Err(e) => tracing::warn!("Rebuild: failed to restore workspace: {}", e),
780 }
781
782 if let Err(e) = editor.start_recovery_session() {
783 tracing::warn!("Rebuild: failed to start recovery session: {}", e);
784 }
785
786 self.terminal = Some(terminal);
787 self.editor = Some(editor);
788
789 self.maybe_prompt_workspace_trust();
793
794 for client in &mut self.clients {
797 client.needs_full_render = true;
798 }
799
800 tracing::info!(
801 "Rebuild: complete, {} clients kept attached",
802 self.clients.len()
803 );
804
805 Ok(())
806 }
807
808 fn handle_new_connection(
810 &self,
811 conn: ServerConnection,
812 client_id: u64,
813 cursor_style: crate::config::CursorStyle,
814 ) -> io::Result<ConnectedClient> {
815 #[cfg(not(windows))]
819 conn.control.set_nonblocking(false)?;
820 let hello_json = conn
821 .read_control()?
822 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "No hello received"))?;
823
824 let client_msg: ClientControl = serde_json::from_str(&hello_json)
825 .map_err(|e| io::Error::other(format!("Invalid hello: {}", e)))?;
826
827 let hello = match client_msg {
828 ClientControl::Hello(h) => h,
829 _ => {
830 return Err(io::Error::other("Expected Hello message"));
831 }
832 };
833
834 if hello.protocol_version != PROTOCOL_VERSION {
836 let mismatch = VersionMismatch {
837 server_version: env!("CARGO_PKG_VERSION").to_string(),
838 client_version: hello.client_version.clone(),
839 action: if hello.protocol_version > PROTOCOL_VERSION {
840 "upgrade_server".to_string()
841 } else {
842 "restart_server".to_string()
843 },
844 message: format!(
845 "Protocol version mismatch: server={}, client={}",
846 PROTOCOL_VERSION, hello.protocol_version
847 ),
848 };
849
850 let response = serde_json::to_string(&ServerControl::VersionMismatch(mismatch))
851 .map_err(|e| io::Error::other(e.to_string()))?;
852 conn.write_control(&response)?;
853
854 return Err(io::Error::other("Version mismatch"));
855 }
856
857 let session_id = self.config.session_name.clone().unwrap_or_else(|| {
859 crate::workspace::encode_path_for_filename(&self.config.working_dir)
860 });
861
862 let server_hello = ServerHello::new(session_id);
863 let response = serde_json::to_string(&ServerControl::Hello(server_hello))
864 .map_err(|e| io::Error::other(e.to_string()))?;
865 conn.write_control(&response)?;
866
867 #[cfg(not(windows))]
870 conn.control.set_nonblocking(true)?;
871
872 let mouse_hover_enabled = self.config.editor_config.editor.mouse_hover_enabled;
874 let setup = terminal_setup_sequences(mouse_hover_enabled);
875 conn.write_data(&setup)?;
876
877 conn.write_data(cursor_style.to_escape_sequence())?;
879
880 tracing::debug!(
881 "Client {} connected: {}x{}, TERM={:?}",
882 client_id,
883 hello.term_size.cols,
884 hello.term_size.rows,
885 hello.term()
886 );
887
888 let data_writer = ClientDataWriter::new(conn.data.clone(), client_id);
890
891 Ok(ConnectedClient {
892 conn,
893 data_writer,
894 term_size: hello.term_size,
895 env: hello.env,
896 id: client_id,
897 input_parser: InputParser::new(),
898 needs_full_render: true,
899 wait_id: None,
900 })
901 }
902
903 fn process_clients(&mut self) -> io::Result<(Vec<Event>, bool, Option<usize>)> {
906 let mut disconnected = Vec::new();
907 let mut input_source_client: Option<usize> = None;
908 let mut input_events = Vec::new();
909 let mut resize_occurred = false;
910 let mut control_messages: Vec<(usize, ClientControl)> = Vec::new();
911
912 for (idx, client) in self.clients.iter_mut().enumerate() {
913 let mut buf = [0u8; 4096];
915 let mut data_eof = false;
916 tracing::debug!("[server] reading from client {} data socket", client.id);
917 match client.conn.read_data(&mut buf) {
918 Ok(0) => {
919 tracing::debug!("[server] Client {} data stream closed (EOF)", client.id);
920 if client.wait_id.is_none() {
922 disconnected.push(idx);
923 }
924 data_eof = true;
925 }
927 Ok(n) => {
928 tracing::debug!(
929 "[server] Client {} read {} bytes from data socket",
930 client.id,
931 n
932 );
933 let events = client.input_parser.parse(&buf[..n]);
934 tracing::debug!(
935 "[server] Client {} parsed {} events",
936 client.id,
937 events.len()
938 );
939 if !events.is_empty() {
940 input_source_client = Some(idx);
941 }
942 input_events.extend(events);
943 }
944 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
945 }
947 Err(e) => {
948 tracing::warn!("[server] Client {} data read error: {}", client.id, e);
949 disconnected.push(idx);
950 data_eof = true;
951 }
953 }
954 let _ = data_eof; #[cfg(not(windows))]
960 #[allow(clippy::let_underscore_must_use)]
961 let _ = client.conn.control.set_nonblocking(true);
962
963 #[cfg(windows)]
965 {
966 let mut buf = [0u8; 1024];
967 match client.conn.control.try_read(&mut buf) {
968 Ok(0) => {
969 tracing::debug!("Client {} control stream closed (EOF)", client.id);
970 disconnected.push(idx);
971 }
972 Ok(n) => {
973 if let Ok(s) = std::str::from_utf8(&buf[..n]) {
975 for line in s.lines() {
976 if !line.trim().is_empty() {
977 if let Ok(msg) = serde_json::from_str::<ClientControl>(line) {
978 control_messages.push((idx, msg));
979 }
980 }
981 }
982 }
983 }
984 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
985 Err(e) => {
986 tracing::warn!("Client {} control read error: {}", client.id, e);
987 }
988 }
989 }
990
991 #[cfg(not(windows))]
992 {
993 let mut reader = std::io::BufReader::new(&client.conn.control);
994 let mut line = String::new();
995 match std::io::BufRead::read_line(&mut reader, &mut line) {
996 Ok(0) => {
997 tracing::debug!("Client {} control stream closed (EOF)", client.id);
998 disconnected.push(idx);
999 }
1000 Ok(_) if !line.trim().is_empty() => {
1001 if let Ok(msg) = serde_json::from_str::<ClientControl>(&line) {
1002 control_messages.push((idx, msg));
1003 }
1004 }
1005 Ok(_) => {}
1006 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
1007 Err(e) => {
1008 tracing::warn!("Client {} control read error: {}", client.id, e);
1009 }
1010 }
1011 }
1012 }
1013
1014 if !control_messages.is_empty() {
1016 tracing::debug!(
1017 "[server] Processing {} control messages",
1018 control_messages.len()
1019 );
1020 }
1021 for (idx, msg) in control_messages {
1022 tracing::debug!("[server] Control message from client {}: {:?}", idx, msg);
1023 if let ClientControl::Quit = msg {
1025 tracing::info!("Client requested quit, shutting down");
1026 self.shutdown.store(true, Ordering::SeqCst);
1027 continue;
1028 }
1029
1030 if let ClientControl::OpenFiles { .. } | ClientControl::OpenWindow { .. } = msg {
1033 } else if disconnected.contains(&idx) {
1035 continue;
1037 }
1038
1039 match msg {
1040 ClientControl::Hello(_) => {
1041 tracing::warn!("Unexpected Hello from client");
1042 }
1043 ClientControl::Resize { cols, rows } => {
1044 if let Some(client) = self.clients.get_mut(idx) {
1045 client.term_size = TermSize::new(cols, rows);
1046 if idx == 0 {
1048 self.term_size = TermSize::new(cols, rows);
1049 resize_occurred = true;
1050 }
1051 }
1052 }
1053 ClientControl::Ping => {
1054 if let Some(client) = self.clients.get_mut(idx) {
1055 let pong = serde_json::to_string(&ServerControl::Pong).unwrap_or_default();
1056 #[allow(clippy::let_underscore_must_use)]
1058 let _ = client.conn.write_control(&pong);
1059 }
1060 }
1061 ClientControl::Detach => {
1062 tracing::info!("Client {} detached", idx);
1063 disconnected.push(idx);
1064 }
1065 ClientControl::OpenFiles { files, wait } => {
1066 if let Some(ref mut editor) = self.editor {
1067 let wait_id = if wait {
1069 let id = self.next_wait_id;
1070 self.next_wait_id += 1;
1071 Some(id)
1072 } else {
1073 None
1074 };
1075
1076 let file_count = files.len();
1077 for (i, file_req) in files.iter().enumerate() {
1078 let path = std::path::PathBuf::from(&file_req.path);
1079 tracing::debug!(
1080 "Queuing file open: {:?} line={:?} col={:?} end_line={:?} end_col={:?} message={:?}",
1081 path,
1082 file_req.line,
1083 file_req.column,
1084 file_req.end_line,
1085 file_req.end_column,
1086 file_req.message,
1087 );
1088 let file_wait_id = if i == file_count - 1 { wait_id } else { None };
1090 editor.queue_file_open(
1091 path,
1092 file_req.line,
1093 file_req.column,
1094 file_req.end_line,
1095 file_req.end_column,
1096 file_req.message.clone(),
1097 file_wait_id,
1098 );
1099 }
1100
1101 if let Some(wait_id) = wait_id {
1103 if let Some(client) = self.clients.get_mut(idx) {
1104 self.waiting_clients.insert(wait_id, client.id);
1105 client.wait_id = Some(wait_id);
1106 }
1107 }
1108
1109 resize_occurred = true; }
1111 }
1112 ClientControl::OpenWindow { path } => {
1113 if let Some(ref mut editor) = self.editor {
1114 let path = std::path::PathBuf::from(path);
1115 if path.is_absolute() {
1116 let label = path
1117 .file_name()
1118 .map(|s| s.to_string_lossy().into_owned())
1119 .unwrap_or_else(|| path.to_string_lossy().into_owned());
1120 let id = editor.create_window_at(path, label);
1121 editor.set_active_window(id);
1122 resize_occurred = true; } else {
1124 tracing::warn!(
1125 "OpenWindow rejected: path must be absolute: {:?}",
1126 path
1127 );
1128 }
1129 }
1130 }
1131 ClientControl::Quit => unreachable!(), }
1133 }
1134
1135 for (idx, client) in self.clients.iter().enumerate() {
1137 if client.data_writer.is_broken() && !disconnected.contains(&idx) {
1138 tracing::info!("Client {} write pipe broken, disconnecting", client.id);
1139 disconnected.push(idx);
1140 }
1141 }
1142
1143 disconnected.sort_unstable();
1145 disconnected.dedup();
1146
1147 for idx in disconnected.into_iter().rev() {
1149 let client = self.clients.remove(idx);
1150 if let Some(wait_id) = client.wait_id {
1152 self.waiting_clients.remove(&wait_id);
1153 if let Some(ref mut editor) = self.editor {
1155 editor.remove_wait_tracking(wait_id);
1156 }
1157 }
1158 let teardown = terminal_teardown_sequences();
1160 let _ = client.data_writer.try_write(&teardown);
1161 tracing::info!("Client {} disconnected", client.id);
1162 if input_source_client == Some(idx) {
1164 input_source_client = None;
1165 }
1166 }
1167
1168 Ok((input_events, resize_occurred, input_source_client))
1169 }
1170
1171 fn update_terminal_size(&mut self) -> io::Result<()> {
1173 if let Some(ref mut terminal) = self.terminal {
1174 let backend = terminal.backend_mut();
1175 backend.resize(self.term_size.cols, self.term_size.rows);
1176 }
1177
1178 if let Some(ref mut editor) = self.editor {
1179 editor.resize(self.term_size.cols, self.term_size.rows);
1180 }
1181
1182 Ok(())
1183 }
1184
1185 fn handle_event(&mut self, event: Event) -> io::Result<bool> {
1187 let Some(ref mut editor) = self.editor else {
1188 return Ok(false);
1189 };
1190
1191 match event {
1192 Event::Key(key_event) => {
1193 if key_event.kind == KeyEventKind::Press {
1194 editor
1195 .handle_key(key_event.code, key_event.modifiers)
1196 .map_err(|e| io::Error::other(e.to_string()))?;
1197 Ok(true)
1198 } else {
1199 Ok(false)
1200 }
1201 }
1202 Event::Mouse(mouse_event) => editor
1203 .handle_mouse(mouse_event)
1204 .map_err(|e| io::Error::other(e.to_string())),
1205 Event::Resize(w, h) => {
1206 editor.resize(w, h);
1207 Ok(true)
1208 }
1209 Event::Paste(text) => {
1210 editor.paste_text(text);
1211 Ok(true)
1212 }
1213 _ => Ok(false),
1214 }
1215 }
1216
1217 fn render_and_broadcast(&mut self) -> io::Result<()> {
1219 let Some(ref mut editor) = self.editor else {
1220 return Ok(());
1221 };
1222
1223 let Some(ref mut terminal) = self.terminal else {
1224 return Ok(());
1225 };
1226
1227 let any_needs_full = self.clients.iter().any(|c| c.needs_full_render);
1229 if any_needs_full {
1230 tracing::info!(
1231 "Full render requested for {} client(s)",
1232 self.clients.iter().filter(|c| c.needs_full_render).count()
1233 );
1234 terminal.backend_mut().reset_style_state();
1236 #[allow(clippy::let_underscore_must_use)]
1238 let _ = terminal.clear();
1239 }
1240
1241 let pending_sequences = editor.take_pending_escape_sequences();
1243
1244 terminal
1246 .draw(|frame| editor.render(frame))
1247 .map_err(|e| io::Error::other(e.to_string()))?;
1248
1249 let output = terminal.backend_mut().take_buffer();
1251
1252 if output.is_empty() && pending_sequences.is_empty() {
1253 return Ok(());
1254 }
1255
1256 for client in &mut self.clients {
1258 if client.wait_id.is_some() {
1259 continue;
1260 }
1261 let frame = if !pending_sequences.is_empty() && !output.is_empty() {
1263 let mut combined = Vec::with_capacity(pending_sequences.len() + output.len());
1264 combined.extend_from_slice(&pending_sequences);
1265 combined.extend_from_slice(&output);
1266 combined
1267 } else if !pending_sequences.is_empty() {
1268 pending_sequences.clone()
1269 } else {
1270 output.clone()
1271 };
1272
1273 if !frame.is_empty() && !client.data_writer.try_write(&frame) {
1274 tracing::warn!("Client {} output buffer full, dropping frame", client.id);
1275 }
1276 client.needs_full_render = false;
1278 }
1279
1280 Ok(())
1281 }
1282
1283 fn disconnect_all_clients(&mut self, reason: &str) -> io::Result<()> {
1285 let teardown = terminal_teardown_sequences();
1286 for client in &mut self.clients {
1287 #[allow(clippy::let_underscore_must_use)]
1289 let _ = client.data_writer.try_write(&teardown);
1290 let quit_msg = serde_json::to_string(&ServerControl::Quit {
1291 reason: reason.to_string(),
1292 })
1293 .unwrap_or_default();
1294 #[allow(clippy::let_underscore_must_use)]
1296 let _ = client.conn.write_control(&quit_msg);
1297 }
1298 self.clients.clear();
1299 Ok(())
1300 }
1301}
1302
1303impl ConnectedClient {
1304 #[allow(dead_code)]
1306 pub fn term(&self) -> Option<&str> {
1307 self.env.get("TERM").and_then(|v| v.as_deref())
1308 }
1309
1310 #[allow(dead_code)]
1312 pub fn supports_truecolor(&self) -> bool {
1313 self.env
1314 .get("COLORTERM")
1315 .and_then(|v| v.as_deref())
1316 .map(|v| v == "truecolor" || v == "24bit")
1317 .unwrap_or(false)
1318 }
1319}