Skip to main content

conpty_oxide/
size.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//! Pseudoconsole dimensions.
6//!
7//! [`Size`] is a validated pair of terminal dimensions. `ConPTY` represents the
8//! console size as a `COORD` whose members are `i16`, so each dimension must
9//! be between 1 and [`Size::MAX_DIMENSION`] inclusive. This module is pure Rust
10//! and has no dependency on `windows-sys`.
11
12use core::fmt;
13
14use crate::error::{Error, Result};
15
16/// Dimensions of a pseudoconsole, in character cells.
17///
18/// A `Size` is always valid: both dimensions are non-zero and at most
19/// [`Size::MAX_DIMENSION`]. Construct one with [`Size::try_new`];
20/// [`Size::default`] is 80 columns by 24 rows.
21///
22/// # Examples
23///
24/// ```
25/// use conpty_oxide::Size;
26///
27/// # fn main() -> conpty_oxide::Result<()> {
28/// let size = Size::try_new(80, 24)?;
29/// assert_eq!(size.rows(), 24);
30/// assert_eq!(size.cols(), 80);
31/// assert_eq!(size, Size::default());
32/// # Ok(())
33/// # }
34/// ```
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub struct Size {
37    rows: u16,
38    cols: u16,
39}
40
41impl Size {
42    /// Maximum value for either dimension: `i16::MAX` (32767).
43    ///
44    /// `ConPTY`'s `COORD` stores dimensions as `i16`, so anything larger cannot
45    /// be represented.
46    pub const MAX_DIMENSION: u16 = i16::MAX as u16;
47
48    /// Creates a `Size`, validating both dimensions.
49    ///
50    /// # Errors
51    ///
52    /// Returns an error with [`crate::ErrorKind::InvalidSize`] if either
53    /// dimension is `0` or greater than [`Size::MAX_DIMENSION`].
54    pub const fn try_new(cols: u16, rows: u16) -> Result<Self> {
55        if rows == 0 || cols == 0 || rows > Self::MAX_DIMENSION || cols > Self::MAX_DIMENSION {
56            return Err(Error::invalid_size(rows, cols));
57        }
58        Ok(Self { rows, cols })
59    }
60
61    /// Returns the number of rows (screen buffer height).
62    #[must_use]
63    pub const fn rows(&self) -> u16 {
64        self.rows
65    }
66
67    /// Returns the number of columns (screen buffer width).
68    #[must_use]
69    pub const fn cols(&self) -> u16 {
70        self.cols
71    }
72
73    /// Returns `(cols, rows)` as `i16`, in that order, for building a
74    /// `ConPTY` `COORD` (`COORD.X` = cols, `COORD.Y` = rows).
75    ///
76    /// The conversion cannot truncate: both dimensions are guaranteed to be
77    /// at most [`Size::MAX_DIMENSION`] (`i16::MAX`).
78    ///
79    /// Takes `self` by value because `Size` is `Copy`
80    /// (`clippy::wrong_self_convention`).
81    #[must_use]
82    #[cfg(any(feature = "blocking", feature = "tokio", test))]
83    pub(super) const fn to_i16_pair(self) -> (i16, i16) {
84        (
85            i16::from_ne_bytes(self.cols.to_ne_bytes()),
86            i16::from_ne_bytes(self.rows.to_ne_bytes()),
87        )
88    }
89}
90
91/// 24 rows by 80 columns, the traditional terminal size.
92impl Default for Size {
93    fn default() -> Self {
94        Self { rows: 24, cols: 80 }
95    }
96}
97
98/// Formats as `<cols>x<rows>` — columns first, matching the conventional
99/// terminal geometry notation (e.g. the default size displays as `80x24`).
100impl fmt::Display for Size {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "{}x{}", self.cols, self.rows)
103    }
104}
105
106/// Constructs a hard-coded valid size for crate-local tests.
107#[cfg(test)]
108pub(super) fn test_size(rows: u16, cols: u16) -> Size {
109    Size::try_new(cols, rows).expect("the hard-coded test size is valid")
110}
111
112#[cfg(test)]
113#[path = "size_tests.rs"]
114mod tests;