show alert buttons carrying their own text in the terminal rather than failing to map them
What changed, and why it matters
This commit fixes a user-interface bug in Sparrow Wallet's terminal (command-line) mode. Previously, alert buttons with custom labels like 'Refresh Wallet' could not be shown correctly and were either mislabeled or failed to map. The change adds a new dialog class that displays buttons with their actual text, so users see the correct choices and can act on them.
No immediate security action required; this is a usability fix. Users running terminal builds should update to include this fix to avoid mislabeled alert buttons.
Security signals we found
UI misrepresentation in terminal alerts
Custom action buttons previously mapped to misleading standard labels
Potential for user confusion or accidental confirmation/cancellation
Evidence from the diff
The patch introduces ButtonTypeDialog, a Lanterna DialogWindow subclass, and updates TerminalInteractionServices.showMessageDialog to use it when any ButtonType is not one of the standard OK/CANCEL/YES/NO/CLOSE enums. Before this, MessageDialogBuilder only supported fixed MessageDialogButton values, so custom ButtonType labels were forced into an incorrect mapping (e.g., a CANCEL_CLOSE role would render as ‘Cancel’ despite performing a wallet refresh). The new dialog preserves button identity and text, returning the exact ButtonType instance that was pressed.
Changed components
Sparrow Wallet terminal UITerminalInteractionServices.showMessageDialogButtonTypeDialogInspect captured patch +58 / −0
### src/main/java/com/sparrowwallet/sparrow/terminal/ButtonTypeDialog.java
@@ -0,0 +1,47 @@
+package com.sparrowwallet.sparrow.terminal;
+
+import com.googlecode.lanterna.TerminalSize;
+import com.googlecode.lanterna.gui2.*;
+import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
+import javafx.scene.control.ButtonType;
+
+import java.util.List;
+
+/**
+ * Shows an alert whose buttons carry text of their own, which Lanterna's MessageDialog cannot do - its buttons are a fixed enum of OK, Cancel, Yes and
+ * so on. Mapping a button such as "Refresh Wallet" onto one of those loses the only description of what it does, and the role it declares is no
+ * substitute: the wallet refresh is a CANCEL_CLOSE button, so it would be offered as Cancel while doing the opposite of cancelling.
+ * <p>
+ * The button that was pressed is returned as the instance the caller supplied, since callers identify their own buttons by identity.
+ */
+public class ButtonTypeDialog extends DialogWindow {
+ private ButtonType result;
+
+ public ButtonTypeDialog(String title, String content, ButtonType[] buttonTypes) {
+ super(title);
+
+ setHints(List.of(Hint.CENTERED));
+
+ Panel buttonPanel = new Panel();
+ buttonPanel.setLayoutManager(new GridLayout(buttonTypes.length).setHorizontalSpacing(1));
+ for(ButtonType buttonType : buttonTypes) {
+ buttonPanel.addComponent(new Button(buttonType.getText(), () -> {
+ result = buttonType;
+ close();
+ }));
+ }
+
+ Panel mainPanel = new Panel();
+ mainPanel.setLayoutManager(new GridLayout(1).setLeftMarginSize(1).setRightMarginSize(1));
+ mainPanel.addComponent(new Label("\n" + content));
+ mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
+ buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false)).addTo(mainPanel);
+ setComponent(mainPanel);
+ }
+
+ @Override
+ public ButtonType showDialog(WindowBasedTextGUI textGUI) {
+ super.showDialog(textGUI);
+ return result;
+ }
+}
### src/main/java/com/sparrowwallet/sparrow/terminal/TerminalInteractionServices.java
@@ -35,6 +35,12 @@ public Optional<ButtonType> showAlert(String title, String content, Alert.AlertT
private Optional<ButtonType> showMessageDialog(String title, String content, ButtonType[] buttons) {
String formattedContent = formatLines(content, 50);
+ //A button carrying text of its own cannot be shown by MessageDialog, and is the only description of what it does, so those alerts get a
+ //dialog built from the supplied buttons directly. Keeping MessageDialog for the rest also keeps getButton injective over the buttons it maps
+ if(Arrays.stream(buttons).anyMatch(TerminalInteractionServices::isCustomButton)) {
+ return Optional.ofNullable(new ButtonTypeDialog(title, formattedContent, buttons).showDialog(SparrowTerminal.get().getGui()));
+ }
+
MessageDialogBuilder builder = new MessageDialogBuilder().setTitle(title).setText("\n" + formattedContent);
for(ButtonType buttonType : buttons) {
builder.addButton(getButton(buttonType));
@@ -44,6 +50,11 @@ private Optional<ButtonType> showMessageDialog(String title, String content, But
return Arrays.stream(buttons).filter(buttonType -> button.equals(getButton(buttonType))).findFirst();
}
+ private static boolean isCustomButton(ButtonType buttonType) {
+ return !ButtonType.OK.equals(buttonType) && !ButtonType.CANCEL.equals(buttonType) && !ButtonType.YES.equals(buttonType)
+ && !ButtonType.NO.equals(buttonType) && !ButtonType.CLOSE.equals(buttonType);
+ }
+
private String formatLines(String input, int maxLength) {
StringBuilder builder = new StringBuilder();
BreakIterator boundary = BreakIterator.getLineInstance(Locale.ROOT);Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.