Menus interactifs avec `prompt_toolkit`
Sommaire
- 1. Installation
- 2. Le pattern de base
- 3. Quel dialog choisir
- 4. Oui / Non
- 5. Saisie de texte
- 6. Choix unique (radiolist)
- 7. Choix multiples (checkboxlist)
- 8. Différence entre radiolist, checkboxlist et button_dialog
- 9. Raccourcis clavier utiles
- 10. Boutons
- 11. Message (info / alerte)
- 12. Combiner les dialogs
- 13. Construire un menu réutilisable
- 14. Saisie avec mot de passe
- 15. Construire values dynamiquement
- 16. Limites
- 17. Quand prompt_toolkit est le bon choix
- 18. Alternative légère : input() natif
- Références
Guide de référence pour utiliser prompt_toolkit depuis un script Catnip quand l'objectif est de construire des menus
interactifs en terminal : choix unique, options multiples, confirmations, boutons et petits assistants à étapes.
Si ce qui t'intéresse est la sélection clavier et les listes cochables, commence ici.
prompt_toolkitexpose des fonctions factory qui retournent des objetsApplication. Un seul appel.run()suffit pour capturer le terminal, afficher le dialog, et récupérer la réponse.
1. Installation
pip install prompt_toolkit
prompt_toolkit n'est pas une dépendance de Catnip. L'installer dans le même environnement suffit.
2. Le pattern de base
Tous les dialogs prompt_toolkit suivent le même cycle :
- Construire un
Applicationvia une fonction factory - Appeler
.run()pour bloquer jusqu'à l'interaction utilisateur - Récupérer la valeur retournée
pt = import("prompt_toolkit.shortcuts")
app = pt.yes_no_dialog(title="Confirmation", text="Lancer le déploiement ?")
result = app.run()
if (result) {
print("go.")
} else {
print("aborted.")
}
Les dialogs sont modaux. Le terminal est capturé pendant
.run(), puis rendu. Comme un appel système, mais poli.
3. Quel dialog choisir
Pour les menus interactifs, trois helpers couvrent l'essentiel :
radiolist_dialog(...): choisir une seule option parmi plusieurscheckboxlist_dialog(...): cocher plusieurs options puis validerbutton_dialog(...): déclencher une action parmi quelques boutons
Règle pratique :
- tu veux une valeur métier unique ->
radiolist_dialog - tu veux une liste d'options actives ->
checkboxlist_dialog - tu veux 2 ou 3 actions explicites ->
button_dialog
4. Oui / Non
pt = import("prompt_toolkit.shortcuts")
confirm = pt.yes_no_dialog(
title="Suppression",
text="Supprimer les fichiers temporaires ?",
yes_text="Oui",
no_text="Non"
).run()
# confirm : True ou False
Retourne True (oui) ou False (non). Retourne None si l'utilisateur fait Escape.
5. Saisie de texte
pt = import("prompt_toolkit.shortcuts")
name = pt.input_dialog(
title="Identité",
text="Nom du projet :",
default="untitled"
).run()
if (name != None) {
print(f"Projet : {name}")
}
cancel_text et ok_text contrôlent les boutons. Si l'utilisateur annule, la valeur retournée est None.
6. Choix unique (radiolist)
pt = import("prompt_toolkit.shortcuts")
env = pt.radiolist_dialog(
title="Environnement",
text="Cible de déploiement :",
values=list(
tuple("dev", "Development"),
tuple("staging", "Staging"),
tuple("prod", "Production")
)
).run()
match (env) {
case "dev" => print("deploying to dev")
case "staging" => print("deploying to staging")
case "prod" => print("deploying to prod (good luck)")
case None => print("cancelled")
}
Chaque entrée de values est un tuple(valeur_retournée, label_affiché). L'utilisateur navigue avec les flèches et
valide avec Entrée.
7. Choix multiples (checkboxlist)
pt = import("prompt_toolkit.shortcuts")
toppings = pt.checkboxlist_dialog(
title="Pizza",
text="Garnitures :",
values=list(
tuple("cheese", "Fromage"),
tuple("ham", "Jambon"),
tuple("mushrooms", "Champignons"),
tuple("olives", "Olives"),
tuple("anchovies", "Anchois")
)
).run()
# toppings : liste des valeurs cochées, ou None si annulé
if (toppings != None) {
print(f"Commande : {toppings}")
}
Retourne une liste des valeurs sélectionnées. Espace pour cocher/décocher, Entrée pour valider.
Pour un menu d'options, c'est le pattern à privilégier : la valeur retournée est déjà directement exploitable dans le script.
8. Différence entre radiolist, checkboxlist et button_dialog
Les signatures se ressemblent, mais pas totalement :
radiolist_dialog(values=...)attend des tuples(valeur_retournée, label_affiché)checkboxlist_dialog(values=...)attend aussi des tuples(valeur_retournée, label_affiché)button_dialog(buttons=...)attend des tuples(label_affiché, valeur_retournée)
Autrement dit, button_dialog inverse l'ordre.
9. Raccourcis clavier utiles
Dans les listes :
↑/↓: naviguerEspace: cocher ou décocher danscheckboxlist_dialogEntrée: validerTab: passer de la liste aux boutonsEscape: annuler, avec retourNone
10. Boutons
pt = import("prompt_toolkit.shortcuts")
action = pt.button_dialog(
title="Action",
text="Que faire avec ce commit ?",
buttons=list(
tuple("Deploy", "deploy"),
tuple("Rollback", "rollback"),
tuple("Ignore", "ignore")
)
).run()
match (action) {
case "deploy" => print("shipping")
case "rollback" => print("rewinding")
case "ignore" => print("nothing happened")
}
Chaque bouton est un tuple(label, valeur_retournée). L'ordre est inversé par rapport à radiolist_dialog (label en
premier).
11. Message (info / alerte)
pt = import("prompt_toolkit.shortcuts")
pt.message_dialog(
title="Statut",
text="Déploiement terminé. 0 erreurs, 3 warnings.",
ok_text="OK"
).run()
Bloque jusqu'à ce que l'utilisateur valide. Retourne None.
12. Combiner les dialogs
Les dialogs se chaînent naturellement. Chaque .run() bloque, puis le script continue.
#!/usr/bin/env catnip
pt = import("prompt_toolkit.shortcuts")
# Étape 1 : saisie
name = pt.input_dialog(title="Setup", text="Nom du projet :").run()
if (name == None) { print("cancelled"); import("sys").exit(0) }
# Étape 2 : choix
lang = pt.radiolist_dialog(
title="Setup",
text="Langage principal :",
values=list(
tuple("rust", "Rust"),
tuple("python", "Python"),
tuple("catnip", "Catnip")
)
).run()
if (lang == None) { print("cancelled"); import("sys").exit(0) }
# Étape 3 : options
features = pt.checkboxlist_dialog(
title="Setup",
text="Features :",
values=list(
tuple("ci", "CI/CD"),
tuple("docker", "Docker"),
tuple("tests", "Tests"),
tuple("docs", "Documentation")
)
).run()
if (features == None) { features = list() }
# Étape 4 : confirmation
ok = pt.yes_no_dialog(
title="Confirmer",
text=f"Créer {name} ({lang}) avec {len(features)} features ?"
).run()
if (ok) {
print(f"Creating {name}...")
print(f" lang: {lang}")
print(f" features: {features}")
} else {
print("aborted.")
}
Un wizard en 30 lignes. Le ratio information/boilerplate est conforme aux accords de Genève sur les interfaces utilisateur.
13. Construire un menu réutilisable
Quand tu as un menu principal, le plus simple est d'encapsuler le dialog dans une fonction puis de dispatcher sur la clé retournée :
pt = import("prompt_toolkit.shortcuts")
sys = import("sys")
main_menu = () => {
pt.radiolist_dialog(
title="Operations",
text="Choisir une commande",
values=list(
tuple("build", "Compiler"),
tuple("test", "Lancer les tests"),
tuple("clean", "Nettoyer"),
tuple("exit", "Quitter")
)
).run()
}
choice = main_menu()
match (choice) {
case "build" => print("build...")
case "test" => print("test...")
case "clean" => print("clean...")
case "exit" => sys.exit(0)
case None => print("cancelled")
}
Le point important : la valeur utile est la clé interne, pas le libellé affiché.
14. Saisie avec mot de passe
pt = import("prompt_toolkit.shortcuts")
token = pt.input_dialog(
title="Auth",
text="API token :",
password=True
).run()
password=True masque la saisie. Le terminal affiche des astérisques.
15. Construire values dynamiquement
Les listes de choix n'ont pas besoin d'être statiques.
pt = import("prompt_toolkit.shortcuts")
os = import("os")
# Lister les fichiers .cat du répertoire courant
files = os.listdir(".")
cat_files = filter((f) => { f.endswith(".cat") }, files)
values = map((f) => { tuple(f, f) }, cat_files)
choice = pt.radiolist_dialog(
title="Script",
text="Quel script exécuter ?",
values=list(values)
).run()
if (choice != None) {
print(f"Running {choice}...")
}
16. Limites
-
Pas de REPL : les dialogs
prompt_toolkitne fonctionnent pas dans la REPL interactive (catnipsans argument). La REPL ratatui active le raw mode terminal et redirige stdout vers un pipe interne pour réinjecter l'affichage via son propre viewport. prompt_toolkit a besoin du contrôle direct du terminal (mode cooked + accès au tty) pour ses séquences de positionnement curseur et d'alternate screen. Les deux sont incompatibles. Utiliser les dialogs depuis un script (catnip script.cat) ou viacatnip -c "...". -
Pas de callback : les dialogs
prompt_toolkitn'acceptent pas de callbacks Catnip pour validation avancée.input_dialoga un paramètrevalidator, mais il attend un objetValidatorPython avec des méthodes. Construire unValidatordepuis Catnip n'est pas direct. -
Terminal requis : les dialogs capturent le terminal (alternate screen). Ils ne fonctionnent pas en mode pipe ou dans un contexte sans TTY.
-
Pas de
progress_dialog: ce dialog attend une callback Python avec une signature spécifique (deux paramètres callable). Les fonctions Catnip ne sont pas compatibles avec cette introspection.
17. Quand prompt_toolkit est le bon choix
Utilise prompt_toolkit si tu veux :
- un menu de lancement
- un choix unique propre au clavier
- une liste d'options multi-sélectionnables
- une confirmation sensible
- un petit wizard terminal
Reste sur io.input(...) si tu veux juste une ou deux questions libres sans dépendance externe.
18. Alternative légère : input() natif
Pour une simple confirmation sans dépendance externe :
io = import("io")
answer = io.input("Continuer ? [y/N] ")
if (answer == "y" or answer == "Y") {
print("ok")
} else {
print("cancelled")
}
prompt_toolkit n'est justifié que quand tu as besoin de navigation clavier, de listes de choix, ou d'un affichage
structuré.
Références
- prompt_toolkit dialogs
- MODULE_LOADING -- chargement de modules Python depuis Catnip
- CLICK_INTEGRATION -- construire une CLI avec Click