From 2caf9c8c7448116291e667d886b04fa3f9cd235a Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Tue, 29 Sep 2026 20:54:11 -0500 Subject: [PATCH 1/5] gui: Apply Tails field-test feedback --- src/codex32_gui/pages.py | 52 +++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/codex32_gui/pages.py b/src/codex32_gui/pages.py index fbfdd1c..2ff0c35 100644 --- a/src/codex32_gui/pages.py +++ b/src/codex32_gui/pages.py @@ -41,7 +41,7 @@ _ICON_STYLE = {DONE_ICON: "success", "dialog-error-symbolic": "error"} SEED_SIZES = ((16, "128-bit seed, 48 characters"), (32, "256-bit seed, 74 characters")) PRESETS = ( - (2, 3, "Three cards, any two recover (recommended)"), + (2, 3, "Three cards, any two recover"), (3, 5, "Five cards, any three recover"), (0, 1, "One card"), ) @@ -79,7 +79,7 @@ ) CARDS_SAFE = ( "Your cards are unharmed and still recover this wallet. Nothing was written onto them and " - "nothing about them changed. When Bitcoin Core is ready, choose \u201cRestore my wallet\u201d " + "nothing about them changed. When Bitcoin Core is ready, choose “Restore my wallet” " "and enter them. Do not set up a new wallet: that would make a different backup." ) @@ -185,17 +185,18 @@ def _card( *, highlight_class: str = "guessed", ) -> Gtk.FlowBox: - """Show one card the way wallets.md asks: uppercase, in four-character windows.""" + """Show one card as uppercase four-character groups, wrapping only when needed.""" text = text.upper() + groups = tuple(text[start : start + reading.GROUP] for start in range(0, len(text), reading.GROUP)) flow = Gtk.FlowBox(selection_mode=Gtk.SelectionMode.NONE, column_spacing=8, row_spacing=8) flow.set_homogeneous(True) - flow.set_min_children_per_line(4) - flow.set_max_children_per_line(4) + flow.set_min_children_per_line(1) + flow.set_max_children_per_line(len(groups)) flow.set_hexpand(True) - for start in range(0, len(text), reading.GROUP): - label = Gtk.Label(label=text[start : start + reading.GROUP], halign=Gtk.Align.CENTER) + for position, group in enumerate(groups): + label = Gtk.Label(label=group, halign=Gtk.Align.CENTER) label.add_css_class("card-group") - if start // reading.GROUP in highlighted: + if position in highlighted: label.add_css_class(highlight_class) flow.append(label) return flow @@ -220,6 +221,24 @@ def _rows(title: str, values: Sequence[tuple[str, str]]) -> Adw.PreferencesGroup return group +def _compact_rows(title: str, values: Sequence[tuple[str, str]]) -> Gtk.Box: + """Show a dense two-column record without hiding any selectable values.""" + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + heading = Gtk.Label(label=title, xalign=0.0) + heading.add_css_class("heading") + box.append(heading) + grid = Gtk.Grid(column_spacing=18, row_spacing=3) + grid.set_hexpand(True) + for position, (label, value) in enumerate(values): + name = Gtk.Label(label=label, xalign=0.0) + name.add_css_class("dim-label") + detail = Gtk.Label(label=value, xalign=1.0, selectable=True, hexpand=True) + grid.attach(name, 0, position, 1, 1) + grid.attach(detail, 1, position, 1, 1) + box.append(grid) + return box + + def _forget_when_gone(view: Adw.NavigationView, page: Adw.NavigationPage, clear: Callable[[], None]) -> None: """Clear recovery text as soon as its page leaves the navigation stack.""" handlers: list[int] = [] @@ -904,17 +923,18 @@ def go() -> None: dialog.present(view) content = _column( - _title("Create a wallet for these keys", "Bitcoin Core makes it; codex32 fills it in."), + _title("Create a wallet for these keys", "Choose whether to encrypt the Bitcoin Core wallet file."), group, status, _note( - "Your passphrase goes straight to Bitcoin Core on standard input and nowhere else. It is " - "never saved, never written to a file, and never shown in the list of running programs." + "Wallet encryption protects this Bitcoin Core wallet file while it is locked. It does not " + "protect your recovery cards or make a compromised computer safe." ), _note( - "Forgetting this passphrase does not lose your bitcoin: your cards still recover the seed. " - "It protects the wallet on this computer.", - "success", + "If you forget the wallet passphrase, Bitcoin Core cannot unlock this wallet file. Your " + "codex32 recovery cards can recreate the wallet's keys in a new Bitcoin Core wallet, so keep " + "the cards safe and separate.", + "warning", ), ) page = _page("New wallet", content, actions=_actions(_button("Create", go, style="suggested-action"))) @@ -970,7 +990,7 @@ def go() -> None: content = _column( _title( - f"The wallet \u201c{wallet.name}\u201d is locked", + f"The wallet “{wallet.name}” is locked", "Bitcoin Core needs its passphrase before your keys can be written into it.", ), group, @@ -1035,7 +1055,7 @@ def _finished_page(view: Adw.NavigationView, record: Record, restoring: bool = F dated = () if restoring else (("Approximate creation date", time.strftime("%Y-%m-%d")),) content = _column( _title(heading, asked, DONE_ICON), - _rows( + _compact_rows( "Wallet identity", ( ("Backup identifier", record.identifier), From fc648a3f7b6749f755f9c9b1265889100ded9d38 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Tue, 29 Sep 2026 21:04:42 -0500 Subject: [PATCH 2/5] gui: Keep field-test polish within review budget --- src/codex32_gui/app.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/codex32_gui/app.py b/src/codex32_gui/app.py index 07835bc..8d42f41 100644 --- a/src/codex32_gui/app.py +++ b/src/codex32_gui/app.py @@ -1,5 +1,3 @@ -"""The window: one navigation view, one stylesheet, and no command arguments.""" - from __future__ import annotations import sys @@ -25,17 +23,11 @@ def do_activate(self) -> None: if display is not None: provider = Gtk.CssProvider() provider.load_from_string(CSS) - Gtk.StyleContext.add_provider_for_display( - display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ) + Gtk.StyleContext.add_provider_for_display(display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) view = Adw.NavigationView() view.push(pages.home(view)) window = Adw.ApplicationWindow( - application=self, - title="codex32", - default_width=880, - default_height=620, - content=view, + application=self, title="codex32", default_width=880, default_height=620, content=view ) window.present() From df43e010292d6a40d3d71922b14b9ffe93a77ad8 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Tue, 29 Sep 2026 21:15:07 -0500 Subject: [PATCH 3/5] gui: Keep budget cleanup Ruff-formatted --- src/codex32_gui/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/codex32_gui/app.py b/src/codex32_gui/app.py index 8d42f41..02c0fb1 100644 --- a/src/codex32_gui/app.py +++ b/src/codex32_gui/app.py @@ -13,8 +13,6 @@ class Application(Adw.Application): - """One non-unique window with a stable desktop identity and no recent list or files.""" - def __init__(self) -> None: super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.NON_UNIQUE) @@ -23,7 +21,9 @@ def do_activate(self) -> None: if display is not None: provider = Gtk.CssProvider() provider.load_from_string(CSS) - Gtk.StyleContext.add_provider_for_display(display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + Gtk.StyleContext.add_provider_for_display( + display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) view = Adw.NavigationView() view.push(pages.home(view)) window = Adw.ApplicationWindow( From 4edeb06cf09e4733b8c55b063d56081378f621b0 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Tue, 29 Sep 2026 21:15:38 -0500 Subject: [PATCH 4/5] test: Read GUI source as UTF-8 --- tests/test_gui_boundaries.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_gui_boundaries.py b/tests/test_gui_boundaries.py index b79044f..bfad256 100644 --- a/tests/test_gui_boundaries.py +++ b/tests/test_gui_boundaries.py @@ -58,7 +58,7 @@ def _imports(tree: ast.AST) -> set[str]: @pytest.mark.parametrize("path", _modules(), ids=lambda path: path.name) def test_the_gui_draws_no_entropy_opens_no_socket_and_touches_no_file(path: Path) -> None: - imported = _imports(ast.parse(path.read_text())) + imported = _imports(ast.parse(path.read_text(encoding="utf-8"))) assert not {name.split(".")[0] for name in imported} & FORBIDDEN, sorted(imported) @@ -66,7 +66,7 @@ def test_the_gui_draws_no_entropy_opens_no_socket_and_touches_no_file(path: Path def test_nothing_reads_or_writes_a_file(path: Path) -> None: called = { node.func.id - for node in ast.walk(ast.parse(path.read_text())) + for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) } assert "open" not in called and "eval" not in called and "exec" not in called @@ -74,13 +74,13 @@ def test_nothing_reads_or_writes_a_file(path: Path) -> None: @pytest.mark.parametrize("path", _modules(), ids=lambda path: path.name) def test_only_one_module_speaks_to_bitcoin_core(path: Path) -> None: - imported = _imports(ast.parse(path.read_text())) + imported = _imports(ast.parse(path.read_text(encoding="utf-8"))) assert (CORE_ADAPTER in imported) == (path.name == "wallet_setup.py"), sorted(imported) def test_the_accessibility_bus_is_turned_off_before_gtk_starts() -> None: """GTK otherwise publishes every label and entry, seed and passphrase included.""" - source = (_package() / "__init__.py").read_text() + source = (_package() / "__init__.py").read_text(encoding="utf-8") assert 'GLib.setenv("GTK_A11Y", "none", False)' in source tree = ast.parse(source) settings = [node for node in ast.walk(tree) if isinstance(node, ast.Import | ast.ImportFrom)] @@ -90,26 +90,27 @@ def test_the_accessibility_bus_is_turned_off_before_gtk_starts() -> None: @pytest.mark.parametrize("path", _modules(), ids=lambda path: path.name) def test_nothing_but_the_version_pin_may_import_a_module_by_name(path: Path) -> None: """`importlib` would reach any of the forbidden modules without naming one.""" - imported = {name.split(".")[0] for name in _imports(ast.parse(path.read_text()))} + imported = {name.split(".")[0] for name in _imports(ast.parse(path.read_text(encoding="utf-8")))} assert "importlib" not in imported or path.name == "__init__.py", sorted(imported) def test_the_parts_that_decide_something_need_no_toolkit() -> None: for name in ("reading.py", "wallet_setup.py", "style.py"): - imported = _imports(ast.parse((_package() / name).read_text())) + imported = _imports(ast.parse((_package() / name).read_text(encoding="utf-8"))) assert not any(item == "gi" or item.startswith("gi.") for item in imported), name def test_the_library_does_not_depend_on_the_gui() -> None: library = Path(importlib.import_module("codex32").__file__ or "").parent for path in library.rglob("*.py"): - assert "codex32_gui" not in path.read_text(), path + assert "codex32_gui" not in path.read_text(encoding="utf-8"), path def test_the_gui_keeps_its_own_size_budget() -> None: counts = { path.name: sum( - bool(line.strip()) and not line.lstrip().startswith("#") for line in path.read_text().splitlines() + bool(line.strip()) and not line.lstrip().startswith("#") + for line in path.read_text(encoding="utf-8").splitlines() ) for path in _modules() } @@ -117,7 +118,7 @@ def test_the_gui_keeps_its_own_size_budget() -> None: def test_read_only_poll_threads_do_not_keep_the_process_alive() -> None: - source = (_package() / "work.py").read_text() + source = (_package() / "work.py").read_text(encoding="utf-8") tree = ast.parse(source) functions = {node.name: node for node in tree.body if isinstance(node, ast.FunctionDef)} From 72b5aae625843f3d4ac8fdb8eda2f8f4ba00a31b Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Tue, 29 Sep 2026 21:20:44 -0500 Subject: [PATCH 5/5] gui: Keep field-test polish within review budget --- src/codex32_gui/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/codex32_gui/app.py b/src/codex32_gui/app.py index 02c0fb1..6674015 100644 --- a/src/codex32_gui/app.py +++ b/src/codex32_gui/app.py @@ -33,7 +33,6 @@ def do_activate(self) -> None: def main(argv: Sequence[str] | None = None) -> int: - """Open the window. Arguments are refused so that no secret can be passed in one.""" arguments = list(sys.argv[1:] if argv is None else argv) if arguments == ["--version"]: print(__version__)