Add a pager module to the standard library

I was wondering why isn’t there a pager in one of the packages of the standard library. I know, there’s pydoc.pager(), but that’s not really a pager in itself it uses more or less, that’s great but if there isn’t more or less for some reasons on the system, it’s quite complex for the user because it just don’t use a pager at all.

So I made a pager, simple though but fonctional and fully in Python, with no need to thing external to Python.

I know that my code can’t be like that directly in the standard library and it won’t probably be its own package, but it will be a good fundation I think.

Here’s my code:

from shutil import get_terminal_size
import sys


# Enable ANSI escape codes on Windows
if sys.platform.startswith('win'):
    if sys.stdout.isatty():
        import ctypes
        kernel32 = ctypes.windll.kernel32
        kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 0x0007)


# Main logic
def pager(text: str) -> None:
    """Display a given text in a pager.

    Args:
        text (str): The text to be displayed in the pager.

    Raises:
        TypeError: If the input text is not a string.
        KeyboardInterrup: When the user presses Ctrl-C when prompting for the character
    """

    if not isinstance(text, str):
        raise TypeError('text arg must be str')

    # Get the character typed by the user
    def get_key() -> str:
        """Get the character typed by the user.

        Returns:
            str: The character typed by the user.
        """

        if sys.platform.startswith('win'):
            import msvcrt
            return msvcrt.getch().decode()
        else:
            import tty, termios
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setraw(fd)
                ch = sys.stdin.read(1)
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
            return ch
    
    print('\033c', end='', flush=True) # Clear the terminal

    lines = text.split('\n')
    rows, _ = get_terminal_size((35, 148))
    prompt = '\033[7m-- More (press \'q\' to exit) --\033[0m'

    for idx, line in enumerate(lines):
        if idx >= (rows - 1):
            print(prompt, end='', flush=True)

            key = get_key()

            # Fully remove the prompt before printing the line
            print('\r' + len(prompt) * ' ' + '\r', end='', flush=True)

            if key == '\x03': # Ctrl-C
                raise KeyboardInterrupt

            if key.lower() == 'q':
                break
            print(line)
        else:
            print(line)

So what is this system you speak of that doesn’t have more? Even MS-DOS 2 has it.

Following the source code of pydoc.pager(), a system without the environment variable MANPAGER or PAGER and with the environment variable TERM equal to dumb or emacs.

Yes, the environment should decide which pager to use, and the user can always explicitly pipe the output to more or a pager of choice.

And that I’m proposing is not to replace the pager used by pydoc, but to add a pager fully in Python in the standard library.

But what is wrong with piping to more when the user actually needs a pager?

It’s possible to don’t have it. And sometimes, like on Windows, the pipe is broken. If you try to pipe something to more in Windows, it will display one character by line.

I don’t see how you can have a system without more unless the user intentionally deletes the more executable, in which case the decision is theirs.

Not sure what you mean here. Can you give us a reproducible code example that can produce improper paged output when piped to more in Windows?

Sometimes the pipe is broken on Windows, sometimes.

For example wsl --help | more gives this output:

C
o
p
y
r
i
g
h
t

(
c
)

M
i
c
r
o
s
o
f
t

C
o
r
p
o
r
a
t
i
o
[...]

Also some lightweight Linux distros don’t have more or less, and most of custom OSes also.
And having a platform-independent pager for Python programs easy to access can be good and easier.

I’ve worked on Linux VMs and Docker images without more.

However, this would probably be best as a PyPI package, and it would surprise me if something like it doesn’t already exist.

2 Likes

Yes, PyPI packages about that already exist.

Not all Windows systems support ANSI terminal codes. Are you willing to support those? What about feature enhancements like searching or scrolling backwards? Or truncating long lines?

If someone were to add a pure Python pager implementation to the stdlib, it would need to handle these types of questions (and if the initial implementation didn’t offer support, the core devs would have to handle the inevitable issues and feature requests). I don’t think this is likely, particularly when piping to the system pager is available already.

In fact Windows supports ANSI escape codes via the ansi.sys driver, and for example colorama provide a good implementation for using them on Windows, but I made a simpler code with only standard library for fundation, I know this isn’t complete.

Do you propose to create a more complete implementation yourself, or are you hoping that someone else will do that? If you do intend to do so, I’d suggest working on the full implementation. If you do that as a package on PyPI, taking care not to use any dependencies outside of the stdlib, you’ll (a) get a better idea of the difficulty of implementing and supporting this, and (b) get a sense of how much interest there would be in having this in the stdlib.

1 Like

I think I’ll complete my current implementation, and yes I’ll keep only using standard library.

1 Like

Update about the code:

I remade it almost from 0 (I kept only the main part), now it’s better. It’s dispatched between multiple files for better reusability of the code and better readability. The main part works well, and I managed to make it go on a newline when the line is too long. Now I need to check if the backward scroll work well and after I’ll had to add the search and make the ANSI codes work on Windows.