1use std::io;
14#[cfg(unix)]
15use std::sync::atomic::AtomicBool;
16#[cfg(unix)]
17use std::sync::Arc;
18
19use crate::server::ipc::{ClientConnection, SocketPaths};
20use crate::server::protocol::{
21 ClientControl, ClientHello, ServerControl, TermSize, PROTOCOL_VERSION,
22};
23
24#[cfg(unix)]
25mod relay_unix;
26
27pub struct ClientConfig {
29 pub socket_paths: SocketPaths,
31 pub term_size: TermSize,
33}
34
35#[derive(Debug)]
37pub enum ClientExitReason {
38 ServerQuit,
40 Detached,
42 VersionMismatch { server_version: String },
44 Error(io::Error),
46}
47
48pub fn run_client(config: ClientConfig) -> io::Result<ClientExitReason> {
58 let conn = ClientConnection::connect(&config.socket_paths)?;
59 run_client_with_connection(config, conn)
60}
61
62pub fn run_client_with_connection(
67 config: ClientConfig,
68 conn: ClientConnection,
69) -> io::Result<ClientExitReason> {
70 let hello = ClientHello::new(config.term_size);
72 let hello_json = serde_json::to_string(&ClientControl::Hello(hello))
73 .map_err(|e| io::Error::other(e.to_string()))?;
74 conn.write_control(&hello_json)?;
75
76 let response = conn
78 .read_control()?
79 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "Server closed connection"))?;
80
81 let server_msg: ServerControl =
82 serde_json::from_str(&response).map_err(|e| io::Error::other(e.to_string()))?;
83
84 match server_msg {
85 ServerControl::Hello(server_hello) => {
86 if server_hello.protocol_version != PROTOCOL_VERSION {
87 return Ok(ClientExitReason::VersionMismatch {
88 server_version: server_hello.server_version,
89 });
90 }
91 tracing::info!(
92 "Connected to session '{}' (server {})",
93 server_hello.session_id,
94 server_hello.server_version
95 );
96 }
97 ServerControl::VersionMismatch(mismatch) => {
98 return Ok(ClientExitReason::VersionMismatch {
99 server_version: mismatch.server_version,
100 });
101 }
102 ServerControl::Error { message } => {
103 return Err(io::Error::other(format!("Server error: {}", message)));
104 }
105 _ => {
106 return Err(io::Error::other("Unexpected server response"));
107 }
108 }
109
110 run_client_relay(conn)
111}
112
113pub fn run_client_relay(
118 #[allow(unused_mut)] mut conn: ClientConnection,
119) -> io::Result<ClientExitReason> {
120 #[cfg(not(windows))]
124 conn.set_data_nonblocking(true)?;
125
126 #[cfg(unix)]
128 {
129 let resize_flag = Arc::new(AtomicBool::new(false));
130 relay_unix::setup_resize_handler(resize_flag.clone())?;
131 relay_unix::relay_loop(&mut conn, resize_flag)
132 }
133
134 #[cfg(windows)]
135 {
136 let result = fresh_winterm::relay_loop(&mut conn)?;
137 return Ok(match result {
138 fresh_winterm::RelayExitReason::ServerQuit => ClientExitReason::ServerQuit,
139 fresh_winterm::RelayExitReason::Detached => ClientExitReason::Detached,
140 });
141 }
142}
143
144fn set_client_clipboard(text: &str, use_osc52: bool, use_system_clipboard: bool) {
149 crate::services::clipboard::copy_to_system_clipboard(text, use_osc52, use_system_clipboard);
150}
151
152pub fn get_terminal_size() -> io::Result<TermSize> {
154 #[cfg(unix)]
155 {
156 let mut size: libc::winsize = unsafe { std::mem::zeroed() };
157 let result = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut size) };
158 if result == -1 {
159 return Err(io::Error::last_os_error());
160 }
161 Ok(TermSize::new(size.ws_col, size.ws_row))
162 }
163
164 #[cfg(windows)]
165 {
166 let size = fresh_winterm::get_terminal_size()?;
167 Ok(TermSize::new(size.cols, size.rows))
168 }
169}
170
171#[cfg(windows)]
174impl fresh_winterm::RelayConnection for ClientConnection {
175 fn try_read_data(&mut self, buf: &mut [u8]) -> io::Result<usize> {
176 self.read_data(buf)
177 }
178
179 fn try_read_control_byte(&mut self, buf: &mut [u8; 1]) -> io::Result<usize> {
180 self.control.try_read(buf)
181 }
182
183 fn write_data(&mut self, buf: &[u8]) -> io::Result<()> {
184 ClientConnection::write_data(self, buf)
185 }
186
187 fn send_resize(&mut self, cols: u16, rows: u16) -> io::Result<()> {
188 let msg = serde_json::to_string(&ClientControl::Resize { cols, rows }).unwrap();
189 ClientConnection::write_control(self, &msg)
190 }
191
192 fn handle_server_control(&mut self, msg: &str) -> Option<fresh_winterm::RelayExitReason> {
193 if let Ok(ctrl) = serde_json::from_str::<ServerControl>(msg) {
194 match ctrl {
195 ServerControl::Quit { .. } => Some(fresh_winterm::RelayExitReason::ServerQuit),
196 ServerControl::SetClipboard {
197 text,
198 use_osc52,
199 use_system_clipboard,
200 } => {
201 set_client_clipboard(&text, use_osc52, use_system_clipboard);
202 None
203 }
204 _ => None,
205 }
206 } else {
207 None
208 }
209 }
210}