Visualiser et scroller un fichier en Python et curses

Pour continuer dans la série de billets exercices sur la bibliothèque ncurses de Python, on va essayer de reproduire sommairement le comportement de la commande less, qui permet de naviguer en scrollant dans un fichier, et de quitter, même si la commande permet plus que ça, c’est du moins la fonctionnalité qu’on cherche ici à reproduire. En bonus, nous afficherons le numéro de ligne.

L’utilité de ce script en est réduit à l’intérêt d’apprendre à le faire, ni plus, ni moins. Dans ce billet, le fichier qu’on cherche à visualisé sera renseigné en ligne de commande, préparez un fichier de quelques centaines de lignes pour en faire un test.

Les touches sont: ‘j’ et ‘k’ pour ce déplacer dans le fichier, et ‘q’ pour quitter.

NOTE: J’ai utilisé ici Python 3 pour écrire le script, bien que ça ne fasse que peu de différence, il est utile de changer quelques détails, pour Python2

Idée générale

Nous allons utiliser deux fenêtre, ou cadre pour faire celà.

  • scr: la console, utilisé de façon classique comme dans les derniers billet, mais qui n’aura que peu d’utilité ici.
  • pad: une fenêtre de taille supérieur à la console, contenant la totalité du fichier text à afficher et scroller.

Imaginons que j’ai un petit cadre (la console) derrière lequel je place une grande feuille (pad) bien plus grand que le cadre, si je déplace la feuille, tout en ne regardant que le cadre, j’aurai l’impression que le contenu dans le cadre est scrollé, et c’est un peu ce comportement qui est reproduit ici.

Voici le script entier dans un premiers temps, sur lequel j’ai placé des commentaires tout du long pour la compréhension, je reviens tout de même sur le pad à la suite de ce billet.

Le script complet

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

import sys
import curses
import curses.wrapper

class Scroll(object):

    def __init__(self, scr, filename):
        self.scr = scr
        self.filename = filename
        # On garde en mémoire la première ligne à affiché
        self.first_line = 0

        self.run_scroller()

    def run_scroller(self):
        self.init_curses()
        self.init_file()
        self.init_pad()
        self.key_handler()

    def init_curses(self):
        '''Quelques banalités pour le fonctionnement de curses
        Voir les billets précédent s'il y a des doutes dessus
        '''

        curses.use_default_colors()
        curses.curs_set(0)
        self.maxyx = self.scr.getmaxyx()

    def init_file(self):
        '''On ouvre le fichie, en gardant son contenu, sous forme de tableau
        avec une ligne par entrée, on compte également le nomble de ligne
        afin de savoir la hauteur utile pour le "pad"
        '''

        f = open(self.filename, 'r')
        self.content = f.readlines()
        self.count = len(self.content)+1
        f.close()

    def init_pad(self):
        '''On créer le pad, dans lequel on affiche ligne par ligne le contenu
        du fichier, avec son numéro de ligne. Le pad est finalement affiché
        avec l'appel à la méthode refresh_pad qu'on va voir en dessous.
        '''

        self.pad = curses.newpad(self.count, self.maxyx[1])

        for i, line in enumerate(self.content):
            self.pad.addstr(i, 0, '{0:3} {1}'.format(i+1, line))

        self.refresh_pad()

    def refresh_pad(self):
        '''Le plus gros du concept est ici, voir le billet pour plus
        d'explication
        '''

        self.pad.refresh(self.first_line, 0, 0, 0,
                self.maxyx[0]-1, self.maxyx[1]-1)

    def key_handler(self):
        '''Une boucle classique, afin d'interpréter les touches'''
        while True:
            ch = self.pad.getch()
            if ch == ord('j'):
                self.scroll_down()
            elif ch == ord('k'):
                self.scroll_up()
            elif ch == ord('q'):
                break
            self.refresh_pad()

    def scroll_down(self):
        '''On scroll, tout en s'assurant de l'affichage restant'''
        if self.maxyx[0] + self.first_line < self.count:
            self.first_line += 1

    def scroll_up(self):
        '''On scroll, en s'assurant qu'on est pas déjà en début
        de fichier.'''

        if self.first_line > 0:
            self.first_line -= 1

if __name__ == '__main__':

    try:
        # On essaie de lire l'argument fournis en console
        # correspondant au nom du fichier
        filename = sys.argv[1]
    except IndexError as e:
        # Si aucun argument est trouvé, on affiche l'usage
        print('Erreur: {}'.format(e))
        print('Usage:  {} filename'.format(sys.argv[0]))
        sys.exit(0)
    # On appelle la classe avec le wrapper curses.
    curses.wrapper(Scroll, filename)

Quelques informations supplémentaires

Comme promis, je reviens sur la méthode suivante:

def refresh_pad(self):
    self.pad.refresh(self.first_line, 0, 0, 0,
            self.maxyx[0]-1, self.maxyx[1]-1)

Pour la signification des arguments, en les prenant deux par deux:

  • coordonnées y, x de départ sur le pad. Pour reprendre l’exemple plus haut de la feuille, ces coordonnées précise à quel point je vais commencer à afficher le contenu. Si j’ai par exemple 2, 10, ça signifie que je commence à afficher à la ligne 2 et à partir du 10 ème caractère, Je ne sais pas encore où je vais afficher, ni quel quantité, mais je connais le commencement de mon contenu. Dans l’exemple, on veux bien sûr afficher dès le 1er caractère et commencer à la première ligne, pour scroller, il suffira d’incrémenter la première ligne.
  • coordonnées y, x de départ sur la console. Puisque je n’affiche qu’une partie, je décide donc de commencer l’affichage tout en haut à gauche de ma console, c’est à dire 0, 0.
  • coordonnées y, x de fin sur la console. Pour avoir notre cadre, il faut le début (voir ci-dessus) et la fin, correspondant au coin inférieur droit de la console, obtenu grâce à la fonction scr.getmaxyx()

Je pense qu’avec les explications, le système de scroll est plus clair, on peut noter que le script est assez simplifié, qu’il manque par exemple la gestion de redimension de l’écran, mais ce n’est pas le but ici d’alourdir.

Voilà, en espérant que ça intéresse quelqu’un d’autre que moi :þ

Vus : 1651
Publié par Nicolas Paris : 149