[FIX] motd : replier la glose au lieu de déborder de 80 colonnes
Le guide tenait dans 80 colonnes nu, et rendait 103 dès qu'une VM portait ses outils et un bureau. Le cadre débordait alors, le terminal repliait où il voulait, et l'alignement en deux colonnes — seule chose qui rend un guide lisible d'un coup d'œil — disparaissait. La mise en page porte désormais la règle, plutôt que la longueur des textes qui n'aurait tenu que jusqu'au prochain outil : la glose se replie sous sa colonne, et passe sous sa commande quand celle-ci ne laisse plus de quoi écrire. L'épreuve de largeur ne couvrait que le guide nu ; elle couvre le guide équipé, qui est celui où le débordement vivait. --- EN --- The guide fitted in 80 columns bare, and rendered 103 as soon as a VM carried its tools and a desktop. The frame then overflowed, the terminal wrapped wherever it liked, and the two-column alignment — the only thing making a guide readable at a glance — was gone. The layout now carries the rule, rather than the length of texts which would have held only until the next tool: a gloss wraps under its column, and moves below its command when that command leaves no room. The width check covered only the bare guide; it now covers the equipped one, which is where the overflow lived. Assisted-by: Claude Opus 5
This commit is contained in:
parent
270aedd790
commit
ce1bf5f4d1
2 changed files with 130 additions and 15 deletions
|
|
@ -2785,10 +2785,50 @@ def motd_block(
|
|||
"""
|
||||
out = [f" {title}"]
|
||||
for cmd, gloss_fr, gloss_en in rows:
|
||||
out.append(f" {cmd.ljust(col)}{_pick((gloss_fr, gloss_en), lang)}")
|
||||
out += _lignes_glose(cmd, _pick((gloss_fr, gloss_en), lang), col)
|
||||
return out
|
||||
|
||||
|
||||
# La largeur du cadre, bordure comprise. Un guide plus large qu'un terminal
|
||||
# ordinaire se replie tout seul, n'importe où, et l'alignement en deux
|
||||
# colonnes — la seule chose qui le rend lisible d'un coup d'œil — disparaît.
|
||||
MOTD_MAX_WIDTH = 80
|
||||
# Ce qui reste au contenu : build_motd ajoute quatre colonnes de cadre.
|
||||
MOTD_TEXT_WIDTH = MOTD_MAX_WIDTH - 4
|
||||
# En deçà, replier la glose en colonne donnerait des bribes de trois mots.
|
||||
MOTD_GLOSE_MIN = 24
|
||||
|
||||
|
||||
def _lignes_glose(cmd: str, glose: str, col: int) -> list[str]:
|
||||
"""Les lignes d'UNE entrée, repliées pour tenir dans MOTD_MAX_WIDTH.
|
||||
|
||||
DEUX MISES EN PAGE, et la place restante décide. Tant qu'il reste de quoi
|
||||
écrire, la glose garde sa colonne et ses suites s'alignent dessous :
|
||||
c'est cet alignement qui fait lire un bloc d'un coup d'œil. Quand la
|
||||
commande est longue au point qu'il ne reste plus rien, la garder
|
||||
donnerait des bribes — la glose passe alors sous sa commande, indentée de
|
||||
six, et retrouve toute la largeur.
|
||||
|
||||
L'INDENTATION APPARTIENT À CETTE FONCTION. La laisser à l'appelant, qui
|
||||
alignait toute suite sur la tête, annulait le second cas : la glose
|
||||
repliée repartait à la colonne d'où elle venait d'être chassée.
|
||||
|
||||
Une glose vide ne rend que la commande : le bloc du système en a.
|
||||
"""
|
||||
tete = f" {cmd.ljust(col)}"
|
||||
if not glose.strip():
|
||||
return [tete.rstrip()]
|
||||
place = MOTD_TEXT_WIDTH - len(tete)
|
||||
if place >= MOTD_GLOSE_MIN:
|
||||
morceaux = textwrap.wrap(glose, place)
|
||||
return [tete + morceaux[0]] + [
|
||||
" " * len(tete) + m for m in morceaux[1:]
|
||||
]
|
||||
return [tete.rstrip()] + [
|
||||
" " + m for m in textwrap.wrap(glose, MOTD_TEXT_WIDTH - 6)
|
||||
]
|
||||
|
||||
|
||||
def build_motd(
|
||||
distro: str,
|
||||
version: str,
|
||||
|
|
|
|||
|
|
@ -149,24 +149,38 @@ class TestMotdErplibreSection(unittest.TestCase):
|
|||
|
||||
class TestMotdLayout(unittest.TestCase):
|
||||
def test_never_wider_than_a_standard_terminal(self):
|
||||
"""Le guide ENTIER, outils et bureau compris.
|
||||
|
||||
L'épreuve ne portait que sur le guide nu, et laissait donc passer ce
|
||||
qu'elle existait pour interdire : une VM équipée rendait 103 colonnes
|
||||
— le cadre débordait, le terminal repliait où il voulait, et
|
||||
l'alignement en deux colonnes, seule chose qui rend un guide lisible
|
||||
d'un coup d'œil, disparaissait.
|
||||
"""
|
||||
outils = tuple(dq.TOOL_GUIDE)
|
||||
self.assertTrue(outils, "catalogue d'outils vide")
|
||||
for distro, version, arch in COMBOS:
|
||||
for lang in ("fr", "en"):
|
||||
for el_dir in ("", "~/git/erplibre", "/opt/erplibre"):
|
||||
motd = dq.build_motd(
|
||||
distro,
|
||||
version,
|
||||
arch,
|
||||
lang,
|
||||
el_dir,
|
||||
"install_odoo_18" if el_dir else "",
|
||||
"vim" if el_dir else "",
|
||||
)
|
||||
for line in motd.splitlines():
|
||||
self.assertLessEqual(
|
||||
len(line),
|
||||
TERM_WIDTH,
|
||||
f"{distro} {version} {lang} {el_dir} : {line}",
|
||||
for equipee in (False, True):
|
||||
motd = dq.build_motd(
|
||||
distro,
|
||||
version,
|
||||
arch,
|
||||
lang,
|
||||
el_dir,
|
||||
"install_odoo_18" if el_dir else "",
|
||||
"vim" if el_dir else "",
|
||||
desktop=equipee,
|
||||
tools=outils if equipee else (),
|
||||
)
|
||||
for line in motd.splitlines():
|
||||
self.assertLessEqual(
|
||||
len(line),
|
||||
TERM_WIDTH,
|
||||
f"{distro} {version} {lang} {el_dir} "
|
||||
f"outils={equipee} : {line}",
|
||||
)
|
||||
|
||||
def test_the_frame_is_never_narrower_than_what_it_frames(self):
|
||||
for distro, version, arch in COMBOS:
|
||||
|
|
@ -666,5 +680,66 @@ class LesOutilsPosesSAnnoncent(unittest.TestCase):
|
|||
self.assertEqual("nixanywhere", parts("arm64", False))
|
||||
|
||||
|
||||
class LaGloseSeReplieAuLieuDeDeborder(unittest.TestCase):
|
||||
"""Ce qui arrive à une entrée trop longue pour 80 colonnes.
|
||||
|
||||
Raccourcir les textes aurait tenu jusqu'au prochain outil ajouté ; c'est
|
||||
la MISE EN PAGE qui porte désormais la règle, et elle vaut pour ce qui
|
||||
n'est pas encore écrit.
|
||||
"""
|
||||
|
||||
def _lignes(self, cmd, glose, col=None):
|
||||
return dq._lignes_glose(cmd, glose, col if col else len(cmd) + 2)
|
||||
|
||||
def test_a_short_entry_stays_on_one_line(self):
|
||||
"""Replier ce qui tient serait une régression à soi seul."""
|
||||
lignes = self._lignes("make todo", "menu ERPLibre")
|
||||
self.assertEqual(len(lignes), 1)
|
||||
self.assertIn("make todo", lignes[0])
|
||||
self.assertIn("menu ERPLibre", lignes[0])
|
||||
|
||||
def test_a_long_gloss_keeps_its_column(self):
|
||||
"""La suite s'aligne sous la glose, pas sous la commande : c'est
|
||||
l'alignement qui fait lire un bloc d'un coup d'œil."""
|
||||
lignes = self._lignes("npm start", "a " * 40, col=20)
|
||||
self.assertGreater(len(lignes), 1)
|
||||
debut = lignes[0].index("a a")
|
||||
for suite in lignes[1:]:
|
||||
self.assertEqual(suite[:debut], " " * debut, suite)
|
||||
self.assertTrue(suite[debut:].strip())
|
||||
|
||||
def test_a_command_that_leaves_no_room_pushes_the_gloss_under_it(self):
|
||||
"""Au-delà d'un seuil, garder la colonne donnerait des bribes de
|
||||
trois mots. La glose passe sous sa commande et retrouve la largeur."""
|
||||
longue = "dbus-run-session -- gnome-extensions enable <uuid>"
|
||||
lignes = self._lignes(longue, "activer depuis ssh")
|
||||
self.assertEqual(lignes[0].strip(), longue)
|
||||
self.assertEqual(lignes[1], " activer depuis ssh")
|
||||
|
||||
def test_an_empty_gloss_yields_the_command_alone(self):
|
||||
"""Une entrée sans glose ne doit pas rendre une ligne d'espaces."""
|
||||
lignes = self._lignes("hostname -I", "")
|
||||
self.assertEqual(lignes, [" hostname -I"])
|
||||
|
||||
def test_no_produced_line_exceeds_the_budget(self):
|
||||
"""L'invariant, sur des entrées fabriquées bien pires que celles du
|
||||
catalogue : quoi qu'on lui donne, rien ne dépasse."""
|
||||
for cmd, glose in (
|
||||
("a", "z " * 80),
|
||||
("x" * 60, "y " * 40),
|
||||
("x" * 70, "court"),
|
||||
("court", ""),
|
||||
):
|
||||
with self.subTest(cmd=cmd[:12]):
|
||||
for ligne in self._lignes(cmd, glose):
|
||||
self.assertLessEqual(len(ligne), dq.MOTD_TEXT_WIDTH, ligne)
|
||||
|
||||
def test_the_budget_leaves_room_for_the_frame(self):
|
||||
"""build_motd ajoute quatre colonnes de cadre : un budget égal à la
|
||||
largeur ferait déborder le cadre de ce qu'il encadre."""
|
||||
self.assertEqual(dq.MOTD_TEXT_WIDTH, dq.MOTD_MAX_WIDTH - 4)
|
||||
self.assertEqual(dq.MOTD_MAX_WIDTH, TERM_WIDTH)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Reference in a new issue