//! Platform-specific process and filesystem operations. //! //! Centralizes OS-dependent behavior behind a clean boundary so core //! modules don't scatter `#[cfg]` branches through product logic. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ForegroundProcess { pub pid: u32, pub name: String, pub argv0: Option, pub argv: Option>, pub cmdline: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ForegroundJob { pub process_group_id: u32, pub processes: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Signal { Hangup, Terminate, Kill, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct PlatformCapabilities { pub(crate) live_handoff: bool, pub(crate) remote_attach: bool, pub(crate) direct_terminal_attach: bool, } pub(crate) const fn capabilities() -> PlatformCapabilities { PlatformCapabilities { live_handoff: cfg!(unix), remote_attach: cfg!(unix), direct_terminal_attach: cfg!(unix), } } #[cfg(unix)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClipboardCommand { pub program: &'static str, pub args: &'static [&'static str], } #[derive(Debug, Clone, PartialEq, Eq)] // Windows does not wire clipboard-image bridging into semantic input yet. #[cfg_attr(windows, allow(dead_code))] pub struct ClipboardImage { pub bytes: Vec, pub extension: &'static str, } #[cfg(unix)] #[derive(Debug, PartialEq, Eq)] pub(crate) enum LimitedRead { Empty, Complete(Vec), Oversized, } #[cfg(unix)] pub(crate) fn read_limited_reader( mut reader: impl std::io::Read, max_bytes: usize, ) -> std::io::Result { let mut bytes = Vec::new(); let mut buffer = [0_u8; 8192]; while bytes.len() < max_bytes { let remaining = max_bytes - bytes.len(); let read_len = remaining.min(buffer.len()); let bytes_read = match reader.read(&mut buffer[..read_len]) { Ok(bytes_read) => bytes_read, Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue, Err(err) => return Err(err), }; if bytes_read == 0 { return if bytes.is_empty() { Ok(LimitedRead::Empty) } else { Ok(LimitedRead::Complete(bytes)) }; } bytes.extend_from_slice(&buffer[..bytes_read]); } let mut sentinel = [0_u8; 1]; loop { return match reader.read(&mut sentinel) { Ok(0) if bytes.is_empty() => Ok(LimitedRead::Empty), Ok(0) => Ok(LimitedRead::Complete(bytes)), Ok(_) => Ok(LimitedRead::Oversized), Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue, Err(err) => Err(err), }; } } #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] pub use linux::*; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] pub use macos::*; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] pub use windows::*; #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] mod fallback; #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] pub use fallback::*; #[cfg(not(target_os = "linux"))] pub fn process_agent_hint(_pid: u32) -> Option { None } #[cfg(not(target_os = "macos"))] #[derive(Debug)] pub(crate) struct InputSourceRestore; #[cfg(not(target_os = "macos"))] pub(crate) fn switch_to_ascii_input_source() -> Option { None } /// Switches the host keyboard input source while prefix mode is active. /// /// `App` drives this through a trait so the prefix-mode transitions can be /// tested with a fake, without touching the real macOS APIs or leaking a /// platform-specific restore type into `App`. pub(crate) trait PrefixInputSource { /// Switch to an ASCII-capable input source for prefix commands. No-op if /// the current source is already ASCII-capable, the platform is /// unsupported, or the switch fails. Calling it again before `restore` /// keeps the source saved by the first call. fn switch_to_ascii(&mut self); /// Restore whatever `switch_to_ascii` saved. No-op if nothing was switched. fn restore(&mut self); } /// Production [`PrefixInputSource`] backed by the per-platform API. #[derive(Default)] pub(crate) struct RealPrefixInputSource { restore: Option, } impl PrefixInputSource for RealPrefixInputSource { fn switch_to_ascii(&mut self) { if self.restore.is_none() { self.restore = switch_to_ascii_input_source(); } } fn restore(&mut self) { let _ = self.restore.take(); } } #[cfg(all(test, unix))] mod tests { use super::*; #[test] fn read_limited_reader_returns_complete_data_under_limit() { let input = std::io::Cursor::new(b"image".to_vec()); assert_eq!( read_limited_reader(input, 16).expect("limited read"), LimitedRead::Complete(b"image".to_vec()) ); } #[test] fn read_limited_reader_returns_empty_for_empty_input() { let input = std::io::Cursor::new(Vec::::new()); assert_eq!( read_limited_reader(input, 16).expect("limited read"), LimitedRead::Empty ); } #[test] fn read_limited_reader_accepts_data_exactly_at_limit() { let input = std::io::Cursor::new(b"four".to_vec()); assert_eq!( read_limited_reader(input, 4).expect("limited read"), LimitedRead::Complete(b"four".to_vec()) ); } #[test] fn read_limited_reader_rejects_data_over_limit() { let input = std::io::Cursor::new(b"oversized".to_vec()); assert_eq!( read_limited_reader(input, 4).expect("limited read"), LimitedRead::Oversized ); } #[test] fn read_limited_reader_retries_interrupted_reads() { struct InterruptedOnce { interrupted: bool, inner: std::io::Cursor>, } impl std::io::Read for InterruptedOnce { fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { if !self.interrupted { self.interrupted = true; return Err(std::io::ErrorKind::Interrupted.into()); } self.inner.read(buffer) } } let input = InterruptedOnce { interrupted: false, inner: std::io::Cursor::new(b"image".to_vec()), }; assert_eq!( read_limited_reader(input, 16).expect("limited read"), LimitedRead::Complete(b"image".to_vec()) ); } }