win_term/
lib.rs

1//! Small Windows-only helper to estimate console pixel sizes.
2//!
3//! Why this exists
4//! ----------------
5//! `GetCurrentConsoleFontEx` and other font-metric APIs are unreliable
6//! on many Windows terminal hosts and emulators (they may return unusable
7//! values or not reflect user DPI/zoom). This crate provides a "good
8//! enough" estimation by using conservative defaults and the current
9//! DPI to compute per-character pixel sizes, then converting console
10//! character dimensions into pixels.
11//!
12//! Short summary
13//! - `get_size_of_the_font()` returns an estimated font cell size in pixels.
14//! - `get_size_of_the_terminal()` returns a pixel-size estimate for the
15//!   console by multiplying the estimated font size by console character
16//!   dimensions.
17//!
18//! Limitations: values are approximations and may be incorrect for
19//! custom fonts, per-window zoom, or terminal emulators that do not
20//! expose accurate metrics.
21
22use windows_sys::Win32::{
23    Foundation::HANDLE,
24    System::Console::{
25        GetConsoleScreenBufferInfo, GetConsoleWindow, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO,
26        COORD, SMALL_RECT, STD_OUTPUT_HANDLE,
27    },
28    UI::HiDpi::GetDpiForWindow,
29};
30#[cfg(target_os = "windows")]
31/// Default font width for Windows console on 96 DPI (approx. Consolas 12pt)
32pub static mut DEFAULT_FONT_SIZE_WIDTH: u16 = 10;
33#[cfg(target_os = "windows")]
34/// Default font height for Windows console on 96 DPI (approx. Consolas 12pt)
35pub static mut DEFAULT_FONT_SIZE_HEIGHT: u16 = 22;
36
37/// Struct to hold terminal size information in terms of width and height.
38#[derive(Debug)]
39pub struct TerminalSize {
40    /// Width of the terminal in pixels
41    pub width: i32,
42    /// Height of the terminal in pixels
43    pub height: i32,
44}
45
46/// Struct to hold font size information in terms of width and height.
47#[derive(Debug)]
48pub struct FontSize {
49    /// Width of a single character in pixels
50    pub width: i32,
51    /// Height of a single character in pixels
52    pub height: i32,
53}
54
55/// Enum to represent possible errors that can occur while getting terminal or font size.
56#[derive(Debug)]
57pub enum TerminalError {
58    /// Standard output handle not found
59    NoStdHandle,
60    /// Failed to retrieve console screen buffer information
61    NoScreenBufferInfo,
62}
63
64/// Estimate the console font cell size in pixels.
65///
66/// Implementation details
67/// - The function uses conservative default font cell dimensions
68///   (`DEFAULT_FONT_SIZE_WIDTH` / `DEFAULT_FONT_SIZE_HEIGHT`) that roughly
69///   match Consolas 12pt at 96 DPI, then scales them using the DPI value
70///   returned by `GetDpiForWindow` for the console window.
71/// - This approach is used because querying `GetCurrentConsoleFontEx`
72///   often returns unusable metrics on many terminal hosts; the result
73///   here is explicitly an estimate and may not match the actual font
74///   used by the terminal.
75///
76/// Returns
77/// - `Ok(FontSize)` with the estimated font cell size in pixels.
78/// - `Err(TerminalError::NoStdHandle)` if the standard output handle
79///   cannot be obtained.
80pub fn get_size_of_the_font() -> Result<FontSize, TerminalError> {
81    unsafe {
82        let h_console: HANDLE = GetStdHandle(STD_OUTPUT_HANDLE);
83        if h_console.is_null() {
84            return Err(TerminalError::NoStdHandle);
85        }
86        let dpi = GetDpiForWindow(GetConsoleWindow());
87        let scale = dpi as f32 / 96.0;
88        let (width, height) = (
89            (DEFAULT_FONT_SIZE_WIDTH as f32 * scale).round() as u16,
90            (DEFAULT_FONT_SIZE_HEIGHT as f32 * scale).round() as u16,
91        );
92        // Failed to retrieve console screen buffer information
93        return Ok(FontSize {
94            width: width as i32,
95            height: height as i32,
96        });
97    }
98}
99
100/// Estimate the terminal window size in pixels.
101///
102/// Implementation details
103/// - This function reads the console screen buffer information and uses the
104///   estimated font cell pixel size (from `get_size_of_the_font()`) to
105///   convert character dimensions into pixels.
106/// - Note: the implementation multiplies the estimated font cell size by
107///   the console buffer dimensions returned in `CONSOLE_SCREEN_BUFFER_INFO`.
108///   That value may represent the full buffer size (not the visible
109///   client window) depending on the host. The returned pixel size is an
110///   estimate intended to be "good enough" for most simple use cases.
111///
112/// Returns
113/// - `Ok(TerminalSize)` with the estimated terminal width and height in pixels.
114/// - `Err(TerminalError::NoStdHandle)` or `Err(TerminalError::NoScreenBufferInfo)`
115///   if native calls fail.
116pub fn get_size_of_the_terminal() -> Result<TerminalSize, TerminalError> {
117    unsafe {
118        let h_console: HANDLE = GetStdHandle(STD_OUTPUT_HANDLE);
119        if h_console.is_null() {
120            return Err(TerminalError::NoStdHandle);
121        }
122
123        let mut info = CONSOLE_SCREEN_BUFFER_INFO {
124            dwSize: COORD { X: 0, Y: 0 },
125            dwCursorPosition: COORD { X: 0, Y: 0 },
126            wAttributes: 0,
127            srWindow: SMALL_RECT {
128                Left: 0,
129                Top: 0,
130                Right: 0,
131                Bottom: 0,
132            },
133            dwMaximumWindowSize: COORD { X: 0, Y: 0 },
134        };
135        if GetConsoleScreenBufferInfo(h_console, &mut info) == 0 {
136            return Err(TerminalError::NoScreenBufferInfo);
137        }
138        let terminal_size = get_size_of_the_font()?;
139        let pixel_size = TerminalSize {
140            width: terminal_size.width * info.dwSize.X as i32,
141            height: terminal_size.height * info.dwSize.Y as i32,
142        };
143        return Ok(pixel_size);
144    }
145}