Surprising behavior of getTerminalSize
The documentation for getTerminalSize says
https://github.com/UnkindPartition/ansi-terminal/blob/16d8d3bfa5a0b4473f0968371a5f32a93f5f3f62/ansi-terminal/src/System/Console/ANSI.hs#L957-L961
My understanding of this was that if the preconditions do not hold (either terminal does not support cursor movement or stdin is not available), getTerminalSize :: IO (Maybe (Int, Int)) will return Nothing. I was surprised to learn that this is not the case.
Namely, running main = getTerminalSize >>= print with redirected stdin fails with an IO exception:
$ ./GetTerminalSize.hs < /dev/null
GetTerminalSize.hs: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.IOException:
<stdin>: hWaitForInput: end of file
HasCallStack backtrace:
ioException, called at libraries/ghc-internal/src/GHC/Internal/IO/Handle/Internals.hs:353:11 in ghc-internal:GHC.Internal.IO.Handle.Internals
Is it intended? Not the end of the world, but I feel like getTerminalSize could have caught the exception and return Nothing instead of passing the bucket to a caller.
Short of catching IO exceptions, getTerminalSize could have checked hIsTerminalDevice stdin. One can argue that other functions in this library do not check hIsTerminalDevice stdout, leaving it to a caller, so checking stdin is also caller's resposibility. Which is not unreasonable, but emitting ANSI codes to a non-terminal stdout does not throw an exception, while getTerminalSize (if preconditions do not hold) does.
My understanding of this was that if the preconditions do not hold (either terminal does not support cursor movement or
stdinis not available),getTerminalSize :: IO (Maybe (Int, Int))will returnNothing. I was surprised to learn that this is not the case.
The document explicitly says
Use 'System.IO.hIsTerminalDevice' to test if 'stdin' is connected to a terminal.
So from what I understand this behaves exactly as documented.
@Bodigrim am I missing something?
So from what I understand this behaves exactly as documented.
I would expect getTerminalSize to return Nothing if preconditions are not satisfied instead of throwing an IO exception. This is what is not clear from the documentation.
Also I argue in https://github.com/UnkindPartition/ansi-terminal/issues/178#issuecomment-3146379856 that getTerminalSize should actually check isTerminalDevice stdin itself. It's a cheap thing to do and defuses a foot gun. A user might have never tested their program with stdin redirected, so the IO expection can be discovered only after a release.
I need to look at getTerminalSize more closely, full stop, as something is up with ansi-terminal-example on Windows 11, namely:
- #182
With #182 now fixed:
@Bodigrim, the original intent was the user would have tested stdin themselves, because they would want to do that only once, not every time they used getTerminalSize. I am sorry if the combination of the type signature and the Haddock documentation was ambiguous. I can fix the documentation. The final line:
For a different approach, one that does not use control character sequences and works when 'stdin' is redirected, see the
terminal-sizepackage.
was meant as a contrast.
@Bodigrim, the original intent was the user would have tested
stdinthemselves, because they would want to do that only once, not every time they usedgetTerminalSize.
Checking isTerminalDevice stdin is cheap, most likely way faster than anything else getTerminalSize does; and I hardly imagine a user running getTerminalSize in a hot loop, so from my perspective convenience of eliminating a source of IO exceptions beats raw performance in this case. But fair enough, a documentation clarification might be helpful for future users.
Related issues/pull requests:
- #124
- #140 (by @Bodigrim !)
Wow, I have absolutely no recollection of it! %)
@mpilgrem did you come to any conclusion? Now that it's documented I'm easy both ways, just would appreciate to have a decision.