Menu navigable en Python avec ncurses

Le but de ce billet est de créer un petit menu navigable en Python avec la bibliothèque ncurses. Ce billet vient dans la ligné de Déplacer une fenêtre dans une console avec Python et ncurses, il est préférable de le lire avant si ce n’est déjà fait, comme ce billet contiendra moins d’explication.

Ce que l’on veut

Deux scripts sont proposé, le second en est une extention.

  • Un menu complet, dans lequel nous pouvons nous déplacer à l’aide de flèche, et sélectionner l’un des éléments. L’élément du menu sera mis en surbrillance en choisissant une couleur différente (ici, il sera rouge). Il faudra s’assurer qu’on ne dépasse pas les limites du menu.
  • Au lieu d’afficher tout le menu, seul quelques éléments seront affiché, mais la liste doit être navigable entièrement, on s’attend donc à voir apparaitre les éléments qui n’était pas affiché tout en se déplacent dans la liste. La navigation serra naturel.

Menu entièrement affiché

#!/usr/bin/python
# -*- coding: utf-8 -*-

import curses
import curses.wrapper


class Menu(object):

    menu = [
        'item 0', 'item 1', 'item 2', 'item 3',
        'item 4', 'item 5', 'item 6', 'item 7',
    ]

    def __init__(self, scr):
        self.scr = scr
        self.init_curses()
        self.item = { 'current': 0 }
        self.draw_menu()
        self.handle_keybinding()

    def init_curses(self):
        curses.curs_set(0)
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_RED, -1)

    def draw_menu(self):
        i = 2
        for element in self.menu:
            if element == self.get_current_item():
                self.scr.addstr(i, 2, element, curses.color_pair(1))
            else:
                self.scr.addstr(i, 2, element)
            i += 1
        self.scr.refresh()

    def get_current_item(self):
        return self.menu[self.item['current']]

    def navigate_up(self):
        if self.item['current'] > 0:
            self.item['current'] -= 1

    def navigate_down(self):
        if self.item['current'] < len(self.menu) -1:
            self.item['current'] += 1

    def show_result(self):
        win = self.scr.subwin(5, 40, 2, 10)
        win.border(0)
        win.addstr(2, 2, 'Vous avez selectionné %s'
                   % self.menu[self.item['current']])
        win.refresh()
        win.getch()
        win.erase()

    def handle_keybinding(self):
        while True:
            ch = self.scr.getch()
            if ch == curses.KEY_UP:
                self.navigate_up()
            elif ch == curses.KEY_DOWN:
                self.navigate_down()
            elif ch == curses.KEY_RIGHT:
                self.show_result()
            elif ch == ord('q'):
                break
            self.draw_menu()

if __name__ == '__main__':
    curses.wrapper(Menu)

Il n’y a pas énormément besoin d’explication, si vous avez bien suivis l’autre billet, celui-ci doit aller tout seul.

Le choix de self.item = { 'current': 0 } est surtout du à la suite du problème, il n’est effectivement pas très justifié ici de prendre un dictionnaire pour un seul élément.

Menu déroulant

#!/usr/bin/python
# -*- coding: utf-8 -*-

import curses
import curses.wrapper


class Menu(object):

    menu = [
        'item 0', 'item 1', 'item 2', 'item 3',
        'item 4', 'item 5', 'item 6', 'item 7',
    ]

    def __init__(self, scr):
        self.scr = scr
        self.init_curses()
        self.item = {
                'current': 0,
                'first':   0,
                'show':    5,
              }
        self.draw_menu()
        self.handle_keybinding()

    def init_curses(self):
        curses.curs_set(0)
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_RED, -1)

    def draw_menu(self):

        first = self.item['first']
        last = self.item['first'] + self.item['show']
        menu = self.menu[first:last]

        i = 2
        for element in menu:
            if element == self.get_current_item():
                self.scr.addstr(i, 2, element, curses.color_pair(1))
            else:
                self.scr.addstr(i, 2, element)
            i += 1
        self.scr.refresh()

    def get_current_item(self):
        return self.menu[self.item['current']]

    def navigate_up(self):
        if self.item['current'] > 0:
            self.item['current'] -= 1
            if self.item['current'] < self.item['first']:
                self.item['first'] -= 1

    def navigate_down(self):
        if self.item['current'] < len(self.menu) -1:
            self.item['current'] += 1
            if self.item['current'] >= self.item['show'] + self.item['first']:
                self.item['first'] += 1

    def show_result(self):
        win = self.scr.subwin(5, 40, 2, 10)
        win.border(0)
        win.addstr(2, 2, 'Vous avez selectionné %s'
                   % self.menu[self.item['current']])
        win.refresh()
        win.getch()
        win.erase()
       

    def handle_keybinding(self):
        while True:
            ch = self.scr.getch()
            if ch == curses.KEY_UP:
                self.navigate_up()
            elif ch == curses.KEY_DOWN:
                self.navigate_down()
            elif ch == curses.KEY_RIGHT:
                self.show_result()
            elif ch == ord('q'):
                break
            self.draw_menu()

if __name__ == '__main__':
    curses.wrapper(Menu)

La particularité est surtout de ne pas afficher tout le menu, il est ainsi découpé avant d’être affiché, ce n’était pas la seul possibilité d’aborder le problème.

first = self.item['first']
last = self.item['first'] + self.item['show']
menu = self.menu[first:last]

L’autre point à faire attention, c’est de bien s’assurer que les méthodes de navigation fassent ce qu’on attend. Par commodité, j’ai choisis un petit dictionnaire item pour repérer certains emplacement clé du menu, comme le nombre d’élément affiché (show).

Un billet un peu plus cours en explication, mais j’espère utile pour se faire la main avec la bibliothèque curses de Python.

Vus : 3245
Publié par Nicolas Paris : 149