#1021

`window_size()` returns a `width` and `height` of 0 on Windows Terminal

Opengcavelieropened 9mo ago

Describe the bug Windows Terminal doesn't fully support the TIOCGWINSZ ioctl command (see the open issue) As a consequence, window_size() returns height and width with a value of 0

To Reproduce

fn main() {
    println!("{:?}", crossterm::terminal::window_size());
}

When executing this code on Windows Terminal (running WSL Ubuntu 24.04.1, so this is not a Windows issue), the result is :

Ok(WindowSize { rows: 48, columns: 208, width: 0, height: 0 })

Expected behavior height and width should be set to the correct values.

OS

  • Ubuntu 24.04.1 in WSL on Windows 10

Terminal/Console

  • Windows Terminal 1.22.12111.0

Proposal When height and width are set to 0 after calling TIOCGWINSZ, crossterm could fallback to using a specific xterm control sequence (\033[14t as described here ("Report xterm window in pixels")) which is implemented in Windows Terminal.

The following python proof-of-concept code produces the correct values

import termios
import fcntl
import struct
import tty

# Trying TIOCGWINSZ on /dev/tty => KO
with open("/dev/tty", 'r') as fd:
    packed = fcntl.ioctl(fd, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
    rows, cols, h_pixels, v_pixels = struct.unpack('HHHH', packed)
    print(f"TIOCGWINSZ on /dev/tty: rows: {rows}, cols: {cols}, h_pixels: {h_pixels}, v_pixels: {v_pixels}")

# Trying TIOCGWINSZ on STDOUT => KO
packed = fcntl.ioctl(1, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
rows, cols, h_pixels, v_pixels = struct.unpack('HHHH', packed)
print(f"TIOCGWINSZ on STDOUT  : rows: {rows}, cols: {cols}, h_pixels: {h_pixels}, v_pixels: {v_pixels}")

# Report xterm window in pixels as CSI 4 ; height ; width t
# cf https://www.xfree86.org/current/ctlseqs.html
with open("/dev/tty", 'a') as fd:
    # get tty_attrs so that we can restore them later
    tty_attrs = termios.tcgetattr(fd)
    # enable raw mode
    tty.setraw(fd)
    # send
    fd.write("\033[14t")
    # restore tty attrs
    termios.tcsetattr(fd, termios.TCSAFLUSH, tty_attrs)
with open("/dev/tty", 'r') as fd:
    # reading response (CSI 4 ; height ; width t)
    tty_attrs = termios.tcgetattr(fd)
    tty.setraw(fd)
    c = fd.read(1)
    packed = ''
    while c != 't':
        packed += c
        c = fd.read(1)
    termios.tcsetattr(fd, termios.TCSAFLUSH, tty_attrs)
    packed = packed.strip("\033[4;")
    v_pixels, h_pixels = packed.split(';')
    print(f"h_pixels: {h_pixels}, v_pixels: {v_pixels}")

Output :

TIOCGWINSZ on /dev/tty: rows: 48, cols: 208, h_pixels: 0, v_pixels: 0
TIOCGWINSZ on STDOUT  : rows: 48, cols: 208, h_pixels: 0, v_pixels: 0
h_pixels: 2080, v_pixels: 960

I would be happy to create a PR implementing this fallback method if that works for you.

Activity

30
joshka9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

joshka9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

joshka9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

joshka9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

joshka9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

joshka9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

gcavelier9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

I don't know...

In this case, TIOCGWINSZ is "semi" supported, it's not like it's all 0, only the width and height fields, and that's the issue. All the values could also be retrieve using xterm control sequences (not tested) but I don't know which way is "better".

gcavelier9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

I don't know...

In this case, TIOCGWINSZ is "semi" supported, it's not like it's all 0, only the width and height fields, and that's the issue. All the values could also be retrieve using xterm control sequences (not tested) but I don't know which way is "better".

gcavelier9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

I don't know...

In this case, TIOCGWINSZ is "semi" supported, it's not like it's all 0, only the width and height fields, and that's the issue. All the values could also be retrieve using xterm control sequences (not tested) but I don't know which way is "better".

gcavelier9mo ago

Fallbacks can be a pain - is there a reliable way to detect when those is not properly supported?

I don't know...

In this case, TIOCGWINSZ is "semi" supported, it's not like it's all 0, only the width and height fields, and that's the issue. All the values could also be retrieve using xterm control sequences (not tested) but I don't know which way is "better".

j4james9mo ago

@gcavelier Note that this is not something that Windows Terminal can fix. TIOCGWINSZ works on Linux because a Linux terminal can make an ioctl call with TIOCSWINSZ to set the window size. But Window Terminal isn't a Linux application, so there is no way it can make that call.

It's also worth noting that TIOCGWINSZ isn't entirely reliable, even on Linux, because there's no official definition for those pixel values. Some terminals will set them to the pixel dimensions of their text area, but others may set them to the dimensions of the terminal window itself (which can be slightly larger). Most of the time you should be fine if you're rounding down when calculating the cell size, but there are edge cases where you can end up with an unexpected result.

Also if you're connecting onto a Linux box via telnet, you may find those pixel dimensions being set to something completely wrong like 32607x0 (the exact values will vary). This is the case even if the connecting terminal is on Linux, because there is no way to forward those values over the telnet protocol.

j4james9mo ago

@gcavelier Note that this is not something that Windows Terminal can fix. TIOCGWINSZ works on Linux because a Linux terminal can make an ioctl call with TIOCSWINSZ to set the window size. But Window Terminal isn't a Linux application, so there is no way it can make that call.

It's also worth noting that TIOCGWINSZ isn't entirely reliable, even on Linux, because there's no official definition for those pixel values. Some terminals will set them to the pixel dimensions of their text area, but others may set them to the dimensions of the terminal window itself (which can be slightly larger). Most of the time you should be fine if you're rounding down when calculating the cell size, but there are edge cases where you can end up with an unexpected result.

Also if you're connecting onto a Linux box via telnet, you may find those pixel dimensions being set to something completely wrong like 32607x0 (the exact values will vary). This is the case even if the connecting terminal is on Linux, because there is no way to forward those values over the telnet protocol.

gcavelier7mo ago

Could someone please take a look at #1025 ?

Thanks!

gcavelier7mo ago

Could someone please take a look at #1025 ?

Thanks!

gcavelier7mo ago

Could someone please take a look at #1025 ?

Thanks!

gcavelier4mo ago

Hi!

Is there a way to unblock this issue (and the associated PR) ?

@joshka @TimonPost, I would be happy to rework the PR (and even delete it) if necessary. Just tell me what I need to do ;)

Thanks!

gcavelier4mo ago

Hi!

Is there a way to unblock this issue (and the associated PR) ?

@joshka @TimonPost, I would be happy to rework the PR (and even delete it) if necessary. Just tell me what I need to do ;)

Thanks!

joshka4mo ago

Thanks for following up. I don’t know this area deeply enough to give a final design answer immediately, but I do think the bug is real: window_size() can return width and height as 0 under Windows Terminal/WSL, which makes the current API less useful for callers that need pixel dimensions.

That said, I’m worried the proposed fix may change behavior in a way that is problematic for some situations while fixing this one. window_size() is currently an OS/ioctl-style query. Adding CSI 14 t makes it an interactive terminal protocol exchange: it writes to the terminal, may need raw mode, reads from the tty, can block or time out, and can consume bytes from the same input stream used by application event handling.

There are also concrete issues in the current PR shape: CSI 14 t replies as CSI 4 ; height ; width t, but the implementation appears to assign those directly as (width, height), so the values are reversed. The direct tty read also has no timeout, which could hang on terminals that do not answer.

So I’d like to step back from this specific implementation before merging it. There may still be a good idea here, but it might be better solved with a different approach.

Possible directions:

  1. Keep window_size() as the OS-reported value and document that pixel dimensions are best-effort and may be zero or unreliable.
  2. Add a separate opt-in API for querying terminal-reported pixel size with CSI 14 t, with clear timeout and input-stream caveats.
  3. Expose lower-level command/parser support so applications that own their event loop can decide when it is safe to query and consume the response.

If you’re interested in continuing this, I’d prefer exploring one of those designs rather than reworking this as an implicit fallback inside window_size().

joshka4mo ago

Thanks for following up. I don’t know this area deeply enough to give a final design answer immediately, but I do think the bug is real: window_size() can return width and height as 0 under Windows Terminal/WSL, which makes the current API less useful for callers that need pixel dimensions.

That said, I’m worried the proposed fix may change behavior in a way that is problematic for some situations while fixing this one. window_size() is currently an OS/ioctl-style query. Adding CSI 14 t makes it an interactive terminal protocol exchange: it writes to the terminal, may need raw mode, reads from the tty, can block or time out, and can consume bytes from the same input stream used by application event handling.

There are also concrete issues in the current PR shape: CSI 14 t replies as CSI 4 ; height ; width t, but the implementation appears to assign those directly as (width, height), so the values are reversed. The direct tty read also has no timeout, which could hang on terminals that do not answer.

So I’d like to step back from this specific implementation before merging it. There may still be a good idea here, but it might be better solved with a different approach.

Possible directions:

  1. Keep window_size() as the OS-reported value and document that pixel dimensions are best-effort and may be zero or unreliable.
  2. Add a separate opt-in API for querying terminal-reported pixel size with CSI 14 t, with clear timeout and input-stream caveats.
  3. Expose lower-level command/parser support so applications that own their event loop can decide when it is safe to query and consume the response.

If you’re interested in continuing this, I’d prefer exploring one of those designs rather than reworking this as an implicit fallback inside window_size().

j4james4mo ago

If you want an easier solution, which doesn't require an asynchronous query, you could always fallback to a simple algorithm like this:

if size.ws_xpixel == 0 || size.ws_ypixel == 0 {
    size.ws_xpixel = size.ws_col * 10;
    size.ws_ypixel = size.ws_row * 20;
}

This should at least fix the issue on Windows Terminal, because it uses a virtual coordinate system that is independent of the configured font - a text cell is always 10x20 virtual pixels, because those were the dimensions of the original VT240 and VT340 sixel terminals.

The downside is that this would potentially produce images that are the wrong size on other terminals, at least in situations where ioctl doesn't work. But that's a rarer case, and maybe an incorrectly sized image is better than nothing?

Just an idea. Feel free to ignore.