Skip to content

Instantly share code, notes, and snippets.

@abishur
Last active May 15, 2024 03:38
Show Gist options
  • Save abishur/2482046 to your computer and use it in GitHub Desktop.
Save abishur/2482046 to your computer and use it in GitHub Desktop.
A simple menu system using python for the Terminal (Framebufer)
#!/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
from time import sleep
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
# 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"
EXITMENU = "exitmenu"
menu_data = {
'title': "Program Launcher", 'type': MENU, 'subtitle': "Please select an option...",
'options':[
{ 'title': "XBMC", 'type': COMMAND, 'command': 'xbmc' },
{ 'title': "Emulation Station - Hit F4 to return to menu, Esc to exit game", 'type': COMMAND, 'command': 'emulationstation' },
{ 'title': "Ur-Quan Masters", 'type': COMMAND, 'command': 'uqm' },
{ 'title': "Dosbox Games", 'type': MENU, 'subtitle': "Please select an option...",
'options': [
{ 'title': "Midnight Rescue", 'type': COMMAND, 'command': 'dosbox /media/samba/Apps/dosbox/doswin/games/SSR/SSR.EXE -exit' },
{ 'title': "Outnumbered", 'type': COMMAND, 'command': 'dosbox /media/samba/Apps/dosbox/doswin/games/SSO/SSO.EXE -exit' },
{ 'title': "Treasure Mountain", 'type': COMMAND, 'command': 'dosbox /media/samba/Apps/dosbox/doswin/games/SST/SST.EXE -exit' },
]
},
{ 'title': "Pianobar", 'type': COMMAND, 'command': 'clear && pianobar' },
{ 'title': "Windows 3.1", 'type': COMMAND, 'command': 'dosbox /media/samba/Apps/dosbox/doswin/WINDOWS/WIN.COM -conf /home/pi/scripts/dosbox2.conf -exit' },
{ 'title': "Reboot", 'type': MENU, 'subtitle': "Select Yes to Reboot",
'options': [
{'title': "NO", 'type': EXITMENU, },
{'title': "", 'type': COMMAND, 'command': '' },
{'title': "", 'type': COMMAND, 'command': '' },
{'title': "", 'type': COMMAND, 'command': '' },
{'title': "YES", 'type': COMMAND, 'command': 'sudo shutdown -r -time now' },
{'title': "", 'type': COMMAND, 'command': '' },
{'title': "", 'type': COMMAND, 'command': '' },
{'title': "", 'type': COMMAND, 'command': '' },
]
},
]
}
# 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"
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 opt$
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('\n'):
if pos != oldpos:
oldpos = 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
# 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 == 259: # up arrow
if pos > 0:
pos += -1
else: pos = optioncount
# 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:
curses.def_prog_mode() # save curent curses environment
os.system('reset')
if menu['options'][getin]['title'] == 'Pianobar':
os.system('amixer cset numid=3 1') # Sets audio output on the pi to 3.5mm headphone jack
screen.clear() #clears previous screen
os.system(menu['options'][getin]['command']) # run the command
screen.clear() #clears previous screen on key press and updates display based on pos
curses.reset_prog_mode() # reset to 'current' curses environment
curses.curs_set(1) # reset doesn't do this right
curses.curs_set(0)
os.system('amixer cset numid=3 2') # Sets audio output on the pi back to HDMI
elif menu['options'][getin]['type'] == MENU:
screen.clear() #clears previous screen on key press and updates display based on pos
processmenu(menu['options'][getin], menu) # display the submenu
screen.clear() #clears previous screen on key press and updates display based on pos
elif menu['options'][getin]['type'] == EXITMENU:
exitmenu = True
# Main program
processmenu(menu_data)
curses.endwin() #VITAL! This closes out the menu system and returns you to the bash prompt.
os.system('clear')
@PhillyNJ
Copy link

Perfect - Needed a menu for my Mame Arcade. Thanks

@chuwy
Copy link

chuwy commented Nov 19, 2014

Omg. I don't know what it is, but I accidentaly open this gist and following code solved problem I struggling for a whole week.

curses.curs_set(1)
curses.curs_set(0)

@ebelliveau
Copy link

Just a little suggestion for a patch:

You've got a very rare condition whereby the menu system will fail to draw if screen.getch() returns an un-ordinable string of length >1. This fixes it. I think it's got something to do with internationalization as the thing seems to break when my keyboard flips itself to French modeÉ

    # What is user input?
    if x >= unichr(1) and x <= unichr(int(optioncount+1)):
      pos = x - unichr(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 == 259: # up arrow
      if pos > 0:
        pos += -1
      else: pos = optioncount

@pablogsal
Copy link

In order to avoid strange behavior of the terminal when something raises an exception inside the code I suggest changing the main menu to:

# Main program
try:
    processmenu(menu_data)
except Exception as exception:
    curses.endwin() #VITAL! This closes out the menu system and returns you to the bash prompt.
    os.system('clear')
    traceback.print_exc()
    sys.exit(1)

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

After, of course:

import traceback, sys

This prevents the terminal to become unresponsive if something happens and the user still have his trace back.

@etkirsch
Copy link

etkirsch commented Nov 9, 2015

If anyone wants something similar to this that abstracts everything to its own class, I took some time to refactor this a bit.

https://gist.github.com/etkirsch/53505478f53aeeac24a5

@idltknow
Copy link

idltknow commented Jan 5, 2016

Hello,

I'm trying to create sub-menu with 12 entries. First, the result was a strange behavior so I added the code from pablogsal to raise an exception.

Now, I get this error message when I try to get into this sub-menu:

Traceback (most recent call last):
File "./myscript.py", line 174, in <module>
processmenu(menu_data)
File "./myscript.py", line 150, in processmenu
getin = runmenu(menu, parent)
File "./myscript.py", line 131, in runmenu
if x >= ord('1') and x <= ord(str(optioncount+1)):
TypeError: ord() expected a character, but string of length 2 found

I see what the problem is but I can't find any solution.

Anyone could help me with that?

Thanks in advance.

@Fabian1976
Copy link

Hello idItknow, look at 3 posts before your post (ebelliveau commented on Dec 5, 2014). It has the solution to your problem.

I'm facing a different problem. My menu's can be larger then 1 screen so it crashes with this error:
_curses.error: addstr() returned ERR

It is because curses tries to draw beyond the boundries of the screen. I'm still searching for a proper solution that also scrolls to the content.

@pmbarrett314
Copy link

If anyone is interested, I used this in a couple of classes and decided I'd like to take it a bit further, so I made it into a library that abstracts it into a bunch of different classes. This makes it a bit more extendable and flexible, and also easier to use. Check it out on github. Or see the docs here. Or pip install curses-menu. And let me know if you have feedback!

@AppelonD
Copy link

AppelonD commented Apr 8, 2016

When Launched from a Windows putty it don't draw the borders correctly, it replaces it with x, y
Why is this, and how can I get putty to display the boarders correctly ?

@mrosile
Copy link

mrosile commented Jun 29, 2016

@AppelonD For your PuTTY connection settings, go to Window, then Translation. Change the 'Remote character set' to 'ISO-8859-1:1998 (Latin-1, West Europe)'

@SngLol
Copy link

SngLol commented Mar 16, 2017

I have a similar but simpler cross-platform menu creator class! And it has a unique interactive design!
It lets you go though the alternatives with an arrow using the arrow keys to move up and down!
If you're interested you can check it out here:
https://github.com/SngLol/PythonMenuSetup

@jamieduk
Copy link

jamieduk commented Jan 31, 2023

what version of python is #!/usr/bin/env python exaclty? im getting error

Traceback (most recent call last):
File "/home/jay/Documents/Scripts/Menus/Python/new_menu.py", line 31, in
import commands
ModuleNotFoundError: No module named 'commands'

works fine on last mint but mint 21 nope!!!

dw i fixed it with this command

pip install virtualenv

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment