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::model::filesystem::{FileSystem, StdFileSystem};
22use crate::server::capture_backend::{
23 terminal_setup_sequences, terminal_teardown_sequences, CaptureBackend,
24};
25use crate::server::input_parser::InputParser;
26use crate::server::ipc::{ServerConnection, ServerListener, SocketPaths, StreamWrapper};
27use crate::server::protocol::{
28 ClientControl, ServerControl, ServerHello, TermSize, VersionMismatch, PROTOCOL_VERSION,
29};
30use crate::view::color_support::ColorCapability;
31
32#[derive(Debug, Clone)]
34pub 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}
48
49pub struct EditorServer {
51 config: EditorServerConfig,
52 listener: ServerListener,
53 clients: Vec<ConnectedClient>,
54 editor: Option<Editor>,
55 terminal: Option<Terminal<CaptureBackend>>,
56 last_client_activity: Instant,
57 shutdown: Arc<AtomicBool>,
58 term_size: TermSize,
60 last_input_client: Option<usize>,
62 next_wait_id: u64,
64 waiting_clients: std::collections::HashMap<u64, u64>,
66}
67
68struct ClientDataWriter {
75 sender: mpsc::SyncSender<Vec<u8>>,
76 pipe_broken: Arc<AtomicBool>,
77}
78
79impl ClientDataWriter {
80 fn new(data: StreamWrapper, client_id: u64) -> Self {
82 let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(16);
84 let pipe_broken = Arc::new(AtomicBool::new(false));
85 let pipe_broken_clone = pipe_broken.clone();
86
87 std::thread::Builder::new()
88 .name(format!("client-{}-writer", client_id))
89 .spawn(move || {
90 while let Ok(buf) = rx.recv() {
91 if let Err(e) = data.write_all(&buf) {
92 tracing::debug!("Client {} writer pipe error: {}", client_id, e);
93 pipe_broken_clone.store(true, Ordering::Relaxed);
94 break;
95 }
96 if let Err(e) = data.flush() {
97 tracing::debug!("Client {} writer flush error: {}", client_id, e);
98 pipe_broken_clone.store(true, Ordering::Relaxed);
99 break;
100 }
101 }
102 tracing::debug!("Client {} writer thread exiting", client_id);
103 })
104 .expect("Failed to spawn client writer thread");
105
106 Self {
107 sender: tx,
108 pipe_broken,
109 }
110 }
111
112 fn try_write(&self, data: &[u8]) -> bool {
115 self.sender.try_send(data.to_vec()).is_ok()
116 }
117
118 fn is_broken(&self) -> bool {
120 self.pipe_broken.load(Ordering::Relaxed)
121 }
122}
123
124struct ConnectedClient {
126 conn: ServerConnection,
127 data_writer: ClientDataWriter,
129 term_size: TermSize,
130 env: std::collections::HashMap<String, Option<String>>,
131 id: u64,
132 input_parser: InputParser,
133 needs_full_render: bool,
135 wait_id: Option<u64>,
137}
138
139impl EditorServer {
140 pub fn new(config: EditorServerConfig) -> io::Result<Self> {
142 let socket_paths = if let Some(ref name) = config.session_name {
143 SocketPaths::for_session_name(name)?
144 } else {
145 SocketPaths::for_working_dir(&config.working_dir)?
146 };
147
148 let listener = ServerListener::bind(socket_paths)?;
149
150 let pid = std::process::id();
152 if let Err(e) = listener.paths().write_pid(pid) {
153 tracing::warn!("Failed to write PID file: {}", e);
154 }
155
156 Ok(Self {
157 config,
158 listener,
159 clients: Vec::new(),
160 editor: None,
161 terminal: None,
162 last_client_activity: Instant::now(),
163 shutdown: Arc::new(AtomicBool::new(false)),
164 term_size: TermSize::new(80, 24), last_input_client: None,
166 next_wait_id: 1,
167 waiting_clients: std::collections::HashMap::new(),
168 })
169 }
170
171 pub fn shutdown_handle(&self) -> Arc<AtomicBool> {
173 self.shutdown.clone()
174 }
175
176 pub fn socket_paths(&self) -> &SocketPaths {
178 self.listener.paths()
179 }
180
181 pub fn run(&mut self) -> io::Result<()> {
183 tracing::info!("Editor server starting for {:?}", self.config.working_dir);
184
185 let mut next_client_id = 1u64;
186 let mut needs_render = true;
187 let mut last_render = Instant::now();
188 const FRAME_DURATION: Duration = Duration::from_millis(16); loop {
191 if self.shutdown.load(Ordering::SeqCst) {
193 tracing::info!("Shutdown requested");
194 break;
195 }
196
197 if let Some(timeout) = self.config.idle_timeout {
199 if self.clients.is_empty() && self.last_client_activity.elapsed() > timeout {
200 tracing::info!("Idle timeout reached, shutting down");
201 break;
202 }
203 }
204
205 tracing::debug!("[server] main loop: calling accept()");
207 match self.listener.accept() {
208 Ok(Some(conn)) => {
209 let cursor_style = self
211 .editor
212 .as_ref()
213 .map(|e| e.config().editor.cursor_style)
214 .unwrap_or(self.config.editor_config.editor.cursor_style);
215 match self.handle_new_connection(conn, next_client_id, cursor_style) {
216 Ok(client) => {
217 tracing::info!("Client {} connected", client.id);
218
219 if self.editor.is_none() {
221 self.term_size = client.term_size;
223 self.initialize_editor()?;
224 } else if self.clients.is_empty() {
225 if self.term_size != client.term_size {
227 self.term_size = client.term_size;
228 self.update_terminal_size()?;
229 }
230 }
231 self.clients.push(client);
234 self.last_client_activity = Instant::now();
235 next_client_id += 1;
236 needs_render = true;
237 }
238 Err(e) => {
239 tracing::warn!("Failed to complete handshake: {}", e);
240 }
241 }
242 }
243 Ok(None) => {}
244 Err(e) => {
245 tracing::error!("Accept error: {}", e);
246 }
247 }
248
249 tracing::debug!("[server] main loop: calling process_clients");
251 let (input_events, resize_occurred, input_source) = self.process_clients()?;
252 if let Some(idx) = input_source {
253 self.last_input_client = Some(idx);
254 }
255 if !input_events.is_empty() {
256 tracing::debug!(
257 "[server] process_clients returned {} events",
258 input_events.len()
259 );
260 }
261
262 if let Some(ref editor) = self.editor {
264 if editor.should_quit() {
265 tracing::info!("Editor requested quit");
266 self.shutdown.store(true, Ordering::SeqCst);
267 continue;
268 }
269 }
270
271 let detach_requested = self
273 .editor
274 .as_ref()
275 .map(|e| e.should_detach())
276 .unwrap_or(false);
277 if detach_requested {
278 if let Some(idx) = self.last_input_client.take() {
280 if idx < self.clients.len() {
281 tracing::info!("Client {} requested detach", self.clients[idx].id);
282 let client = self.clients.remove(idx);
283 let teardown = terminal_teardown_sequences();
284 #[allow(clippy::let_underscore_must_use)]
286 let _ = client.data_writer.try_write(&teardown);
287 let quit_msg = serde_json::to_string(&ServerControl::Quit {
288 reason: "Detached".to_string(),
289 })
290 .unwrap_or_default();
291 #[allow(clippy::let_underscore_must_use)]
293 let _ = client.conn.write_control(&quit_msg);
294 }
295 } else {
296 tracing::info!("Detach requested but no input source, detaching all");
298 self.disconnect_all_clients("Detached")?;
299 }
300 if let Some(ref mut editor) = self.editor {
302 editor.clear_detach();
303 }
304 continue;
305 }
306
307 if resize_occurred {
309 self.update_terminal_size()?;
310 needs_render = true;
311 }
312
313 if !input_events.is_empty() {
315 self.last_client_activity = Instant::now();
316 for event in input_events {
317 if self.handle_event(event)? {
318 needs_render = true;
319 }
320 }
321 }
322
323 if let Some(ref mut editor) = self.editor {
325 if editor.process_async_messages() {
326 needs_render = true;
327 }
328 if editor.process_pending_file_opens() {
329 needs_render = true;
330 }
331
332 for wait_id in editor.take_completed_waits() {
334 if let Some(client_id) = self.waiting_clients.remove(&wait_id) {
335 if let Some(client) = self.clients.iter_mut().find(|c| c.id == client_id) {
337 let msg = serde_json::to_string(&ServerControl::WaitComplete)
338 .unwrap_or_default();
339 #[allow(clippy::let_underscore_must_use)]
340 let _ = client.conn.write_control(&msg);
341 client.wait_id = None;
342 }
343 }
344 }
345
346 if editor.check_mouse_hover_timer() {
347 needs_render = true;
348 }
349 }
350
351 if needs_render && last_render.elapsed() >= FRAME_DURATION {
353 self.render_and_broadcast()?;
354 last_render = Instant::now();
355 needs_render = false;
356 }
357
358 std::thread::sleep(Duration::from_millis(5));
360 }
361
362 self.disconnect_all_clients("Server shutting down")?;
364
365 Ok(())
366 }
367
368 fn initialize_editor(&mut self) -> io::Result<()> {
370 let backend = CaptureBackend::new(self.term_size.cols, self.term_size.rows);
371 let terminal = Terminal::new(backend)
372 .map_err(|e| io::Error::other(format!("Failed to create terminal: {}", e)))?;
373
374 let filesystem: Arc<dyn FileSystem + Send + Sync> = Arc::new(StdFileSystem);
375 let color_capability = ColorCapability::TrueColor; let mut editor = Editor::with_working_dir(
378 self.config.editor_config.clone(),
379 self.term_size.cols,
380 self.term_size.rows,
381 Some(self.config.working_dir.clone()),
382 self.config.dir_context.clone(),
383 self.config.plugins_enabled,
384 color_capability,
385 filesystem,
386 )
387 .map_err(|e| io::Error::other(format!("Failed to create editor: {}", e)))?;
388
389 editor.set_session_mode(true);
391
392 let session_display_name = self.config.session_name.clone().unwrap_or_else(|| {
394 self.config
396 .working_dir
397 .file_name()
398 .and_then(|n| n.to_str())
399 .map(|s| s.to_string())
400 .unwrap_or_else(|| "session".to_string())
401 });
402 editor.set_session_name(Some(session_display_name));
403
404 self.terminal = Some(terminal);
405 self.editor = Some(editor);
406
407 tracing::info!(
408 "Editor initialized with size {}x{}",
409 self.term_size.cols,
410 self.term_size.rows
411 );
412
413 Ok(())
414 }
415
416 fn handle_new_connection(
418 &self,
419 conn: ServerConnection,
420 client_id: u64,
421 cursor_style: crate::config::CursorStyle,
422 ) -> io::Result<ConnectedClient> {
423 #[cfg(not(windows))]
427 conn.control.set_nonblocking(false)?;
428 let hello_json = conn
429 .read_control()?
430 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "No hello received"))?;
431
432 let client_msg: ClientControl = serde_json::from_str(&hello_json)
433 .map_err(|e| io::Error::other(format!("Invalid hello: {}", e)))?;
434
435 let hello = match client_msg {
436 ClientControl::Hello(h) => h,
437 _ => {
438 return Err(io::Error::other("Expected Hello message"));
439 }
440 };
441
442 if hello.protocol_version != PROTOCOL_VERSION {
444 let mismatch = VersionMismatch {
445 server_version: env!("CARGO_PKG_VERSION").to_string(),
446 client_version: hello.client_version.clone(),
447 action: if hello.protocol_version > PROTOCOL_VERSION {
448 "upgrade_server".to_string()
449 } else {
450 "restart_server".to_string()
451 },
452 message: format!(
453 "Protocol version mismatch: server={}, client={}",
454 PROTOCOL_VERSION, hello.protocol_version
455 ),
456 };
457
458 let response = serde_json::to_string(&ServerControl::VersionMismatch(mismatch))
459 .map_err(|e| io::Error::other(e.to_string()))?;
460 conn.write_control(&response)?;
461
462 return Err(io::Error::other("Version mismatch"));
463 }
464
465 let session_id = self.config.session_name.clone().unwrap_or_else(|| {
467 crate::workspace::encode_path_for_filename(&self.config.working_dir)
468 });
469
470 let server_hello = ServerHello::new(session_id);
471 let response = serde_json::to_string(&ServerControl::Hello(server_hello))
472 .map_err(|e| io::Error::other(e.to_string()))?;
473 conn.write_control(&response)?;
474
475 #[cfg(not(windows))]
478 conn.control.set_nonblocking(true)?;
479
480 let setup = terminal_setup_sequences();
482 conn.write_data(&setup)?;
483
484 conn.write_data(cursor_style.to_escape_sequence())?;
486
487 tracing::debug!(
488 "Client {} connected: {}x{}, TERM={:?}",
489 client_id,
490 hello.term_size.cols,
491 hello.term_size.rows,
492 hello.term()
493 );
494
495 let data_writer = ClientDataWriter::new(conn.data.clone(), client_id);
497
498 Ok(ConnectedClient {
499 conn,
500 data_writer,
501 term_size: hello.term_size,
502 env: hello.env,
503 id: client_id,
504 input_parser: InputParser::new(),
505 needs_full_render: true,
506 wait_id: None,
507 })
508 }
509
510 fn process_clients(&mut self) -> io::Result<(Vec<Event>, bool, Option<usize>)> {
513 let mut disconnected = Vec::new();
514 let mut input_source_client: Option<usize> = None;
515 let mut input_events = Vec::new();
516 let mut resize_occurred = false;
517 let mut control_messages: Vec<(usize, ClientControl)> = Vec::new();
518
519 for (idx, client) in self.clients.iter_mut().enumerate() {
520 let mut buf = [0u8; 4096];
522 let mut data_eof = false;
523 tracing::debug!("[server] reading from client {} data socket", client.id);
524 match client.conn.read_data(&mut buf) {
525 Ok(0) => {
526 tracing::debug!("[server] Client {} data stream closed (EOF)", client.id);
527 if client.wait_id.is_none() {
529 disconnected.push(idx);
530 }
531 data_eof = true;
532 }
534 Ok(n) => {
535 tracing::debug!(
536 "[server] Client {} read {} bytes from data socket",
537 client.id,
538 n
539 );
540 let events = client.input_parser.parse(&buf[..n]);
541 tracing::debug!(
542 "[server] Client {} parsed {} events",
543 client.id,
544 events.len()
545 );
546 if !events.is_empty() {
547 input_source_client = Some(idx);
548 }
549 input_events.extend(events);
550 }
551 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
552 let timeout_events = client.input_parser.flush_timeout();
555 if !timeout_events.is_empty() {
556 input_source_client = Some(idx);
557 input_events.extend(timeout_events);
558 }
559 }
560 Err(e) => {
561 tracing::warn!("[server] Client {} data read error: {}", client.id, e);
562 disconnected.push(idx);
563 data_eof = true;
564 }
566 }
567 let _ = data_eof; #[cfg(not(windows))]
573 #[allow(clippy::let_underscore_must_use)]
574 let _ = client.conn.control.set_nonblocking(true);
575
576 #[cfg(windows)]
578 {
579 let mut buf = [0u8; 1024];
580 match client.conn.control.try_read(&mut buf) {
581 Ok(0) => {
582 tracing::debug!("Client {} control stream closed (EOF)", client.id);
583 disconnected.push(idx);
584 }
585 Ok(n) => {
586 if let Ok(s) = std::str::from_utf8(&buf[..n]) {
588 for line in s.lines() {
589 if !line.trim().is_empty() {
590 if let Ok(msg) = serde_json::from_str::<ClientControl>(line) {
591 control_messages.push((idx, msg));
592 }
593 }
594 }
595 }
596 }
597 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
598 Err(e) => {
599 tracing::warn!("Client {} control read error: {}", client.id, e);
600 }
601 }
602 }
603
604 #[cfg(not(windows))]
605 {
606 let mut reader = std::io::BufReader::new(&client.conn.control);
607 let mut line = String::new();
608 match std::io::BufRead::read_line(&mut reader, &mut line) {
609 Ok(0) => {
610 tracing::debug!("Client {} control stream closed (EOF)", client.id);
611 disconnected.push(idx);
612 }
613 Ok(_) if !line.trim().is_empty() => {
614 if let Ok(msg) = serde_json::from_str::<ClientControl>(&line) {
615 control_messages.push((idx, msg));
616 }
617 }
618 Ok(_) => {}
619 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
620 Err(e) => {
621 tracing::warn!("Client {} control read error: {}", client.id, e);
622 }
623 }
624 }
625 }
626
627 eprintln!(
629 "[server] Processing {} control messages",
630 control_messages.len()
631 );
632 for (idx, msg) in control_messages {
633 eprintln!("[server] Control message from client {}: {:?}", idx, msg);
634 if let ClientControl::Quit = msg {
636 tracing::info!("Client requested quit, shutting down");
637 self.shutdown.store(true, Ordering::SeqCst);
638 continue;
639 }
640
641 if let ClientControl::OpenFiles { .. } = msg {
643 } else if disconnected.contains(&idx) {
645 continue;
647 }
648
649 match msg {
650 ClientControl::Hello(_) => {
651 tracing::warn!("Unexpected Hello from client");
652 }
653 ClientControl::Resize { cols, rows } => {
654 if let Some(client) = self.clients.get_mut(idx) {
655 client.term_size = TermSize::new(cols, rows);
656 if idx == 0 {
658 self.term_size = TermSize::new(cols, rows);
659 resize_occurred = true;
660 }
661 }
662 }
663 ClientControl::Ping => {
664 if let Some(client) = self.clients.get_mut(idx) {
665 let pong = serde_json::to_string(&ServerControl::Pong).unwrap_or_default();
666 #[allow(clippy::let_underscore_must_use)]
668 let _ = client.conn.write_control(&pong);
669 }
670 }
671 ClientControl::Detach => {
672 tracing::info!("Client {} detached", idx);
673 disconnected.push(idx);
674 }
675 ClientControl::OpenFiles { files, wait } => {
676 if let Some(ref mut editor) = self.editor {
677 let wait_id = if wait {
679 let id = self.next_wait_id;
680 self.next_wait_id += 1;
681 Some(id)
682 } else {
683 None
684 };
685
686 let file_count = files.len();
687 for (i, file_req) in files.iter().enumerate() {
688 let path = std::path::PathBuf::from(&file_req.path);
689 tracing::debug!(
690 "Queuing file open: {:?} line={:?} col={:?} end_line={:?} end_col={:?} message={:?}",
691 path,
692 file_req.line,
693 file_req.column,
694 file_req.end_line,
695 file_req.end_column,
696 file_req.message,
697 );
698 let file_wait_id = if i == file_count - 1 { wait_id } else { None };
700 editor.queue_file_open(
701 path,
702 file_req.line,
703 file_req.column,
704 file_req.end_line,
705 file_req.end_column,
706 file_req.message.clone(),
707 file_wait_id,
708 );
709 }
710
711 if let Some(wait_id) = wait_id {
713 if let Some(client) = self.clients.get_mut(idx) {
714 self.waiting_clients.insert(wait_id, client.id);
715 client.wait_id = Some(wait_id);
716 }
717 }
718
719 resize_occurred = true; }
721 }
722 ClientControl::Quit => unreachable!(), }
724 }
725
726 for (idx, client) in self.clients.iter().enumerate() {
728 if client.data_writer.is_broken() && !disconnected.contains(&idx) {
729 tracing::info!("Client {} write pipe broken, disconnecting", client.id);
730 disconnected.push(idx);
731 }
732 }
733
734 disconnected.sort_unstable();
736 disconnected.dedup();
737
738 for idx in disconnected.into_iter().rev() {
740 let client = self.clients.remove(idx);
741 if let Some(wait_id) = client.wait_id {
743 self.waiting_clients.remove(&wait_id);
744 if let Some(ref mut editor) = self.editor {
746 editor.remove_wait_tracking(wait_id);
747 }
748 }
749 let teardown = terminal_teardown_sequences();
751 let _ = client.data_writer.try_write(&teardown);
752 tracing::info!("Client {} disconnected", client.id);
753 if input_source_client == Some(idx) {
755 input_source_client = None;
756 }
757 }
758
759 Ok((input_events, resize_occurred, input_source_client))
760 }
761
762 fn update_terminal_size(&mut self) -> io::Result<()> {
764 if let Some(ref mut terminal) = self.terminal {
765 let backend = terminal.backend_mut();
766 backend.resize(self.term_size.cols, self.term_size.rows);
767 }
768
769 if let Some(ref mut editor) = self.editor {
770 editor.resize(self.term_size.cols, self.term_size.rows);
771 }
772
773 Ok(())
774 }
775
776 fn handle_event(&mut self, event: Event) -> io::Result<bool> {
778 let Some(ref mut editor) = self.editor else {
779 return Ok(false);
780 };
781
782 match event {
783 Event::Key(key_event) => {
784 if key_event.kind == KeyEventKind::Press {
785 editor
786 .handle_key(key_event.code, key_event.modifiers)
787 .map_err(|e| io::Error::other(e.to_string()))?;
788 Ok(true)
789 } else {
790 Ok(false)
791 }
792 }
793 Event::Mouse(mouse_event) => editor
794 .handle_mouse(mouse_event)
795 .map_err(|e| io::Error::other(e.to_string())),
796 Event::Resize(w, h) => {
797 editor.resize(w, h);
798 Ok(true)
799 }
800 Event::Paste(text) => {
801 editor.paste_text(text);
802 Ok(true)
803 }
804 _ => Ok(false),
805 }
806 }
807
808 fn render_and_broadcast(&mut self) -> io::Result<()> {
810 let Some(ref mut editor) = self.editor else {
811 return Ok(());
812 };
813
814 let Some(ref mut terminal) = self.terminal else {
815 return Ok(());
816 };
817
818 let any_needs_full = self.clients.iter().any(|c| c.needs_full_render);
820 if any_needs_full {
821 tracing::info!(
822 "Full render requested for {} client(s)",
823 self.clients.iter().filter(|c| c.needs_full_render).count()
824 );
825 terminal.backend_mut().reset_style_state();
827 #[allow(clippy::let_underscore_must_use)]
829 let _ = terminal.clear();
830 }
831
832 let pending_sequences = editor.take_pending_escape_sequences();
834
835 terminal
837 .draw(|frame| editor.render(frame))
838 .map_err(|e| io::Error::other(e.to_string()))?;
839
840 let output = terminal.backend_mut().take_buffer();
842
843 if output.is_empty() && pending_sequences.is_empty() {
844 return Ok(());
845 }
846
847 for client in &mut self.clients {
849 if client.wait_id.is_some() {
850 continue;
851 }
852 let frame = if !pending_sequences.is_empty() && !output.is_empty() {
854 let mut combined = Vec::with_capacity(pending_sequences.len() + output.len());
855 combined.extend_from_slice(&pending_sequences);
856 combined.extend_from_slice(&output);
857 combined
858 } else if !pending_sequences.is_empty() {
859 pending_sequences.clone()
860 } else {
861 output.clone()
862 };
863
864 if !frame.is_empty() && !client.data_writer.try_write(&frame) {
865 tracing::warn!("Client {} output buffer full, dropping frame", client.id);
866 }
867 client.needs_full_render = false;
869 }
870
871 Ok(())
872 }
873
874 fn disconnect_all_clients(&mut self, reason: &str) -> io::Result<()> {
876 let teardown = terminal_teardown_sequences();
877 for client in &mut self.clients {
878 #[allow(clippy::let_underscore_must_use)]
880 let _ = client.data_writer.try_write(&teardown);
881 let quit_msg = serde_json::to_string(&ServerControl::Quit {
882 reason: reason.to_string(),
883 })
884 .unwrap_or_default();
885 #[allow(clippy::let_underscore_must_use)]
887 let _ = client.conn.write_control(&quit_msg);
888 }
889 self.clients.clear();
890 Ok(())
891 }
892}
893
894impl ConnectedClient {
895 #[allow(dead_code)]
897 pub fn term(&self) -> Option<&str> {
898 self.env.get("TERM").and_then(|v| v.as_deref())
899 }
900
901 #[allow(dead_code)]
903 pub fn supports_truecolor(&self) -> bool {
904 self.env
905 .get("COLORTERM")
906 .and_then(|v| v.as_deref())
907 .map(|v| v == "truecolor" || v == "24bit")
908 .unwrap_or(false)
909 }
910}