From 77ea5c4ccd3092029ec963bb78ca3539d97905bd Mon Sep 17 00:00:00 2001 From: Brett Sutton Date: Fri, 29 May 2026 11:38:23 +1000 Subject: [PATCH] Fix #9: query terminal window size via platform APIs --- CHANGELOG.md | 4 +++ lib/src/console.dart | 4 +-- lib/src/ffi/termlib.dart | 3 ++ lib/src/ffi/unix/termios.dart | 49 ++++++++++++++++++++++++------ lib/src/ffi/unix/termlib_unix.dart | 33 ++++++++++++++++++-- lib/src/ffi/win/termlib_win.dart | 20 ++++++++++++ 6 files changed, 100 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c0213..0d2e818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased + - Fix #9: Query console window size through platform terminal APIs so tmux + panes report their pane dimensions instead of the outer terminal size. + # 5.0.0 - *BREAKING*: Raise the minimum Dart SDK constraint to `>=3.10.0`. - *BREAKING*: Update `win32` dependency support to `>=6.0.1 <7.0.0`. diff --git a/lib/src/console.dart b/lib/src/console.dart index f6ba9ef..8b157c7 100644 --- a/lib/src/console.dart +++ b/lib/src/console.dart @@ -127,7 +127,7 @@ class Console { /// Returns the width of the current console window in characters. int get windowWidth { if (hasTerminal) { - return stdout.terminalColumns; + return _termlib.windowWidth ?? stdout.terminalColumns; } else { // Treat a window that has no terminal as if it is 80x25. This should be // more compatible with CI/CD environments. @@ -138,7 +138,7 @@ class Console { /// Returns the height of the current console window in characters. int get windowHeight { if (hasTerminal) { - return stdout.terminalLines; + return _termlib.windowHeight ?? stdout.terminalLines; } else { // Treat a window that has no terminal as if it is 80x25. This should be // more compatible with CI/CD environments. diff --git a/lib/src/ffi/termlib.dart b/lib/src/ffi/termlib.dart index 6bbf579..b63dd03 100644 --- a/lib/src/ffi/termlib.dart +++ b/lib/src/ffi/termlib.dart @@ -13,6 +13,9 @@ import 'unix/termlib_unix.dart'; import 'win/termlib_win.dart'; abstract class TermLib { + int? get windowHeight; + int? get windowWidth; + int setWindowHeight(int height); int setWindowWidth(int width); diff --git a/lib/src/ffi/unix/termios.dart b/lib/src/ffi/unix/termios.dart index ecb07d1..aa9232f 100644 --- a/lib/src/ffi/unix/termios.dart +++ b/lib/src/ffi/unix/termios.dart @@ -64,6 +64,9 @@ const int TCSAFLUSH = 2; // drain output, flush input const int VMIN = 16; // minimum number of characters to receive const int VTIME = 17; // time in 1/10s before returning +const int TIOCGWINSZ_LINUX = 0x5413; +const int TIOCGWINSZ_MACOS = 0x40087468; + // typedef unsigned long tcflag_t; typedef tcflag_t = UnsignedLong; @@ -104,16 +107,44 @@ base class TermIOS extends Struct { external int c_ospeed; } +// struct winsize { +// unsigned short ws_row; +// unsigned short ws_col; +// unsigned short ws_xpixel; +// unsigned short ws_ypixel; +// }; +base class WinSize extends Struct { + @UnsignedShort() + external int ws_row; + @UnsignedShort() + external int ws_col; + @UnsignedShort() + external int ws_xpixel; + @UnsignedShort() + external int ws_ypixel; +} + // int tcgetattr(int, struct termios *); -typedef TCGetAttrNative = Int32 Function( - Int32 fildes, Pointer termios); +typedef TCGetAttrNative = + Int32 Function(Int32 fildes, Pointer termios); typedef TCGetAttrDart = int Function(int fildes, Pointer termios); // int tcsetattr(int, int, const struct termios *); -typedef TCSetAttrNative = Int32 Function( - Int32 fildes, - Int32 optional_actions, - Pointer termios, -); -typedef TCSetAttrDart = int Function( - int fildes, int optional_actions, Pointer termios); +typedef TCSetAttrNative = + Int32 Function( + Int32 fildes, + Int32 optional_actions, + Pointer termios, + ); +typedef TCSetAttrDart = + int Function(int fildes, int optional_actions, Pointer termios); + +// int ioctl(int, unsigned long, ...); +typedef IOCtlNative = + Int32 Function( + Int32 fildes, + UnsignedLong request, + Pointer winsize, + ); +typedef IOCtlDart = + int Function(int fildes, int request, Pointer winsize); diff --git a/lib/src/ffi/unix/termlib_unix.dart b/lib/src/ffi/unix/termlib_unix.dart index c4dfcbb..4f8b75a 100644 --- a/lib/src/ffi/unix/termlib_unix.dart +++ b/lib/src/ffi/unix/termlib_unix.dart @@ -23,6 +23,32 @@ class TermLibUnix implements TermLib { late final TCGetAttrDart tcgetattr; late final TCSetAttrDart tcsetattr; + late final IOCtlDart ioctl; + + int get _windowSizeRequest => + Platform.isMacOS ? TIOCGWINSZ_MACOS : TIOCGWINSZ_LINUX; + + int? _readWindowSize(int Function(WinSize size) value) { + final winsize = calloc(); + try { + for (final fd in [STDOUT_FILENO, STDIN_FILENO, STDERR_FILENO]) { + if (ioctl(fd, _windowSizeRequest, winsize) == 0 && + winsize.ref.ws_col > 0 && + winsize.ref.ws_row > 0) { + return value(winsize.ref); + } + } + return null; + } finally { + calloc.free(winsize); + } + } + + @override + int? get windowHeight => _readWindowSize((size) => size.ws_row); + + @override + int? get windowWidth => _readWindowSize((size) => size.ws_col); @override int setWindowHeight(int height) { @@ -47,8 +73,10 @@ class TermLibUnix implements TermLib { ..ref.c_cflag = (origTermIOS.c_cflag & ~CSIZE) | CS8 ..ref.c_lflag = origTermIOS.c_lflag & ~(ECHO | ICANON | IEXTEN | ISIG) ..ref.c_cc = origTermIOS.c_cc - ..ref.c_cc[VMIN] = 0 // VMIN -- return each byte, or 0 for timeout - ..ref.c_cc[VTIME] = 1 // VTIME -- 100ms timeout (unit is 1/10s) + ..ref.c_cc[VMIN] = + 0 // VMIN -- return each byte, or 0 for timeout + ..ref.c_cc[VTIME] = + 1 // VTIME -- 100ms timeout (unit is 1/10s) ..ref.c_ispeed = origTermIOS.c_ispeed ..ref.c_oflag = origTermIOS.c_ospeed; @@ -74,6 +102,7 @@ class TermLibUnix implements TermLib { tcsetattr = _stdlib.lookupFunction( 'tcsetattr', ); + ioctl = _stdlib.lookupFunction('ioctl'); // store console mode settings so we can return them again as necessary _origTermIOSPointer = calloc(); diff --git a/lib/src/ffi/win/termlib_win.dart b/lib/src/ffi/win/termlib_win.dart index 0c7202d..d24113d 100644 --- a/lib/src/ffi/win/termlib_win.dart +++ b/lib/src/ffi/win/termlib_win.dart @@ -18,6 +18,26 @@ class TermLibWindows implements TermLib { late final HANDLE inputHandle; late final HANDLE outputHandle; + int? _readWindowSize(int Function(CONSOLE_SCREEN_BUFFER_INFO info) value) { + final pBufferInfo = calloc(); + try { + if (!GetConsoleScreenBufferInfo(outputHandle, pBufferInfo).value) { + return null; + } + return value(pBufferInfo.ref); + } finally { + calloc.free(pBufferInfo); + } + } + + @override + int? get windowHeight => + _readWindowSize((info) => info.srWindow.Bottom - info.srWindow.Top + 1); + + @override + int? get windowWidth => + _readWindowSize((info) => info.srWindow.Right - info.srWindow.Left + 1); + @override int setWindowHeight(int height) { throw UnsupportedError(