185 lines
5.4 KiB
Python
185 lines
5.4 KiB
Python
"""
|
|
This module provides functions to get the terminal size across different
|
|
platforms.
|
|
|
|
Functions:
|
|
get_terminal_size: Get the current size of the terminal.
|
|
_get_terminal_size_windows: Get terminal size on Windows.
|
|
_get_terminal_size_tput: Get terminal size using `tput`.
|
|
_get_terminal_size_linux: Get terminal size on Linux.
|
|
|
|
Usage example:
|
|
>>> width, height = get_terminal_size()
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import typing
|
|
|
|
from . import converters
|
|
|
|
Dimensions = tuple[int, int]
|
|
OptionalDimensions = typing.Optional[Dimensions]
|
|
_StrDimensions = tuple[str, str]
|
|
_OptionalStrDimensions = typing.Optional[_StrDimensions]
|
|
|
|
|
|
def get_terminal_size() -> Dimensions: # pragma: no cover
|
|
"""Get the current size of your terminal.
|
|
|
|
Multiple returns are not always a good idea, but in this case it greatly
|
|
simplifies the code so I believe it's justified. It's not the prettiest
|
|
function but that's never really possible with cross-platform code.
|
|
|
|
Returns:
|
|
width, height: Two integers containing width and height
|
|
"""
|
|
w: int | None
|
|
h: int | None
|
|
|
|
with contextlib.suppress(Exception):
|
|
# Default to 79 characters for IPython notebooks
|
|
from IPython import get_ipython # type: ignore[attr-defined]
|
|
|
|
ipython = get_ipython() # type: ignore[no-untyped-call]
|
|
from ipykernel import zmqshell # type: ignore[import-not-found]
|
|
|
|
if isinstance(ipython, zmqshell.ZMQInteractiveShell):
|
|
return 79, 24
|
|
with contextlib.suppress(Exception):
|
|
# This works for Python 3, but not Pypy3. Probably the best method if
|
|
# it's supported so let's always try
|
|
import shutil
|
|
|
|
w, h = shutil.get_terminal_size()
|
|
if w and h:
|
|
# The off by one is needed due to progressbars in some cases, for
|
|
# safety we'll always substract it.
|
|
return w - 1, h
|
|
with contextlib.suppress(Exception):
|
|
w = converters.to_int(os.environ.get('COLUMNS'))
|
|
h = converters.to_int(os.environ.get('LINES'))
|
|
if w and h:
|
|
return w, h
|
|
with contextlib.suppress(Exception):
|
|
import blessings # type: ignore[import-untyped]
|
|
|
|
terminal = blessings.Terminal()
|
|
w = terminal.width
|
|
h = terminal.height
|
|
if w and h:
|
|
return w, h
|
|
with contextlib.suppress(Exception):
|
|
# The method can return None so we don't unpack it
|
|
wh = _get_terminal_size_linux()
|
|
if wh is not None and all(wh):
|
|
return wh
|
|
|
|
with contextlib.suppress(Exception):
|
|
# Windows detection doesn't always work, let's try anyhow
|
|
wh = _get_terminal_size_windows()
|
|
if wh is not None and all(wh):
|
|
return wh
|
|
|
|
with contextlib.suppress(Exception):
|
|
# needed for window's python in cygwin's xterm!
|
|
wh = _get_terminal_size_tput()
|
|
if wh is not None and all(wh):
|
|
return wh
|
|
|
|
return 79, 24
|
|
|
|
|
|
def _get_terminal_size_windows() -> OptionalDimensions: # pragma: no cover
|
|
res = None
|
|
try:
|
|
from ctypes import ( # type: ignore[attr-defined]
|
|
create_string_buffer,
|
|
windll,
|
|
)
|
|
|
|
# stdin handle is -10
|
|
# stdout handle is -11
|
|
# stderr handle is -12
|
|
|
|
h = windll.kernel32.GetStdHandle(-12)
|
|
csbi = create_string_buffer(22)
|
|
res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
|
|
except Exception:
|
|
return None
|
|
|
|
if res:
|
|
import struct
|
|
|
|
(_, _, _, _, _, left, top, right, bottom, _, _) = struct.unpack(
|
|
'hhhhHhhhhhh', csbi.raw
|
|
)
|
|
w = right - left
|
|
h = bottom - top
|
|
return w, h
|
|
else:
|
|
return None
|
|
|
|
|
|
def _get_terminal_size_tput() -> OptionalDimensions: # pragma: no cover
|
|
# get terminal width src: http://stackoverflow.com/questions/263890/
|
|
try:
|
|
import subprocess
|
|
|
|
proc = subprocess.Popen(
|
|
['tput', 'cols'],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
output = proc.communicate(input=None)
|
|
w = int(output[0])
|
|
proc = subprocess.Popen(
|
|
['tput', 'lines'],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
output = proc.communicate(input=None)
|
|
h = int(output[0])
|
|
except Exception:
|
|
return None
|
|
else:
|
|
return w, h
|
|
|
|
|
|
def _get_terminal_size_linux() -> OptionalDimensions: # pragma: no cover
|
|
def ioctl_gwinsz(fd: int) -> tuple[str, str] | None:
|
|
try:
|
|
import fcntl
|
|
import struct
|
|
import termios
|
|
|
|
return typing.cast(
|
|
_OptionalStrDimensions,
|
|
struct.unpack(
|
|
'hh',
|
|
fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'), # type: ignore[call-overload]
|
|
),
|
|
)
|
|
except Exception:
|
|
return None
|
|
|
|
size: _OptionalStrDimensions
|
|
size = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2)
|
|
|
|
if not size:
|
|
with contextlib.suppress(Exception):
|
|
fd = os.open(os.ctermid(), os.O_RDONLY)
|
|
size = ioctl_gwinsz(fd)
|
|
os.close(fd)
|
|
if not size:
|
|
try:
|
|
size = os.environ['LINES'], os.environ['COLUMNS']
|
|
except Exception:
|
|
return None
|
|
|
|
return int(size[1]), int(size[0])
|