Skip to main content

conpty_oxide/
api.rs

1// SPDX-FileCopyrightText: 2026 conpty-oxide contributors <https://github.com/P4suta/conpty-oxide/graphs/contributors>
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Public types shared by the blocking and Tokio front ends.
6
7use std::fmt;
8#[cfg(any(feature = "blocking", feature = "tokio"))]
9use std::sync::Arc;
10
11use crate::backend::ConPtyBackend;
12#[cfg(any(feature = "blocking", feature = "tokio"))]
13use crate::core::session::Session as SessionCore;
14use crate::error::Result;
15use crate::size::Size;
16use crate::status::ExitStatus;
17
18/// Safe configuration for a managed pseudoconsole session.
19///
20/// Managed sessions deliberately expose only the initial terminal size and
21/// backend choice. Cursor inheritance, manual EOF policy, and detached
22/// spawning are outside the 0.1 API.
23///
24/// # Examples
25///
26/// ```no_run
27/// use conpty_oxide::{SessionOptions, Size};
28///
29/// # fn main() -> conpty_oxide::Result<()> {
30/// let options = SessionOptions::new().size(Size::try_new(120, 40)?);
31/// # let _ = options;
32/// # Ok(())
33/// # }
34/// ```
35#[derive(Debug, Clone, Default)]
36pub struct SessionOptions {
37    size: Size,
38    backend: Option<ConPtyBackend>,
39}
40
41impl SessionOptions {
42    /// Creates options with an 80x24 terminal and automatic backend selection.
43    #[must_use]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Sets the initial terminal size.
49    #[must_use]
50    pub const fn size(mut self, size: Size) -> Self {
51        self.size = size;
52        self
53    }
54
55    /// Selects a `ConPTY` backend for this session.
56    #[must_use]
57    pub fn backend(mut self, backend: ConPtyBackend) -> Self {
58        self.backend = Some(backend);
59        self
60    }
61
62    #[cfg(any(feature = "blocking", feature = "tokio"))]
63    #[must_use]
64    pub(super) fn into_parts(self) -> (Size, Option<ConPtyBackend>) {
65        (self.size, self.backend)
66    }
67}
68
69/// Virtual-terminal output collected from a managed session.
70///
71/// `ConPTY` exposes one rendered VT byte stream rather than distinct stdout and
72/// stderr channels, so this type intentionally does not pretend otherwise.
73///
74/// The byte buffer may be large, so collecting output does not also make it
75/// implicitly cloneable; a hidden compile-fail doctest pins the missing
76/// `Clone`.
77pub struct SessionOutput {
78    status: ExitStatus,
79    bytes: Vec<u8>,
80}
81
82impl SessionOutput {
83    #[cfg(any(feature = "blocking", feature = "tokio"))]
84    #[must_use]
85    pub(super) const fn new(status: ExitStatus, bytes: Vec<u8>) -> Self {
86        Self { status, bytes }
87    }
88
89    /// Returns the root process's exit status.
90    #[must_use]
91    pub const fn status(&self) -> ExitStatus {
92        self.status
93    }
94
95    /// Borrows the rendered UTF-8/VT byte stream.
96    #[must_use]
97    pub fn as_bytes(&self) -> &[u8] {
98        &self.bytes
99    }
100
101    /// Consumes the result and returns the rendered UTF-8/VT byte stream.
102    #[must_use]
103    pub fn into_bytes(self) -> Vec<u8> {
104        self.bytes
105    }
106}
107
108impl fmt::Debug for SessionOutput {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.debug_struct("SessionOutput")
111            .field("status", &self.status)
112            .field("bytes", &format_args!("{} bytes", self.bytes.len()))
113            .finish()
114    }
115}
116
117/// Cloneable control handle shared by both public front ends.
118///
119/// It contains no pipe or runtime-specific state. Clones may be used from any
120/// thread, and the pseudoconsole remains alive while the controller or either
121/// owned I/O half still exists.
122#[derive(Clone)]
123pub struct PtyController {
124    #[cfg(any(feature = "blocking", feature = "tokio"))]
125    pub(super) inner: Arc<SessionCore>,
126    #[cfg(not(any(feature = "blocking", feature = "tokio")))]
127    uninhabited: std::convert::Infallible,
128}
129
130impl PtyController {
131    #[cfg(any(feature = "blocking", feature = "tokio"))]
132    pub(super) const fn new(inner: Arc<SessionCore>) -> Self {
133        Self { inner }
134    }
135}
136
137#[cfg(any(feature = "blocking", feature = "tokio"))]
138impl PtyController {
139    /// Resizes the pseudoconsole.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error with [`crate::ErrorKind::Resize`] when the session is
144    /// closed or the backend rejects the requested size.
145    pub fn resize(&self, size: Size) -> Result<()> {
146        self.inner.resize(size)
147    }
148
149    /// Returns the last successfully applied terminal size.
150    #[must_use]
151    pub fn size(&self) -> Size {
152        self.inner.size()
153    }
154
155    /// Clears the pseudoconsole screen and scrollback.
156    ///
157    /// # Errors
158    ///
159    /// Returns an error with [`crate::ErrorKind::UnsupportedFeature`] when
160    /// clearing is unavailable, or [`crate::ErrorKind::Clear`] when the
161    /// operation fails.
162    pub fn clear(&self) -> Result<()> {
163        self.inner.clear()
164    }
165
166    /// Returns whether this backend provides `ClearPseudoConsole`.
167    #[must_use]
168    pub fn supports_clear(&self) -> bool {
169        self.inner.supports_clear()
170    }
171
172    #[must_use]
173    #[cfg(test)]
174    pub(crate) fn supports_release(&self) -> bool {
175        self.inner.supports_release()
176    }
177
178    #[must_use]
179    #[cfg(test)]
180    pub(crate) fn reader_finished(&self) -> bool {
181        self.inner.reader_finished()
182    }
183
184    #[must_use]
185    #[cfg(test)]
186    pub(crate) fn backend_kind(&self) -> &crate::backend::BackendKind {
187        self.inner.backend_kind()
188    }
189}
190
191#[cfg(any(feature = "blocking", feature = "tokio"))]
192impl fmt::Debug for PtyController {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        f.debug_struct("PtyController")
195            .field("size", &self.inner.size())
196            .field("supports_clear", &self.inner.supports_clear())
197            .finish_non_exhaustive()
198    }
199}
200
201/// [`SessionOutput`] never becomes implicitly cloneable:
202///
203/// ```compile_fail
204/// use conpty_oxide::SessionOutput;
205///
206/// fn require_clone<T: Clone>() {}
207/// require_clone::<SessionOutput>();
208/// ```
209#[cfg(doctest)]
210mod api_boundary {}