Search code examples
pythonmultithreadingcurses

Multithreading curses output in Python


I am trying to implement a simple spinner (using code adapted from this answer) below a progress bar for a long-running function.

[########         ] x%
/ Compressing filename

I have the compression and progress bar running in the main thread of my script and the spinner running in another thread, so it can actually spin while compression takes place. However, I am using curses for both the progress bar and the spinner, and both use curses.refresh()

Sometimes the terminal will randomly output gibberish, and I'm not sure why. I think it is due to the multi-threaded nature of the spinner, as when I disable the spinner the problem goes away.

Here is the pseudocode of the spinner:

def start(self):
  self.busy = True
  global stdscr 
  stdscr = curses.initscr()
  curses.noecho()
  curses.cbreak()
  threading.Thread(target=self.spinner_task).start()

def spinner_task(self):
  while self.busy:
    stdscr.addstr(1, 0, next(self.spinner_generator))
    time.sleep(self.delay)
    stdscr.refresh()

And here is the pseudocode for the progress bar:

progress_bar = "\r[{}] {:.0f}%".format("#" * block + " " * (bar_length - block), round(progress * 100, 0))
progress_file = " {} {}".format(s, filename)
stdscr.clrtoeol()
stdscr.addstr(1, 1, "                                                              ")
stdscr.clrtoeol()
stdscr.addstr(0, 0, progress_bar)
stdscr.addstr(1, 1, progress_file)
stdscr.refresh()

And called from main() like:

spinner.start()
for each file:
  update_progress_bar
  compress(file)
spinner.stop()

Why would the output sometimes become corrupted? Is it because of the separate threads? If so, any suggestions on a better way to design this?


Solution

  • If this is the only place where you're using the curses module, the best solution will be to stop using it.

    The only functionality of curses that you're really using here is its ability to clear the screen and move the cursor. This can easily be replicated by outputting the appropriate control sequences directly, e.g:

    sys.stdout.write("\x1b[f\x1b[J" + progress_bar + "\n" + progress_file)
    

    The \x1b[f sequence moves the cursor to 1,1, and \x1b[J clears all content from the cursor position to the end of the screen.

    No additional calls are needed to refresh the screen, or to reset it when you're done. You can output "\x1b[f\x1b[J" again to clear the screen if you want.

    This approach does, admittedly, assume that the user is using a VT100-compatible terminal. However, terminals which do not implement this standard are effectively extinct, so this is probably a safe assumption.