Search code examples
pythonwindowspromptcurses

Why is the Windows prompt unresponsive after using a python curses window?


First of all, I'm a newby in python. Had to take a course of it in college and got hooked by its efficiency.

I have this sticky problem where the Windows 7 prompt becomes unresponsive after using a curses window. In Windows 10 it works well. Note that I'm using the Win7 terminal with its default settings. In my code I create a curses window to show 2 simultaneous progress bars, each for a file download. I implemented this by passing the curses window to a FileDownload class (one class instance for each download) that handles its progress bar inside this window. Oddly, in Windows 7 when the downloads are done and the control returns to the prompt, it becomes unresponsive to the keyboard. I worked around this by invoking curses.endwin() after using the window, but this causes the prompt to display all the way down the screen buffer, what hides the curses window.

Here is my code. Any ideas are greatly appreciated. Thanks!

# Skeleton version for simulations.
# Downloads 2 files simultaneously and shows a progress bar for each.
# Each file download is a FileDownload object that interacts with a
# common curses window passed as an argument.

import requests, math, threading, curses, datetime


class FileDownload:

    def __init__(self, y_pos, window, url):
        # Y position of the progress bar in the download queue window.
        self.__bar_pos = int(y_pos)
        self.__progress_window = window
        self.__download_url = url
        # Status of the file download object.
        self.__status = "queued"
        t = threading.Thread(target=self.__file_downloader)
        t.start()

    # Downloads selected file and handles its progress bar.
    def __file_downloader(self):
        file = requests.get(self.__download_url, stream=True)
        self.__status = "downloading"
        self.__progress_window.addstr(self.__bar_pos + 1, 1, "0%" + " " * 60 + "100%")
        size = int(file.headers.get('content-length'))
        win_prompt = "Downloading " + format(size, ",d") + " Bytes:"
        self.__progress_window.addstr(self.__bar_pos, 1, win_prompt)

        file_name = str(datetime.datetime.now().strftime("%Y-%m-%d_%H.%M.%d"))
        dump = open(file_name, "wb")

        # Progress bar length.
        bar_space = 58
        # Same as an index.
        current_iteration = 0
        # Beginning position of the progress bar.
        progress_position = 4
        # How many iterations will be needed (in chunks of 1 MB).
        iterations = math.ceil(size / 1024 ** 2)

        # Downloads the file in 1MB chunks.
        for block in file.iter_content(1024 ** 2):
            dump.write(block)

            # Progress bar controller.
            current_iteration += 1
            step = math.floor(bar_space / iterations)
            if current_iteration > 1:
                progress_position += step
            if current_iteration == iterations:
                step = bar_space - step * (current_iteration - 1)
            # Updates the progress bar.
            self.__progress_window.addstr(self.__bar_pos + 1, progress_position,
                                          "#" * step)
        dump.close()
        self.__status = "downloaded"

    # Returns the current status of the file download ("queued", "downloading" or
    # "downloaded").
    def get_status(self):
        return self.__status


# Instantiates each file download.
def files_downloader():

    # Creates curses window.
    curses.initscr()
    win = curses.newwin(8, 70)
    win.border(0)
    win.immedok(True)

    # Download URLs.
    urls = ["http://ipv4.download.thinkbroadband.com/10MB.zip",
            "http://ipv4.download.thinkbroadband.com/5MB.zip"]

    downloads_dct = {}
    for n in range(len(urls)):
        # Progress bar position in the window for the file.
        y_pos = n * 4 + 1
        downloads_dct[n + 1] = FileDownload(y_pos, win, urls[n])

    # Waits for all files to be downloaded before passing control of the terminal
    # to the user.
    all_downloaded = False
    while not all_downloaded:
        all_downloaded = True
        for key, file_download in downloads_dct.items():
            if file_download.get_status() != "downloaded":
                all_downloaded = False

    # Prevents the prompt from returning inside the curses window.
    win.addstr(7, 1, "-")

    # This solves the unresponsive prompt issue but hides the curses window if the screen buffer
    # is higher than the window size.
    # curses.endwin()

while input("\nEnter to continue: ") == "":
    files_downloader()

Solution

  • Perhaps you're using cygwin (and ncurses): ncurses (like any other curses implementation) changes the terminal I/O mode when it is running. The changes that you probably are seeing is that

    • input characters are not echoed
    • you have to type controlJ to end an input line, rather than just Enter
    • output is not flushed automatically at the end of each line

    It makes those changes to allow it to read single characters and also to use the terminal more efficiently.

    To change back to the terminal's normal I/O mode, you would use the endwin function. The reset_shell_mode function also would be useful.

    Further reading: