3
\$\begingroup\$

I want to prompt the user to press y or n but I don't want to require them to press return. Is this the simplest/best way to do this? It seems a little complicated. I am on linux.

import sys
import tty
import termios

def prompt_yes_no():
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)  # Save the current terminal settings

    try:
        tty.setraw(fd)  # Set the terminal to raw mode
        print("Do you want to continue? (y/n): ", end='', flush=True)

        while True:
            ch = sys.stdin.read(1).lower()  # Read a single character
            if ch == 'y':
                return True
            elif ch == 'n':
                return False
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)  # Restore terminal settings

# Example usage
if prompt_yes_no():
    print("\nYou pressed 'y'. Continuing...")
else:
    print("\nYou pressed 'n'. Exiting...")
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

In Python 3.12+, tty.setraw is already returning the old settings, you don't need to do it yourself. And, in fact, the tty module is re-using all the symbols from the termios module, so you can only use it in lieu of termios, giving you something like:

def prompt_yes_no():
    fd = sys.stdin.fileno()
    old = tty.setraw(fd)
    try:
        print('Do you want to continue? (y/n): ', end='', flush=True)
        while True:
            match sys.stdin.read(1).lower():
                case 'y':
                    return True
                case 'n':
                    return False
                case _ as char:
                    print(repr(char), 'is not a recognized input', end='\r\n')
                    print('Do you want to continue? (y/n): ', end='', flush=True)
    finally:
        tty.tcsetattr(fd, tty.TCSADRAIN, old)

But the try … finally really feels like it would be better as a context manager:

import sys
import tty
import contextlib


@contextlib.contextmanager
def make_raw(stream=sys.stdin):
    fd = stream.fileno()
    old = tty.setraw(fd)
    try:
        yield stream
    finally:
        tty.tcsetattr(fd, tty.TCSADRAIN, old)

And if you really want a function to prompt yes or no, you can build on it, thus better separating concerns:

def prompt_yes_no(prompt='>', default_to=True):
    with make_raw() as stream:
        while True:
            print(prompt, end=' ', flush=True)
            match stream.read(1):
                case 'y' | 'Y':
                    return True
                case 'n' | 'N':
                    return False
                case '\r':
                    return default_to
                case _ as char:
                    print(repr(char), 'is not a recognized input', end='\r\n')

Or you can just call the proper function in each cases… YMMV.


However, all that being said, setraw is way too heavy for the job. Especially given the fact that it breaks user expectations about how the terminal should behave. Not only do you need special care to properly handle outputs (see the end='\r\n' bit in the code snippets) and inputs (getting only \r when the user presses the enter key instead of the usual \n), it also ignores the SIGINT signal leaving the user unable to Ctrl+C while on your prompt.

I would, instead, recommend the use of tty.setcbreak that still have good properties for what you’re trying to achieve but leave the terminal in a more user-friendly state. Note that, doing so, you will need to change the case '\r' into a case '\n' if you want to handle a default value.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.