How to make a scrolling menu in python-curses

12.6k views Asked by At

There is a way to make a scrolling menu in python-curses? I have a list of records that I got from a query in sqlite3 and I have to show them in a box but they are more than the max number of rows: can I make a little menu to show them all without making curses crashing?

4

There are 4 answers

3
Alessio Ragno On BEST ANSWER

This code allows you to create a little menu in a box from a list of strings.
You can also use this code getting the list of strings from a sqlite query or from a csv file.
To edit the max number of rows of the menu you just have to edit max_row.
If you press enter the program will print the selected string value and its position.

from __future__ import division  #You don't need this in Python3
import curses
from math import *



screen = curses.initscr()
curses.noecho()
curses.cbreak()
curses.start_color()
screen.keypad( 1 )
curses.init_pair(1,curses.COLOR_BLACK, curses.COLOR_CYAN)
highlightText = curses.color_pair( 1 )
normalText = curses.A_NORMAL
screen.border( 0 )
curses.curs_set( 0 )
max_row = 10 #max number of rows
box = curses.newwin( max_row + 2, 64, 1, 1 )
box.box()


strings = [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "l", "m", "n" ] #list of strings
row_num = len( strings )

pages = int( ceil( row_num / max_row ) )
position = 1
page = 1
for i in range( 1, max_row + 1 ):
    if row_num == 0:
        box.addstr( 1, 1, "There aren't strings", highlightText )
    else:
        if (i == position):
            box.addstr( i, 2, str( i ) + " - " + strings[ i - 1 ], highlightText )
        else:
            box.addstr( i, 2, str( i ) + " - " + strings[ i - 1 ], normalText )
        if i == row_num:
            break

screen.refresh()
box.refresh()

x = screen.getch()
while x != 27:
    if x == curses.KEY_DOWN:
        if page == 1:
            if position < i:
                position = position + 1
            else:
                if pages > 1:
                    page = page + 1
                    position = 1 + ( max_row * ( page - 1 ) )
        elif page == pages:
            if position < row_num:
                position = position + 1
        else:
            if position < max_row + ( max_row * ( page - 1 ) ):
                position = position + 1
            else:
                page = page + 1
                position = 1 + ( max_row * ( page - 1 ) )
    if x == curses.KEY_UP:
        if page == 1:
            if position > 1:
                position = position - 1
        else:
            if position > ( 1 + ( max_row * ( page - 1 ) ) ):
                position = position - 1
            else:
                page = page - 1
                position = max_row + ( max_row * ( page - 1 ) )
    if x == curses.KEY_LEFT:
        if page > 1:
            page = page - 1
            position = 1 + ( max_row * ( page - 1 ) )

    if x == curses.KEY_RIGHT:
        if page < pages:
            page = page + 1
            position = ( 1 + ( max_row * ( page - 1 ) ) )
    if x == ord( "\n" ) and row_num != 0:
        screen.erase()
        screen.border( 0 )
        screen.addstr( 14, 3, "YOU HAVE PRESSED '" + strings[ position - 1 ] + "' ON POSITION " + str( position ) )

    box.erase()
    screen.border( 0 )
    box.border( 0 )

    for i in range( 1 + ( max_row * ( page - 1 ) ), max_row + 1 + ( max_row * ( page - 1 ) ) ):
        if row_num == 0:
            box.addstr( 1, 1, "There aren't strings",  highlightText )
        else:
            if ( i + ( max_row * ( page - 1 ) ) == position + ( max_row * ( page - 1 ) ) ):
                box.addstr( i - ( max_row * ( page - 1 ) ), 2, str( i ) + " - " + strings[ i - 1 ], highlightText )
            else:
                box.addstr( i - ( max_row * ( page - 1 ) ), 2, str( i ) + " - " + strings[ i - 1 ], normalText )
            if i == row_num:
                break



    screen.refresh()
    box.refresh()
    x = screen.getch()

curses.endwin()
exit()

Screenshot of the code

0
AudioBubble On

I used https://github.com/wong2/pick

>>> title = 'Please choose your favorite programming language (press SPACE to mark, ENTER to continue): '
>>> options = ['Java', 'JavaScript', 'Python', 'PHP', 'C++', 'Erlang', 'Haskell']
>>> pick(options, title, multi_select=True, min_selection_count=1)

It creates an ncurses-based picker that takes up the entire terminal window and lets you select multiple options (it'll scroll the options if they don't all fit on the page). After you chose stuff it returns the values and their indeces:

[('Java', 0), ('C++', 4)]
0
elig On

To make a scrollable widget that can scroll through text that is larger than a screenful you will need to use curses.newpad

You can find a simple example here: https://stackoverflow.com/a/2523020/9205341
And the Python 3/Python 2 docs there.

0
dotbit On

use this skeleton but add page-up / -down to display entries in groups of 10.

very crash-proof in python2 and python3 !


#!/usr/bin/env python2
# -*- coding: UTF-8 -*-
#kate: syntax Python ;

# a skeleton menu with python2 & py3                  https://paste.cutelyst.org/

# https://0bin.net/paste/yauebNkDNbwgZqwy#a2YfWpSTHh0RV-0Yfz3FIypudIU6oi4DWvV9EGXo1Pv

'''function 1 on F1 1 and NUM-1'''
def funkt1():
    return 1

def funkt2():
    return 2

def funkt3():
    return 3

def funkt4():
    return 4

def funkt5():
    return 5

def funkt6():
    return 6

def funkt7():
    return 7

def funkt8():
    return 8

def funkt9():
    return 9




import sys,os
import curses

global menuE
global e
global noofmes
global keychar
e       = 0
menuE   = 1   # active entry
noofmes = 10  # number of menu entries +1
keychar = " " # like 1 or 2 ...

class switch(object):
    value = None
    def __new__(class_, value):
        class_.value = value
        return True

def case(*args):
    return any((arg == switch.value for arg in args))


def draw_menu(stdscr):
    global menuE
    global e
    global noofmes
    global keychar
    k        = 0
    cursor_x = 0
    cursor_y = 0

    # Clear and refresh the screen for a blank canvas
    stdscr.clear()
    stdscr.refresh()

    # Start colors in curses
    curses.start_color()
    curses.init_pair(1, curses.COLOR_CYAN,  curses.COLOR_BLACK)
    curses.init_pair(2, curses.COLOR_RED,   curses.COLOR_BLACK)
    curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)

    # Loop where k is the last character pressed
    while (k != ord('q')):

        # Initialization
        stdscr.clear()
        height, width = stdscr.getmaxyx()

        ki              = int(k)
        try:    keychar = chr(k)
        except: keychar = 0
        callfunc        = False


        while switch(k):
            if case(49, 265, 360): # allows keys   1  or  NUM-1  or  F1     to be  Entry1    for  0..9
                menuE = 1
                break
            if case(50, 266     ): # 258   mouse u
                menuE = 2
                break
            if case(51, 267, 338):
                menuE = 3
                break
            if case(52, 268     ): #260
                menuE = 4
                break
            if case(53, 269, 69):
                menuE = 5
                break
            if case(54, 270     ): #261
                menuE = 6
                break
            if case(55, 271, 262):
                menuE = 7
                break
            if case(56, 272     ): #259   mouse d
                menuE = 8
                break
            if case(57, 273, 339):
                menuE = 9
                break
            if case(10, 32 , 83 ):   # SPACE  ,   ENTER  ,   83 mouse middle
                callfunc = True
                break
            pass
            break

        if callfunc:
            if   menuE == 1: e = funkt1()
            elif menuE == 2: e = funkt2()
            elif menuE == 3: e = funkt3()
            elif menuE == 4: e = funkt4()
            elif menuE == 5: e = funkt5()
            elif menuE == 6: e = funkt6()
            elif menuE == 7: e = funkt7()
            elif menuE == 8: e = funkt8()
            elif menuE == 9: e = funkt9()


        if k == curses.KEY_DOWN:
            cursor_y = cursor_y + 1
            menuE = (menuE + 1) % noofmes


        elif k == curses.KEY_UP:
            cursor_y = cursor_y - 1
            menuE = (menuE - 1) % noofmes

        elif k == curses.KEY_RIGHT:
            cursor_x = cursor_x + 1

        elif k == curses.KEY_LEFT:
            cursor_x = cursor_x - 1


        cursor_x = max(0, cursor_x)
        cursor_x = min(width-1, cursor_x)

        cursor_y = max(0, cursor_y)
        cursor_y = min(height-1, cursor_y)

        # Declaration of strings
        title        = "Python2 , py3  menu demo with lib ´curses´ "[:width-1]
        subtitle     = "because py2 will never die!"[:width-1]
        keystr       = str(e) + " <-- funkt  " + "Last key pressed: {}".format(k)[:width-1]
        statusbarstr = "Press 'q' to exit | STATUS BAR | Pos: {}, {}".format(cursor_x, cursor_y)
        if k == 0:
            keystr = "No key press detected..."[:width-1]

        # Centering calculations
        start_x_title    = int((  width // 2) - (len(title)    // 2) - len(title)    % 2)
        start_x_subtitle = int((  width // 2) - (len(subtitle) // 2) - len(subtitle) % 2)
        start_x_keystr   = int((  width // 2) - (len(keystr)   // 2) - len(keystr)   % 2)
        start_y          = int(( height // 2) -                   2)

        # Rendering some text
        whstr = "Width: {}, Height: {}".format(width, height)
        stdscr.addstr(0, 0, whstr, curses.color_pair(1))

        # Render status bar
        stdscr.attron(curses.color_pair(3))
        stdscr.addstr(height-1,  0, statusbarstr)
        stdscr.addstr(height-1, len(statusbarstr), " " * (width - len(statusbarstr) - 1))
        stdscr.attroff(curses.color_pair(3))

        # Turning on attributes for title
        stdscr.attron(curses.color_pair(2))
        stdscr.attron(curses.A_BOLD)

        # Rendering title
        stdscr.addstr(start_y -6, start_x_title, title)


        # Turning off attributes for title
        stdscr.attroff(curses.color_pair(2))
        stdscr.attroff(curses.A_BOLD)

        # Print rest of text
        stdscr.addstr( start_y - 4, start_x_subtitle, subtitle)

        #stdscr.addstr(start_y - 3, (width // 2) - 2, '-' * 4)
        stdscr.addstr( start_y - 2, start_x_keystr, keystr)


        # menu
        stdscr.addstr(start_y + 0,  start_x_subtitle, "~~~  Menu  ~~~")


        if menuE == 1 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 1,  start_x_subtitle, "1 Entry1")
        if menuE == 2 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 2,  start_x_subtitle, "2 Entry2")
        if menuE == 3 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 3 , start_x_subtitle, "3 Entry3")
        if menuE == 4 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 4 , start_x_subtitle, "4 Entry4")
        if menuE == 5 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 5 , start_x_subtitle, "5 Entry5")
        if menuE == 6 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 6 , start_x_subtitle, "6 Entry6")
        if menuE == 7 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 7 , start_x_subtitle, "7 Entry7")
        if menuE == 8 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 8 , start_x_subtitle, "8 Entry8")
        if menuE == 9 :
            stdscr.attron( curses.A_BOLD)
        else          :  stdscr.attroff(curses.A_BOLD)
        stdscr.addstr(start_y + 9 , start_x_subtitle, "9 Entry9")


        stdscr.move(cursor_y, cursor_x)

        # Refresh the screen
        stdscr.refresh()

        # Wait for next input
        k = stdscr.getch()

def main():
    curses.wrapper(draw_menu)

if __name__ == "__main__":
    main()