diff -r 8d6d068e9966 Lib/termsize.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/termsize.py Fri Dec 16 17:03:30 2011 +0100 @@ -0,0 +1,46 @@ +"""Query the terminal size and return the result as (colums, rows). + +get_terminal_size is the highlevel function which should normally be +used, query_terminal_size is the low-level implementation. +""" + +from collections import namedtuple as _namedtuple +import os as _os +from _termsize import query_terminal_size + +TerminalSize = _namedtuple('TerminalSize', ['columns', 'rows']) + +def get_terminal_size(columns=80, rows=25): + """Get the size of the terminal window. + + First, the environment variables COLUMNS and ROWS are checked. If + they are defined and the values are valid positive integers, they + are used. + + When COLUMNS or ROWS are not defined, which is the common case, + the terminal connected to sys.stdout is queried. + + If the terminal size cannot be sucessfully queried, either because + the system doesn't support querying, or we are not connected to a + terminal, the default values given in the parameters are used. + + The value returned is a named tuple of type TerminalSize. + + Right now the values are not cached, but this might change. + """ + try: + columns = int(_os.environ['COLUMNS']) + except (KeyError, ValueError): + columns = 0 + + try: + rows = int(_os.environ['ROWS']) + except (KeyError, ValueError): + rows = 0 + + if columns <= 0 or rows <= 0: + size = query_terminal_size() + columns = columns if columns > 0 else size[0] + rows = rows if rows > 0 else size[1] + + return TerminalSize(columns, rows) diff -r 8d6d068e9966 Lib/test/test_termsize.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/test/test_termsize.py Fri Dec 16 17:03:30 2011 +0100 @@ -0,0 +1,38 @@ +import termsize +import unittest +import os +import contextlib +from test import support + +@contextlib.contextmanager +def set_environ(**variables): + old = {var:os.environ.get(var, None) for var in variables} + os.environ.update(variables) + try: + yield + finally: + for var, value in old: + if value is None: + del os.environ[var] + else: + os.environ[var] = value + +class TermsizeTests(unittest.TestCase): + def test_does_not_crash(): + # There's no easy portable way to actually check the size of + # the terminal, so let's check if it returns something + # sensible instead. + size = termsize.get_terminal_size() + self.assertTrue(0 < size.columns) + self.assertTrue(0 < size.rows) + + def test_os_environ_first(): + # Check if environment variables have precedence + + with set_environ(COLUMNS=777): + size = termsize.get_terminal_size() + self.assertTrue(size.columns == 777) + + with set_environ(ROWS=888): + size = termsize.get_terminal_size() + self.assertTrue(size.rows == 888) diff -r 8d6d068e9966 Modules/_termsizemodule.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Modules/_termsizemodule.c Fri Dec 16 17:03:30 2011 +0100 @@ -0,0 +1,101 @@ +#include "Python.h" + +#ifdef HAVE_SYS_IOCTL_H +#include +#define termsize_use_ioctl +#else + +#ifdef HAVE_CONIO_H +#include +#include +#define termsize_use_conio + +#else + +#define termsize_use_notimplemented + +#endif /* HAVE_CONIO_H */ +#endif /* HAVE_SYS_IOCTL_H */ + +#define termsize_docstring \ + "Return the size of the terminal window as (columns, rows).\n" \ + "\n" \ + "The optional argument fd (default 1, or standard output) specifies\n" \ + "which file descriptor should be queried.\n" \ + "\n" \ + "If the file descriptor is not connected to a terminal, an IOError\n" \ + "is thrown. If the function is not available for this system, a\n" \ + "NotImplementedError is thrown." + +static PyObject* query_terminal_size(PyObject *self, PyObject *args) +{ + int fd = 1; /* stdout */ + + if(!PyArg_ParseTuple(args, "|i", &fd)) + return NULL; + +#ifdef termsize_use_notimplemented + return PyErr_Format(PyExc_NotImplementedError, + "not imlemented for this system, sorry"); +#endif + + int columns, rows; + +#ifdef termsize_use_ioctl + struct winsize w; + if(ioctl(fd, TIOCGWINSZ, &w)) { + switch(errno){ + case EBADF: + return PyErr_Format(PyExc_ValueError, + "bad file descriptor"); + default: + return PyErr_SetFromErrno(PyExc_IOError); + } + } + columns = w.ws_col; + rows = w.ws_row; +#endif + +#ifdef termsize_use_conio + int nhandle; + switch(fd){ + case 0: nhandle = STD_INPUT_HANDLE; + break; + case 1: nhandle = STD_OUTPUT_HANDLE; + break; + case 2: nhandle = STD_ERROR_HANDLE; + break; + default: + return PyErr_Format(PyExc_ValueError, "bad file descriptor"); + } + + HANDLE handle = GetStdHandle(fd); + CONSOLE_SCREEN_BUFFER_INFO csbi; + if(GetConsoleScreenBufferInfo(handle, &csbi)) + return PyErr_Format(PyExc_IOError, "error %i", + (int) GetLastError()); + + columns = csbi.srWindow.Right - csbi.srWindow.Left + 1; + rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; +#endif + + return Py_BuildValue("(ii)", columns, rows); +} + +static PyMethodDef methods[] = { + {"query_terminal_size", query_terminal_size, METH_VARARGS, + termsize_docstring}, + {NULL, NULL, 0, NULL} /* sentinel */ +}; + +static struct PyModuleDef _termsize = { + PyModuleDef_HEAD_INIT, + .m_name = "_termsize", + .m_size = 0, + .m_methods = methods, +}; + +PyMODINIT_FUNC PyInit__termsize(void) +{ + return PyModule_Create(&_termsize); +} diff -r 8d6d068e9966 configure --- a/configure Thu Dec 15 19:24:49 2011 -0500 +++ b/configure Fri Dec 16 17:03:30 2011 +0100 @@ -6147,7 +6147,7 @@ sys/audioio.h sys/xattr.h sys/bsdtty.h sys/event.h sys/file.h sys/loadavg.h \ sys/lock.h sys/mkdev.h sys/modem.h \ sys/param.h sys/select.h sys/sendfile.h sys/socket.h sys/statvfs.h \ -sys/stat.h sys/termio.h sys/time.h \ +sys/stat.h sys/termio.h sys/time.h sys/ioctl.h \ sys/times.h sys/types.h sys/uio.h sys/un.h sys/utsname.h sys/wait.h pty.h \ libutil.h sys/resource.h netpacket/packet.h sysexits.h bluetooth.h \ bluetooth/bluetooth.h linux/tipc.h spawn.h util.h @@ -14564,8 +14564,8 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 # Files that config.status was made for. -config_files="`echo $ac_config_files`" -config_headers="`echo $ac_config_headers`" +config_files="$ac_config_files" +config_headers="$ac_config_headers" _ACEOF diff -r 8d6d068e9966 configure.in --- a/configure.in Thu Dec 15 19:24:49 2011 -0500 +++ b/configure.in Fri Dec 16 17:03:30 2011 +0100 @@ -1337,7 +1337,7 @@ sys/audioio.h sys/xattr.h sys/bsdtty.h sys/event.h sys/file.h sys/loadavg.h \ sys/lock.h sys/mkdev.h sys/modem.h \ sys/param.h sys/select.h sys/sendfile.h sys/socket.h sys/statvfs.h \ -sys/stat.h sys/termio.h sys/time.h \ +sys/stat.h sys/termio.h sys/time.h sys/ioctl.h \ sys/times.h sys/types.h sys/uio.h sys/un.h sys/utsname.h sys/wait.h pty.h \ libutil.h sys/resource.h netpacket/packet.h sysexits.h bluetooth.h \ bluetooth/bluetooth.h linux/tipc.h spawn.h util.h) diff -r 8d6d068e9966 pyconfig.h.in --- a/pyconfig.h.in Thu Dec 15 19:24:49 2011 -0500 +++ b/pyconfig.h.in Fri Dec 16 17:03:30 2011 +0100 @@ -902,6 +902,9 @@ /* Define to 1 if you have the header file. */ #undef HAVE_SYS_FILE_H +/* Define to 1 if you have the header file. */ +#undef HAVE_SYS_IOCTL_H + /* Define to 1 if you have the header file. */ #undef HAVE_SYS_LOADAVG_H diff -r 8d6d068e9966 setup.py --- a/setup.py Thu Dec 15 19:24:49 2011 -0500 +++ b/setup.py Fri Dec 16 17:03:30 2011 +0100 @@ -535,6 +535,9 @@ # static Unicode character database exts.append( Extension('unicodedata', ['unicodedata.c']) ) + # querying of terminal size and width + exts.append( Extension('_termsize', ['_termsizemodule.c']) ) + # Modules with some UNIX dependencies -- on by default: # (If you have a really backward UNIX, select and socket may not be # supported...)