Python Curses – Custom Menu

Continuing on the same project as the previous post, I came to wanting to make a custom menu with curses in Python.  Realizing that there are functions to create menus in curses already, I wanted to build this fro m the bottom up.  The concept was to produce a menu that would highlight the selection change on the arrow keys or direct input, and then on a press of the Enter key the menu would return that selection.

Now, while writing this, you’ll see in my code that I took probable the least efficient way of building this menu, but it helps in making itself explanatory for the person learning curses.  As for the context of this coding, I built it as a function in my XFDL viewer, so there are 5 options in the menu.  I used win.keypad(0) to enable the use of the arrow keys but for some reason, the curses.KEY_UP was not being detected so the arrow key up and arrow key down are 259 and 258, respectively.  This does work though, I also have the menu catch numbers 1-5 and set the highlighted line accordingly.

def menu():
    curses.init_pair(1,curses.COLOR_RED, curses.COLOR_WHITE)
    screen.keypad(1)
    pos = 1
    x = None
    # I'm going to be lazy and save some typing here.
    h = curses.color_pair(1)
    n = curses.A_NORMAL
    while x != ord('\n'):
        # Gotta reset the screen from the root or lose the border, window, etc.
        screen.clear()
        screen.border(0)
        screen.addstr(2,2, "XFDL VIEWER", curses.A_STANDOUT)
        screen.addstr(4,2, "Please select an option...", curses.A_BOLD)
        # Detect what is highlighted by the 'pos' variable.
        if pos == 1:
            screen.addstr(5,4, "1 - XFDL -> XML",h)
        else:
            screen.addstr(5,4, "1 - XFDL -> XML",n)
        if pos == 2:
            screen.addstr(6,4, "2 - XML  -> XFDL",h)
        else:
            screen.addstr(6,4, "2 - XML  -> XFDL",n)
        if pos == 3:
            screen.addstr(7,4, "3 - Show XML",h)
        else:
            screen.addstr(7,4, "3 - Show XML",n)
        if pos == 4:
            screen.addstr(8,4, "4 - Exit",h)
        else:
            screen.addstr(8,4, "4 - Exit",n)
        if pos == 5:
            screen.addstr(9,4, "5 - DEBUG", h)
        else:
            screen.addstr(9,4, "5 - DEBUG", n)
        screen.refresh()
        x = screen.getch()
        # Is 'x' 1-5 or arrow up, arrow down?
        if x == ord('1'):
            pos = 1
        elif x == ord('2'):
            pos = 2
        elif x == ord('3'):
            pos = 3
        elif x == ord('4'):
            pos = 4
        elif x == ord('5'):
            pos = 5
        # It was a pain in the ass trying to get the arrows working.
        elif x == 258:
            if pos < 5:
                pos += 1
            else:
                pos = 1
        # Since the curses.KEY_* did not work, I used the raw return value.
        elif x == 259:
            if pos > 1:
                pos += -1
            else:
                pos = 5
        elif x != ord('\n'):
            curses.flash()
            # show_error() is my custom function for displaying a message:
            # show_error(str:message, int:line#, int:seconds_to_display)
            show_error('Invalid Key',11,1)

    return ord(str(pos))

I’ve highlighted the lines pertaining to my work around for the key pad.  This function will return the menu option and then that is processed for a reaction.  Reminder: the ‘screen’ object for my curses window is a global variable. I’m quite thrilled at the simplicity of this and the curses library, although I am disappointed in the lack of tutorials on the web deeper than typical ‘Hello World’ tutorials, but I hope these posts go to help others exploring this library!

Here’s a nice picture of the library in action:

Customize menu in action...
Be sure to comment on these tutorials and let me know if there is more detail needed or if they are helpful!!

10 thoughts on “Python Curses – Custom Menu”

  1. Dude, thank you sooooooo much… like yourself, I found almost no clear examples of curses implementation for python (I found the basic tutorial to be… neither basic nor particularly instructive). I think the entire world thinks that peeps like us (who want basic terminal gui’s for menu-based applications) considers us to be totally insane throwbacks who should just use tkinter… tee hee… the terminal is so much more efficacious. Anyway, I did some monkeying with your code (quick and dirty and probably wrong and redundant) to get it working in python 3.1 (for which NOTHING I’ve tried over the last week in regards to curses/snack/etc. has worked… very frustrating, which is what grudgingly forced me back to curses). Anyway. They’re dumb changes, but they make it work…
    #!/usr/bin/env python3
    import curses
    screen=curses.initscr()

    def menu():
    curses.initscr()
    curses.start_color()

    #and then, of course, I actually called the menu at the end… I know not pythonic but blah… I wanted to see it work! 🙂
    Regardless, thanks again for helping me get a handle on this dark little enigmatic corner of the lingo. Have you seen the charming python tutorial for curses? It’s very very old and relies on some crazy modules that he created himself and that I’ve had a hard time finding, and I’ve been unable to replicate his turbo-vision-esque layout. Hopefully, now that you’ve given me a concrete place to start, I can begin attempting that. Thanks so much.

  2. Yeah, I agree that there just has been nothing that good for curses in python on the web, or really even using it in C. Unfortunately, many people perfer the fancy visuals they can get through a UI library like WX. I do use that for apps I want for other people but for myself or other Linux users, I prefer curses for most apps as I’m using a terminal 90% of the time. When doing kernel development, I’m not even logged into an X environment and am using the console a lot of the time. Vim (a great curses app) is all I need for coding and command line tools are what I use to compile and test. I had never heard of snack, I’ll have to look that up. If you have any good links for that, let me kno!

  3. Sorry, i’m a totally newbie. I like very much this menu, but i’m not able to run ‘as is’. What i need to add to your code in order to start the menu?

    thanks,

    Steve,

  4. Thanks for the nice, easy to understand write-up! The demo works quite well.

    I did notice that the terminal flashes whenever a menu item is highlighted so I tried commenting out the screen.clear() line which improved performance. Obviously this could be problematic if the textual content of the menu item changes. However, this could be compensated for, perhaps by writing a correctly sized string of spaces to screen under certain conditions. I plan to write a class based on this code which manages the menu items. A modification to the menu item text can be done under controlled conditions which would trigger a proper clear and redraw of the effected cells. That way I can keep from clearing the entire terminal each time something in the menu changes.

  5. Hi,
    i figure out that u can easly use the next code
    define:
    up = curses.KEY_UP
    down = curses.KEY_DOWN

    if x == down:
    if position 1:
    position += -1
    else:
    position = 5

    instead yours
    # Is ‘x’ 1-5 or arrow up, arrow down?
    if x == ord(‘1’):
    pos = 1
    elif x == ord(‘2’):
    pos = 2
    elif x == ord(‘3’):
    pos = 3
    elif x == ord(‘4’):
    pos = 4
    elif x == ord(‘5’):
    pos = 5
    # It was a pain in the ass trying to get the arrows working.
    elif x == 258:
    if pos 1:
    pos += -1
    else:
    pos = 5
    elif x != ord(‘\n’):
    curses.flash()
    # show_error() is my custom function for displaying a message:
    # show_error(str:message, int:line#, int:seconds_to_display)
    show_error(‘Invalid Key’,11,1)

    It works perfectly.
    Have a nice day.

    1. Comment system cuts my comment.
      Here is a code:

      up = curses.KEY_UP
      down = curses.KEY_DOWN

      if x == down:
      if position 1:
      position += -1
      else:
      position = 5

  6. Hi everyone,
    To get the code working, add the following to a .py file and run it with python:

    import curses

    def menu():
    #

    if __name__ == ‘__main__’:
    menu()

  7. Hello! I’m trying to get this code (see code one) to be changed to this (see code 2).

    Code 1:

    pimame_files/menu.py

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # Topmenu and the submenus are based of the example found at this location http://blog.skeltonnetworks.com/2010/03/python-curses-custom-menu/
    # The rest of the work was done by Matthew Bennett and he requests you keep these two mentions when you reuse the code 🙂
    # Basic code refactoring by Andrew Scheller

    import subprocess
    import curses, os #curses is the interface for capturing key presses on the menu, os launches the files
    screen = curses.initscr() #initializes a new window for capturing key presses
    curses.noecho() # Disables automatic echoing of key presses (prevents program from input each key twice)
    curses.cbreak() # Disables line buffering (runs each key as it is pressed rather than waiting for the return key to pressed)
    curses.start_color() # Lets you use colors when highlighting selected menu option
    screen.keypad(1) # Capture input from keypad

    wlan = subprocess.check_output(“/sbin/ifconfig wlan0 | grep ‘inet addr:’ | cut -d: -f2 | awk ‘{ print $1}’ “, shell=True)
    ether = subprocess.check_output(“/sbin/ifconfig eth0 | grep ‘inet addr:’ | cut -d: -f2 | awk ‘{ print $1}’ “, shell=True)

    myip = ”
    if wlan != ”:
    myip += wlan
    if ether != ”:
    myip += ‘ ‘ + ether

    if myip != ”:
    myip = “Your IP is: ” + myip + ” – ”

    # Change this to use different colors when highlighting
    curses.init_pair(1,curses.COLOR_BLACK, curses.COLOR_WHITE) # Sets up color pair #1, it does black text with white background
    h = curses.color_pair(1) #h is the coloring for a highlighted menu option
    n = curses.A_NORMAL #n is the coloring for a non highlighted menu option

    MENU = “menu”
    COMMAND = “command”

    menu_data = {
    ‘title’: “PiMAME Menu (v0.7.10)”, ‘type’: MENU, ‘subtitle’: “Please select an option…”,
    ‘options’: [
    { ‘title’: “Arcade”, ‘type’: MENU, ‘subtitle’: “Arcade Emulators”,
    ‘options’: [
    { ‘title’: “AdvanceMAME”, ‘type’: COMMAND, ‘command’: ‘advmenu’ },
    { ‘title’: “Neo Geo (GNGeo)”, ‘type’: COMMAND, ‘command’: ‘gngeo -i roms/’ },
    { ‘title’: “MAME4All”, ‘type’: COMMAND, ‘command’: ‘/home/pi/emulators/mame4all-pi/mame’ },
    { ‘title’: “FBA (CPS1, CPS2, Neo Geo)”, ‘type’: COMMAND, ‘command’: ‘/home/pi/emulators/fba/fbacapex’ },
    ]
    },
    { ‘title’: “Consoles”, ‘type’: MENU, ‘subtitle’: “Console Emulators”,
    ‘options’: [
    { ‘title’: “PlayStation 1 (PCSX_ReARMed)”, ‘type’: COMMAND, ‘command’: ‘/home/pi/emulators/pcsx_rearmed/pcsx’ },
    { ‘title’: “Genesis (DGen)”, ‘type’: COMMAND, ‘command’: ‘advmenu -cfg advmenu-dgen.rc’ },
    { ‘title’: “Nintendo Emulators”, ‘type’: MENU, ‘subtitle’: ‘Nintendo Emulators’,
    ‘options’: [
    { ‘title’: “SNES (PiSNES / SNES9x Advmenu)”, ‘type’: COMMAND, ‘command’: ‘advmenu -cfg advmenu-snes.rc’ },
    { ‘title’: “NES (AdvanceMESS)”, ‘type’: COMMAND, ‘command’: ‘advmenu -cfg advmenu-nes.rc’ },
    { ‘title’: “Gameboy (Gearboy Advmenu)”, ‘type’: COMMAND, ‘command’: ‘advmenu -cfg advmenu-gameboy.rc’ },
    { ‘title’: “Gameboy Advance (gpsp)”, ‘type’: COMMAND, ‘command’: ‘/home/pi/emulators/gpsp/gpsp’ },
    { ‘title’: “N64 WARNING ALPHA!”, ‘type’: COMMAND, ‘command’: ‘advmenu -cfg advmenu-mupen.rc’},
    ]
    },
    { ‘title’: “Atari 2600 (Stella)”, ‘type’: COMMAND, ‘command’: ‘stella’ },
    { ‘title’: “Commodore 64 (VICE)”, ‘type’: COMMAND, ‘command’: ‘x64’ },
    ]
    },
    { ‘title’: “CaveStory (NXEngine)”, ‘type’: COMMAND, ‘command’: ‘/home/pi/emulators/cs.sh’ },
    { ‘title’: “ScummVM”, ‘type’: COMMAND, ‘command’: ‘scummvm’ },
    { ‘title’: “Tools”, ‘type’: MENU, ‘subtitle’: myip,
    ‘options’: [
    { ‘title’: “Install PIP (http://pip.sheacob.com/about.html)”, ‘type’: COMMAND, ‘command’: ‘sudo /home/pi/pimame_files/pipinstall.py’ },
    { ‘title’: “Remove PIP”, ‘type’: COMMAND, ‘command’: ‘sudo /home/pi/pimame_files/pipinstall.py -r’ },
    { ‘title’: “raspi-config”, ‘type’: COMMAND, ‘command’: ‘sudo raspi-config’ },
    { ‘title’: “Reboot”, ‘type’: COMMAND, ‘command’: ‘sudo reboot’ },
    { ‘title’: “Shutdown”, ‘type’: COMMAND, ‘command’: ‘sudo poweroff’ },
    ]
    },
    ]
    }

    # This function displays the appropriate menu and returns the option selected
    def runmenu(menu, parent):

    # work out what text to display as the last menu option
    if parent is None:
    lastoption = “Exit (Return to Command Line)”
    else:
    lastoption = “Return to %s menu” % parent[‘title’]

    optioncount = len(menu[‘options’]) # how many options in this menu

    pos=0 #pos is the zero-based index of the hightlighted menu option. Every time runmenu is called, position returns to 0, when runmenu ends the position is returned and tells the program what option has been selected
    oldpos=None # used to prevent the screen being redrawn every time
    x = None #control for while loop, let’s you scroll through options until return key is pressed then returns pos to program

    # Loop until return key is pressed

    while x !=ord(‘c’):
    if pos != oldpos:
    oldpos = pos
    screen.clear() #clears previous screen on key press and updates display based on pos
    screen.border(0)
    screen.addstr(2,2, menu[‘title’], curses.A_STANDOUT) # Title for this menu
    screen.addstr(4,2, menu[‘subtitle’], curses.A_BOLD) #Subtitle for this menu

    # Display all the menu items, showing the ‘pos’ item highlighted
    for index in range(optioncount):
    textstyle = n
    if pos==index:
    textstyle = h
    screen.addstr(5+index,4, “%d – %s” % (index+1, menu[‘options’][index][‘title’]), textstyle)
    # Now display Exit/Return at bottom of menu
    textstyle = n
    if pos==optioncount:
    textstyle = h
    screen.addstr(5+optioncount,4, “%d – %s” % (optioncount+1, lastoption), textstyle)
    screen.refresh()
    # finished updating screen

    x = screen.getch() # Gets user input
    if x == ord(‘\n’):
    x = ord(‘c’)

    # What is user input?
    if x >= ord(‘1’) and x <= ord(str(optioncount+1)):
    pos = x – ord('0') – 1 # convert keypress back to a number, then subtract 1 to get index
    elif x == 258: # down arrow
    if pos < optioncount:
    pos += 1
    else: pos = 0
    elif x == 8: # down arrow
    if pos 0:
    pos += -1
    else: pos = optioncount
    elif x == 259: # up arrow
    if pos > 0:
    pos += -1
    else: pos = optioncount
    elif x != ord(‘\n’):
    curses.flash()

    # return index of the selected item
    return pos

    # This function calls showmenu and then acts on the selected item
    def processmenu(menu, parent=None):
    optioncount = len(menu[‘options’])
    exitmenu = False
    while not exitmenu: #Loop until the user exits the menu
    getin = runmenu(menu, parent)
    if getin == optioncount:
    exitmenu = True
    elif menu[‘options’][getin][‘type’] == COMMAND:
    os.system(menu[‘options’][getin][‘command’]) # run the command
    elif menu[‘options’][getin][‘type’] == MENU:
    processmenu(menu[‘options’][getin], menu) # display the submenu

    # Main program
    processmenu(menu_data)
    curses.endwin() #VITAL! This closes out the menu system and returns you to the bash prompt.
    Desktop version

    Code 2:

    #!/usr/bin/env python

    # -*- coding: utf-8 -*-

    # Topmenu and the submenus are based of the example found at this location http://blog.skeltonnetworks.com/2010/03/python-curses-custom-menu/

    # The rest of the work was done by Matthew Bennett and he requests you keep these two mentions when you reuse the code 🙂

    # Basic code refactoring by Andrew Scheller

    import subprocess

    import curses, os #curses is the interface for capturing key presses on the menu, os launches the files

    screen = curses.initscr() #initializes a new window for capturing key presses

    curses.noecho() # Disables automatic echoing of key presses (prevents program from input each key twice)

    curses.cbreak() # Disables line buffering (runs each key as it is pressed rather than waiting for the return key to pressed)

    curses.start_color() # Lets you use colors when highlighting selected menu option

    screen.keypad(1) # Capture input from keypad

    wlan = subprocess.check_output(“/sbin/ifconfig wlan0 | grep ‘inet addr:’ | cut -d: -f2 | awk ‘{ print $1}’ “, shell=True)

    ether = subprocess.check_output(“/sbin/ifconfig eth0 | grep ‘inet addr:’ | cut -d: -f2 | awk ‘{ print $1}’ “, shell=True)

    myip = ”

    if wlan != ”:

    myip += wlan

    if ether != ”:

    myip += ‘ ‘ + ether

    if myip != ”:

    myip = “Your IP is: ” + myip + ” – ”

    # Change this to use different colors when highlighting

    curses.init_pair(1,curses.COLOR_BLACK, curses.COLOR_WHITE) # Sets up color pair #1, it does black text with white background

    h = curses.color_pair(1) #h is the coloring for a highlighted menu option

    n = curses.A_NORMAL #n is the coloring for a non highlighted menu option

    MENU = “menu”

    COMMAND = “command”

    menu_data = {

    ‘title’: “Skangster76 valikko (v0.1.0)”, ‘type’: MENU, ‘subtitle’: “Valitse…”,

    ‘options’: [

    { ‘title’: “Kolikkopelit”, ‘type’: MENU, ‘subtitle’: “Kolikkopeli emulaattorit”,

    ‘options’: [

    { ‘title’: “AdvanceMAME”, ‘type’: COMMAND, ‘command’: ‘advmenu’ },

    { ‘title’: “MAME4All”, ‘type’: COMMAND, ‘command’: ‘/home/pi/emulators/mame4all-pi/mame’ },

    ]

    },

    { ‘title’: “Konsolit”, ‘type’: MENU, ‘subtitle’: “Konsoli emulaattorit”,

    ‘options’: [

    { ‘title’: “Nintendo Emulaattorit”, ‘type’: MENU, ‘subtitle’: ‘Nintendo Emulaattorit’,

    ‘options’: [

    { ‘title’: “NES (AdvanceMESS)”, ‘type’: COMMAND, ‘command’: ‘advmenu -cfg advmenu-nes.rc’ },

    { ‘title’: “Gameboy (Gearboy Advmenu)”, ‘type’: COMMAND, ‘command’: ‘advmenu -cfg advmenu-gameboy.rc’ },

    ]

    },

    ]

    },

    { ‘title’: “Työkalut”, ‘type’: MENU, ‘subtitle’: myip,

    ‘options’: [

    { ‘title’: “raspi-config”, ‘type’: COMMAND, ‘command’: ‘sudo raspi-config’ },

    { ‘title’: “Buuttaa”, ‘type’: COMMAND, ‘command’: ‘sudo reboot’ },

    { ‘title’: “Sammuta”, ‘type’: COMMAND, ‘command’: ‘sudo poweroff’ },

    ]

    },

    ]

    }

    # This function displays the appropriate menu and returns the option selected

    def runmenu(menu, parent):

    # work out what text to display as the last menu option

    if parent is None:

    lastoption = “Poistu (Palaa komentoriviin)”

    else:

    lastoption = “Palaa %s ” % parent[‘title’]

    optioncount = len(menu[‘options’]) # how many options in this menu

    pos=0 #pos is the zero-based index of the hightlighted menu option. Every time runmenu is called, position returns to 0, when runmenu ends the position is returned and tells the program what option has been selected

    oldpos=None # used to prevent the screen being redrawn every time

    x = None #control for while loop, let’s you scroll through options until return key is pressed then returns pos to program

    # Loop until return key is pressed

    while x !=ord(‘c’):

    if pos != oldpos:

    oldpos = pos

    screen.clear() #clears previous screen on key press and updates display based on pos

    screen.border(0)

    screen.addstr(2,2, menu[‘title’], curses.A_STANDOUT) # Title for this menu

    screen.addstr(4,2, menu[‘subtitle’], curses.A_BOLD) #Subtitle for this menu

    # Display all the menu items, showing the ‘pos’ item highlighted

    for index in range(optioncount):

    textstyle = n

    if pos==index:

    textstyle = h

    screen.addstr(5+index,4, “%d – %s” % (index+1, menu[‘options’][index][‘title’]), textstyle)

    # Now display Exit/Return at bottom of menu

    textstyle = n

    if pos==optioncount:

    textstyle = h

    screen.addstr(5+optioncount,4, “%d – %s” % (optioncount+1, lastoption), textstyle)

    screen.refresh()

    # finished updating screen

    x = screen.getch() # Gets user input

    if x == ord(‘\n’):

    x = ord(‘c’)

    # What is user input?

    if x >= ord(‘1’) and x <= ord(str(optioncount+1)):

    pos = x – ord('0') – 1 # convert keypress back to a number, then subtract 1 to get index

    elif x == 258: # down arrow

    if pos < optioncount:

    pos += 1

    else: pos = 0

    elif x == 8: # down arrow

    if pos 0:

    pos += -1

    else: pos = optioncount

    elif x == 259: # up arrow

    if pos > 0:

    pos += -1

    else: pos = optioncount

    elif x != ord(‘\n’):

    curses.flash()

    # return index of the selected item

    return pos

    # This function calls showmenu and then acts on the selected item

    def processmenu(menu, parent=None):

    optioncount = len(menu[‘options’])

    exitmenu = False

    while not exitmenu: #Loop until the user exits the menu

    getin = runmenu(menu, parent)

    if getin == optioncount:

    exitmenu = True

    elif menu[‘options’][getin][‘type’] == COMMAND:

    os.system(menu[‘options’][getin][‘command’]) # run the command

    elif menu[‘options’][getin][‘type’] == MENU:

    processmenu(menu[‘options’][getin], menu) # display the submenu

    # Main program

    processmenu(menu_data)

    curses.endwin() #VITAL! This closes out the menu system and returns you to the bash prompt.

    As you might have guessed i have no idea what i’m doing :). Every little help would be appriciated.

Leave a Reply

Your email address will not be published. Required fields are marked *