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}