diff --git a/repo_contract/launcher.py b/repo_contract/launcher.py index 609066f..f4e6359 100644 --- a/repo_contract/launcher.py +++ b/repo_contract/launcher.py @@ -122,7 +122,10 @@ def desktop_problems(repo: Path) -> list[str]: if not names: out.append(f"{rel}: no Name= in [Desktop Entry]") for name in names: - out += [f"{rel}: Name={p}" for p in launcher_label.label_problems(name)] + out += [ + f"{rel}: Name={p}" + for p in launcher_label.label_problems(name, words=True) + ] return out diff --git a/repo_contract/launcher_label.py b/repo_contract/launcher_label.py index 2211d04..a307643 100644 --- a/repo_contract/launcher_label.py +++ b/repo_contract/launcher_label.py @@ -7,7 +7,8 @@ the launcher font is capped too (`label_widths.py`): FocusOwner, 10 characters, was still cut. The same rules live in `~/src/testsAndMisc/python_pkg/app_icons/labels.py` (`label_problems`); the messages are mirrored here, not imported, because utils must not depend -on another repo. +on another repo. A desktop entry's `Name=` (rule 2) may hold single spaces +between words (`words=True`); the phone launcher may not. The name is `` of the main manifest, plus the `android:label` of any activity (or alias) whose intent-filter holds MAIN + @@ -38,27 +39,39 @@ from repo_contract.label_widths import MAX_WIDTH, label_width MAX_LENGTH = 10 STRINGS = Path("android/app/src/main/res/values/strings.xml") _ASCII_LETTERS = re.compile(r"[A-Za-z]+") +_ASCII_WORDS = re.compile(r"[A-Za-z]+( [A-Za-z]+)*") _STRING_REF = re.compile(r"@string/(\w+)") _PLACEHOLDER = re.compile(r"\$\{(\w+)\}") _UNRESOLVED = "cannot resolve the launcher name" -def label_problems(name: str) -> list[str]: - """Every launcher-name rule `name` breaks (app_icons labels.py wording).""" +def label_problems(name: str, *, words: bool = False) -> list[str]: + """Every launcher-name rule `name` breaks (app_icons labels.py wording). + + `words=True` is for a desktop entry's `Name=` (rule 2), shown in a menu or + dmenu where "Sick days" reads better than "Sickdays": one space between + words is fine. The phone launcher (rule 1) never passes it. + """ if not name: return ["label is empty"] problems: list[str] = [] - letters_only = _ASCII_LETTERS.fullmatch(name) is not None - if not letters_only: - problems.append( - f"{name!r} must contain only ASCII letters (no spaces, _ or symbols)" + if words: + valid = _ASCII_WORDS.fullmatch(name) is not None + message = ( + f"{name!r} must be ASCII letters, one space between words (no _ or symbols)" ) + else: + valid = _ASCII_LETTERS.fullmatch(name) is not None + message = f"{name!r} must contain only ASCII letters (no spaces, _ or symbols)" + if not valid: + problems.append(message) if not name[0].isupper(): problems.append(f"{name!r} must start with a capital letter") # The width table only knows ASCII letters; other names already failed. - if letters_only and label_width(name) > MAX_WIDTH: + letters = name.replace(" ", "") + if valid and label_width(letters) > MAX_WIDTH: problems.append( - f"{name!r} is {label_width(name)} wide in the launcher font; " + f"{name!r} is {label_width(letters)} wide in the launcher font; " f"at most {MAX_WIDTH} fit" ) if len(name) > MAX_LENGTH: diff --git a/repo_contract/tests/test_launcher.py b/repo_contract/tests/test_launcher.py index c7a26a5..55d73d6 100644 --- a/repo_contract/tests/test_launcher.py +++ b/repo_contract/tests/test_launcher.py @@ -98,6 +98,7 @@ def test_no_flutter_app_no_rules(repo: Path) -> None: def test_desktop_names_checked(repo: Path) -> None: flutter_app(repo) write(repo, "linux/good.desktop", DESKTOP.format(name="Todo")) + write(repo, "linux/spaced.desktop", DESKTOP.format(name="Sick days")) write(repo, "linux/bad.desktop", DESKTOP.format(name="todo app")) write( repo, @@ -108,10 +109,6 @@ def test_desktop_names_checked(repo: Path) -> None: track(repo) write(repo, "linux/untracked.desktop", DESKTOP.format(name="bad name")) assert launcher.problems(repo) == [ - ( - "launcher: linux/bad.desktop: Name='todo app' must contain only ASCII" - " letters (no spaces, _ or symbols)" - ), "launcher: linux/bad.desktop: Name='todo app' must start with a capital letter", "launcher: linux/none.desktop: no Name= in [Desktop Entry]", ] diff --git a/repo_contract/tests/test_launcher_label.py b/repo_contract/tests/test_launcher_label.py index 0c39662..0efe743 100644 --- a/repo_contract/tests/test_launcher_label.py +++ b/repo_contract/tests/test_launcher_label.py @@ -177,3 +177,45 @@ def test_manifest_without_application(tmp_path: Path) -> None: assert ll.problems(app) == [ f"{MAIN}: has no android:label; cannot resolve the launcher name" ] + + +WORDS_RULE = "must be ASCII letters, one space between words (no _ or symbols)" + + +@pytest.mark.parametrize( + ("name", "expected"), + [ + ("Sick days", []), + ("Todo", []), + ("sick days", ["'sick days' must start with a capital letter"]), + ( + "Sick days", + [f"'Sick days' {WORDS_RULE}"], + ), + ( + "Sick days ", + [f"'Sick days ' {WORDS_RULE}"], + ), + ( + "Sick_days", + [f"'Sick_days' {WORDS_RULE}"], + ), + ( + "Focus owner x", + [ + "'Focus owner x' is 607 wide in the launcher font; at most 522 fit", + "'Focus owner x' has 13 characters; at most 10 fit", + ], + ), + ], +) +def test_desktop_names_may_have_spaces_between_words( + name: str, expected: list[str] +) -> None: + assert ll.label_problems(name, words=True) == expected + + +def test_the_phone_label_still_refuses_a_space() -> None: + assert ll.label_problems("Sick days") == [ + "'Sick days' must contain only ASCII letters (no spaces, _ or symbols)" + ] diff --git a/sickdays/sickdays.desktop b/sickdays/sickdays.desktop index b61b894..a20f9b8 100644 --- a/sickdays/sickdays.desktop +++ b/sickdays/sickdays.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Name=Sickdays +Name=Sick days Comment=Mark sick days and choose which lockers still apply Exec=sickdays gui Icon=face-sick