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;