Add arrow-key menus and report loading status

wizard: when stdin and stdout are both TTYs, menus render inline and
are driven by keys: up/down/j/k/Home/End move the bold '>' highlight,
Enter accepts, Esc/Ctrl-C/q back out of cancellable menus while Ctrl-C
on fixed menus still aborts. Rows redraw with erase+CR across the
option block only and clear before returning so prompts stay aligned.
Non-TTY stdio keeps the original numbered prompt verbatim.

report: each configured source shows a transient "# Generating
report: pulling <name> data..." line that is erased on completion,
only when stdout is a TTY; piped output remains exactly the report.

tests: cover the numbered fallback, row rendering, status suppression
and show+clear parity; reformat stray test files to satisfy ruff.
This commit is contained in:
2026-08-27 07:46:48 +00:00
parent 5f4487f3ad
commit 85ddfb4a71
6 changed files with 340 additions and 28 deletions
+27 -19
View File
@@ -43,9 +43,7 @@ def sample_v2_config() -> ActivityConfig:
],
github=[
GithubSettings(username='gh"user', token="tok\\en"),
GithubSettings(
username="acme-jane", token_env="WORK_GH_TOKEN", name="work"
),
GithubSettings(username="acme-jane", token_env="WORK_GH_TOKEN", name="work"),
],
gitlab=[
GitlabSettings(
@@ -121,13 +119,11 @@ class ConfigLoadingTest(unittest.TestCase):
def test_unknown_top_level_key_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text('version = 2\nsources = 3\n')
load_config_from_text("version = 2\nsources = 3\n")
def test_unknown_account_key_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text(
'[github]\naccounts = [{username = "x", tokken = "t"}]\n'
)
load_config_from_text('[github]\naccounts = [{username = "x", tokken = "t"}]\n')
def test_section_must_hold_only_accounts_table(self) -> None:
with self.assertRaises(ConfigError):
@@ -141,13 +137,11 @@ class ConfigLoadingTest(unittest.TestCase):
with self.assertRaises(ConfigError):
load_config_from_text('version = 2\n\n[github]\naccounts = "nope"\n')
with self.assertRaises(ConfigError):
load_config_from_text('version = 2\n\n[gitlab]\naccounts = [42]\n')
load_config_from_text("version = 2\n\n[gitlab]\naccounts = [42]\n")
def test_non_string_value_rejected(self) -> None:
with self.assertRaises(ConfigError):
load_config_from_text(
'version = 2\n\n[[github.accounts]]\nusername = 42\n'
)
load_config_from_text("version = 2\n\n[[github.accounts]]\nusername = 42\n")
def test_bad_launchpad_mode_rejected(self) -> None:
with self.assertRaises(ConfigError):
@@ -169,8 +163,8 @@ class ConfigLoadingTest(unittest.TestCase):
def test_account_errors_carry_position(self) -> None:
text = (
"version = 2\n"
"\n[[launchpad.accounts]]\nusername = \"ok\"\n"
"\n[[launchpad.accounts]]\nusername = \"bad\"\nservice = \"oops\"\n"
'\n[[launchpad.accounts]]\nusername = "ok"\n'
'\n[[launchpad.accounts]]\nusername = "bad"\nservice = "oops"\n'
)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "weekly-activity.toml"
@@ -209,13 +203,27 @@ email = "dev@example.org"
def test_v1_file_migrates_to_single_unnamed_accounts_in_memory(self) -> None:
config = load_config_from_text(self.V1_TEXT)
self.assertEqual(config.enabled_sources(), ["launchpad", "github", "gitlab", "bts"])
self.assertEqual(config.launchpad, [LaunchpadSettings(
username="lp-user", mode="credentials", credentials_file="~/creds.json",
)])
self.assertEqual(
config.launchpad,
[
LaunchpadSettings(
username="lp-user",
mode="credentials",
credentials_file="~/creds.json",
)
],
)
self.assertEqual(config.github, [GithubSettings(username="octo", token="t0k")])
self.assertEqual(config.gitlab, [GitlabSettings(
url="https://salsa.debian.org", username="gl", token_env="SALSA_TOKEN",
)])
self.assertEqual(
config.gitlab,
[
GitlabSettings(
url="https://salsa.debian.org",
username="gl",
token_env="SALSA_TOKEN",
)
],
)
self.assertEqual(config.bts, [BtsSettings(email="dev@example.org")])
def test_saving_migrated_config_persists_v2_shape(self) -> None: