Python curses: Working with Windowed Content material


Python tutorials

Welcome again to the third – and remaining – installment in our sequence on tips on how to work with the curses library in Python to attract with textual content. Should you missed the primary two elements of this programming tutorial sequence – or should you want to reference the code contained inside them – you may learn them right here:

As soon as reviewed, let’s transfer on to the subsequent portion: tips on how to enhance home windows with borders and bins utilizing Python’s curses module.

Adorning Home windows With Borders and Containers

Home windows could be embellished utilizing customized values, in addition to a default “field” adornment in Python. This may be completed utilizing the window.field() and window.border(…) features. The Python code instance under creates a crimson 5×5 window after which alternates displaying and clearing the border on every key press:

# demo-window-border.py

import curses
import math
import sys

def predominant(argv):
  # BEGIN ncurses startup/initialization...
  # Initialize the curses object.
  stdscr = curses.initscr()

  # Don't echo keys again to the consumer.
  curses.noecho()

  # Non-blocking or cbreak mode... don't look forward to Enter key to be pressed.
  curses.cbreak()

  # Flip off blinking cursor
  curses.curs_set(False)

  # Allow shade if we will...
  if curses.has_colors():
    curses.start_color()

  # Elective - Allow the keypad. This additionally decodes multi-byte key sequences
  # stdscr.keypad(True)

  # END ncurses startup/initialization...

  caughtExceptions = ""
  attempt:
   # Create a 5x5 window within the heart of the terminal window, after which
   # alternate displaying a border and never on every key press.

   # We needn't know the place the approximate heart of the terminal
   # is, however we do want to make use of the curses terminal dimension constants to
   # calculate the X, Y coordinates of the place we will place the window in
   # order for it to be roughly centered.
   topMostY = math.ground((curses.LINES - 5)/2)
   leftMostX = math.ground((curses.COLS - 5)/2)

   # Place a caption on the backside left of the terminal indicating 
   # motion keys.
   stdscr.addstr (curses.LINES-1, 0, "Press Q to give up, every other key to alternate.")
   stdscr.refresh()
   
   # We're simply utilizing white on crimson for the window right here:
   curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_RED)

   index = 0
   finished = False
   whereas False == finished:
    # If we're on the primary iteration, let's skip straight to creating the window.
    if 0 != index:
     # Grabs a price from the keyboard with out Enter having to be pressed. 
     ch = stdscr.getch()
     # Must match on each upper-case or lower-case Q:
     if ch == ord('Q') or ch == ord('q'): 
      finished = True
    mainWindow = curses.newwin(5, 5, topMostY, leftMostX)
    mainWindow.bkgd(' ', curses.color_pair(1))
    if 0 == index % 2:
     mainWindow.field()
    else:
     # There is no solution to "unbox," so clean out the border as a substitute.
     mainWindow.border(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ')
    mainWindow.refresh()

    stdscr.addstr(0, 0, "Iteration [" + str(index) + "]")
    stdscr.refresh()
    index = 1 + index

  besides Exception as err:
   # Simply printing from right here won't work, as this system continues to be set to
   # use ncurses.
   # print ("Some error [" + str(err) + "] occurred.")
   caughtExceptions = str(err)

  # BEGIN ncurses shutdown/deinitialization...
  # Flip off cbreak mode...
  curses.nocbreak()

  # Flip echo again on.
  curses.echo()

  # Restore cursor blinking.
  curses.curs_set(True)

  # Flip off the keypad...
  # stdscr.keypad(False)

  # Restore Terminal to unique state.
  curses.endwin()

  # END ncurses shutdown/deinitialization...

  # Show Errors if any occurred:
  if "" != caughtExceptions:
   print ("Received error(s) [" + caughtExceptions + "]")

if __name__ == "__main__":
  predominant(sys.argv[1:])

This code was run over an SSH connection, so there may be an computerized clearing of the display upon its completion. The border “crops” the inside of the window, and any textual content that’s positioned throughout the window have to be adjusted accordingly. And, as the decision to the window.border(…) perform suggests, any character can be utilized for the border.

The code works by ready for a key to be pressed. If both Q or Shift+Q is pressed, the termination situation of the loop can be activated and this system will give up. Observe that, urgent the arrow keys might return key presses and skip iterations.

How one can Replace Content material in “Home windows” with Python curses

Simply as is the case with conventional graphical windowed applications, the textual content content material of a curses window could be modified. And, simply as is the case with graphical windowed applications, the outdated content material of the window have to be “blanked out” earlier than any new content material could be positioned within the window.

The Python code instance under demonstrates a digital clock that’s centered on the display. It makes use of Python lists to retailer units of characters which when displayed, appear like massive variations of digits.

A short notice: the code under will not be supposed to be essentially the most environment friendly technique of displaying a clock, slightly, it’s supposed to be a extra transportable demonstration of how curses home windows are up to date.

# demo-clock.py

# These checklist assignments could be finished on single strains, nevertheless it's a lot simpler to see what
# these values signify by doing it this fashion.
house = [
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     ",
"     "]

colon = [
"     ",
"     ",
"  :::  ",
"  :::  ",
"     ",
"     ",
"  :::  ",
"  :::  ",
"     ",
"     "]

forwardSlash = [
"     ",
"    //",
"   // ",
"   // ",
"  //  ",
"  //  ",
" //   ",
" //   ",
"//    ",
"     "]

number0 = [
" 000000 ",
" 00  00 ",
" 00  00 ",
" 00  00 ",
" 00  00 ",
" 00  00 ",
" 00  00 ",
" 00  00 ",
" 00  00 ",
" 000000 "]

number1 = [
"  11  ",
"  111  ",
" 1111  ",
"  11  ",
"  11  ",
"  11  ",
"  11  ",
"  11  ",
"  11  ",
" 111111 "]

number2 = [
" 222222 ",
" 22  22 ",
" 22  22 ",
"   22  ",
"  22  ",
"  22   ",
" 22   ",
" 22    ",
" 22    ",
" 22222222 "]

number3 = [
" 333333 ",
" 33  33 ",
" 33  33 ",
"    33 ",
"  3333 ",
"    33 ",
"    33 ",
" 33  33 ",
" 33  33 ",
" 333333 "]

number4 = [
"   44  ",
"  444  ",
"  4444  ",
" 44 44  ",
" 44 44  ",
"444444444 ",
"   44  ",
"   44  ",
"   44  ",
"   44  "]

number5 = [
" 55555555 ",
" 55    ",
" 55    ",
" 55    ",
" 55555555 ",
"    55 ",
"    55 ",
"    55 ",
"    55 ",
" 55555555 "]

number6 = [
" 666666 ",
" 66  66 ",
" 66    ",
" 66    ",
" 6666666 ",
" 66  66 ",
" 66  66 ",
" 66  66 ",
" 66  66 ",
" 666666 "]

number7 = [
" 77777777 ",
"    77 ",
"   77 ",
"   77  ",
"  77  ",
"  77   ",
" 77   ",
" 77    ",
" 77    ",
" 77    "]

number8 = [
" 888888 ",
" 88  88 ",
" 88  88 ",
" 88  88 ",
" 888888 ",
" 88  88 ",
" 88  88 ",
" 88  88 ",
" 88  88 ",
" 888888 "]

number9 = [
" 999999 ",
" 99  99 ",
" 99  99 ",
" 99  99 ",
" 999999 ",
"    99 ",
"    99 ",
"    99 ",
" 99  99 ",
" 999999 "]

import curses
import math
import sys
import datetime

def putChar(windowObj, inChar, inAttr = 0):
 #windowObj.field()
 #windowObj.addstr(inChar)
 # The logic under maps the traditional character enter to a listing which accommodates a "huge"
 # illustration of that character.
 charToPut = ""
 if '0' == inChar:
  charToPut = number0
 elif '1' == inChar:
  charToPut = number1
 elif '2' == inChar:
  charToPut = number2
 elif '3' == inChar:
  charToPut = number3
 elif '4' == inChar:
  charToPut = number4
 elif '5' == inChar:
  charToPut = number5
 elif '6' == inChar:
  charToPut = number6
 elif '7' == inChar:
  charToPut = number7
 elif '8' == inChar:
  charToPut = number8
 elif '9' == inChar:
  charToPut = number9
 elif ':' == inChar:
  charToPut = colon
 elif '/' == inChar:
  charToPut = forwardSlash
 elif ' ' == inChar:
  charToPut = house

 lineCount = 0
 # This loop will iterate every line within the window to show a "line" of the digit
 # to be displayed.
 for line in charToPut:
  # Attributes, or the bitwise combos of a number of attributes, are handed as-is
  # into addstr. Observe that not all attributes, or combos of attributes, will 
  # work with each terminal.
  windowObj.addstr(lineCount, 0, charToPut[lineCount], inAttr)
  lineCount = 1 + lineCount
 windowObj.refresh()

def predominant(argv):
  # Initialize the curses object.
  stdscr = curses.initscr()

  # Don't echo keys again to the consumer.
  curses.noecho()

  # Non-blocking or cbreak mode... don't look forward to Enter key to be pressed.
  curses.cbreak()

  # Flip off blinking cursor
  curses.curs_set(False)

  # Allow shade if we will...
  if curses.has_colors():
    curses.start_color()

  # Elective - Allow the keypad. This additionally decodes multi-byte key sequences
  # stdscr.keypad(True)

  caughtExceptions = ""
  attempt:
    # First issues first, make sure that we've sufficient room!
    if curses.COLS <= 88 or curses.LINES <= 11:
     increase Exception ("This terminal window is just too small.rn")
    currentDT = datetime.datetime.now()
    hour = currentDT.strftime("%H")
    min = currentDT.strftime("%M")
    sec = currentDT.strftime("%S")

    # Relying on how the ground values are calculated, an additional character for every
    # window could also be wanted. This code crashed when the home windows had been set to precisely
    # 10x10

    topMostY = math.ground((curses.LINES - 11)/2)
    leftMostX = math.ground((curses.COLS - 88)/2)

    # Observe that print statements don't work when utilizing ncurses. If you wish to write
    # to the terminal outdoors of a window, use the stdscr.addstr methodology and specify
    # the place the textual content will go. Then use the stdscr.refresh methodology to refresh the 
    # show.
    stdscr.addstr(curses.LINES-1, 0, "Press a key to give up.")
    stdscr.refresh()


    # Containers - Every field have to be 1 char larger than stuff put into it.
    hoursLeftWindow = curses.newwin(11, 11, topMostY,leftMostX)
    putChar(hoursLeftWindow, hour[0:1])
    hoursRightWindow = curses.newwin(11, 11, topMostY,leftMostX+11)
    putChar(hoursRightWindow, hour[-1])
    leftColonWindow = curses.newwin(11, 11, topMostY,leftMostX+22)
    putChar(leftColonWindow, ':', curses.A_BLINK | curses.A_BOLD)
    minutesLeftWindow = curses.newwin(11, 11, topMostY, leftMostX+33)
    putChar(minutesLeftWindow, min[0:1])
    minutesRightWindow = curses.newwin(11, 11, topMostY, leftMostX+44)
    putChar(minutesRightWindow, min[-1])
    rightColonWindow = curses.newwin(11, 11, topMostY, leftMostX+55)
    putChar(rightColonWindow, ':', curses.A_BLINK | curses.A_BOLD)
    leftSecondWindow = curses.newwin(11, 11, topMostY, leftMostX+66)
    putChar(leftSecondWindow, sec[0:1])
    rightSecondWindow = curses.newwin(11, 11, topMostY, leftMostX+77)
    putChar(rightSecondWindow, sec[-1])

    # One of many bins have to be non-blocking or we will by no means give up.
    hoursLeftWindow.nodelay(True)
    whereas True:
     c = hoursLeftWindow.getch()

     # In non-blocking mode, the getch methodology returns -1 besides when any secret is pressed.
     if -1 != c:
      break
     currentDT = datetime.datetime.now()
     currentDTUsec = currentDT.microsecond
     # Refreshing the clock "4ish" occasions a second could also be overkill, however doing
     # on each single loop iteration shoots lively CPU utilization up considerably.
     # Sadly, if we solely refresh as soon as a second it's potential to 
     # skip a second.

     # Nonetheless, such a restriction breaks performance in Home windows, so
     # for that atmosphere, this has to run on Each. Single. Iteration.
     if 0 == currentDTUsec % 250000 or sys.platform.startswith("win"):

      hour = currentDT.strftime("%H")
      min = currentDT.strftime("%M")
      sec = currentDT.strftime("%S")

      putChar(hoursLeftWindow, hour[0:1], curses.A_BOLD)
      putChar(hoursRightWindow, hour[-1], curses.A_BOLD)
      putChar(minutesLeftWindow, min[0:1], curses.A_BOLD)
      putChar(minutesRightWindow, min[-1], curses.A_BOLD)
      putChar(leftSecondWindow, sec[0:1], curses.A_BOLD)
      putChar(rightSecondWindow, sec[-1], curses.A_BOLD)
    # After breaking out of the loop, we have to clear up the show earlier than quitting.
    # The code under blanks out the subwindows.
    putChar(hoursLeftWindow, ' ')
    putChar(hoursRightWindow, ' ')
    putChar(leftColonWindow, ' ')
    putChar(minutesLeftWindow, ' ')
    putChar(minutesRightWindow, ' ')
    putChar(rightColonWindow, ' ')
    putChar(leftSecondWindow, ' ')
    putChar(rightSecondWindow, ' ')    
    
    # De-initialize the window objects.
    hoursLeftWindow = None
    hoursRightWindow = None
    leftColonWindow = None
    minutesLeftWindow = None
    minutesRightWindow = None
    rightColonWindow = None
    leftSecondWindow = None
    rightSecondWindow = None

  besides Exception as err:
   # Simply printing from right here won't work, as this system continues to be set to
   # use ncurses.
   # print ("Some error [" + str(err) + "] occurred.")
   caughtExceptions = str(err)

  # Finish of Program...
  # Flip off cbreak mode...
  curses.nocbreak()

  # Flip echo again on.
  curses.echo()

  # Restore cursor blinking.
  curses.curs_set(True)

  # Flip off the keypad...
  # stdscr.keypad(False)

  # Restore Terminal to unique state.
  curses.endwin()

  # Show Errors if any occurred:
  if "" != caughtExceptions:
   print ("Received error(s) [" + caughtExceptions + "]")

if __name__ == "__main__":
  predominant(sys.argv[1:])


Checking Window Dimension

Observe how the primary line throughout the attempt block within the predominant perform checks the dimensions of the terminal window and raises an exception ought to it not be sufficiently massive sufficient to show the clock. It is a demonstration of “preemptive” error dealing with, as if the person window objects are written to a display which is just too small, a really uninformative exception can be raised.

Cleansing Up Home windows with curses

The instance above forces a cleanup of the display for all 3 working environments. That is finished utilizing the putChar(…) perform to print a clean house character to every window object upon breaking out of the whereas loop.. The objects are then set to None. Cleansing up window objects on this method generally is a good apply when it isn’t potential to know all of the totally different terminal configurations that the code might be operating on, and having a clean display on exit provides these sorts of functions a cleaner look general.

CPU Utilization

Just like the earlier code instance, this too works as an “infinite” loop within the sense that it’s damaged by a situation that’s generated by urgent any key. Exhibiting two other ways to interrupt the loop is intentional, as some builders might lean in the direction of one methodology or one other. Observe that this code leads to extraordinarily excessive CPU utilization as a result of, when run inside a loop, Python will eat as a lot CPU time because it presumably can. Usually, the sleep(…) perform is used to pause execution, however within the case of implementing a clock, this will not be one of the best ways to cut back general CPU utilization. Apparently sufficient although, the CPU utilization, as reported by the Home windows Activity Supervisor for this course of is simply about 25%, in comparison with 100% in Linux.

One other fascinating remark about CPU utilization in Linux: even when simulating important CPU utilization by means of the stress utility, as per the command under:

$ stress -t 30 -c 16

the demo-clock.py script was nonetheless capable of run with out shedding the correct time.

Going Additional with Python curses

This three-part introduction solely barely scratches the floor of the Python curses module, however with this basis, the duty of making strong consumer interfaces for text-based Python functions turns into fairly doable, even for a novice developer. The one downsides are having to fret about how particular person terminal emulation implementations can influence the code, however that won’t be that important of an obstacle, and naturally, having to cope with the maths concerned in preserving window objects correctly sized and positioned.

The Python curses module does present mechanisms for “shifting” home windows (albeit not very effectively natively, however this may be mitigated), in addition to resizing home windows and even compensating for adjustments within the terminal window dimension! Even advanced text-based video games could be (and have been) applied utilizing the Python curses module, or its underlying ncurses C/C++ libraries.

The entire documentation for the ncurses module could be discovered at curses — Terminal dealing with for character-cell shows — Python 3.10.5 documentation. Because the Python curses module makes use of syntax that’s “shut sufficient” to the underlying ncurses C/C++ libraries, the guide pages for these libraries, in addition to reference assets for these libraries may also be consulted for extra info.

Comfortable “fake” Windowed Programming!

Learn extra Python programming tutorials and software program improvement ideas.

Leave a Comment