Created
March 28, 2026 18:57
-
-
Save rcdailey/5c37bc152bd792e8ee2f2c6149fe2dd4 to your computer and use it in GitHub Desktop.
Workaround for Terminal.Gui OptionSelector Space-cycling and initial visual desync
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Workaround for OptionSelector Space-cycling and initial visual desync. | |
| // | |
| // Problem: OptionSelector always calls Cycle() when Space is pressed on the | |
| // currently selected CheckBox. There's no way to disable this. Additionally, | |
| // OptionSelector defaults Value=0 but doesn't call UpdateChecked(), so child | |
| // CheckBoxes are visually unchecked even though the selector thinks the first | |
| // item is selected. | |
| // | |
| // Solution: Intercept Space at the app keyboard level, suppress it entirely | |
| // for OptionSelectors, and manually set the value or force the visual sync. | |
| // In your app setup: | |
| app.Keyboard.KeyDown += OnKeyDown; | |
| // Key handler (Enter/Esc are wizard-specific; the Space case is the workaround) | |
| private void OnKeyDown(object? sender, Key key) | |
| { | |
| if (sender is not IKeyboard { App.TopRunnableView: WizardMainView wizard }) | |
| { | |
| return; | |
| } | |
| // ... other key handling ... | |
| if (key == Key.Space) | |
| { | |
| SelectFocusedOption(wizard); | |
| key.Handled = true; | |
| } | |
| } | |
| // Select the focused option without triggering Cycle(). | |
| // When the value is already correct but CheckBoxes are visually out of sync | |
| // (initial load where Value=0 is the default but no CheckBox is checked), | |
| // UpdateChecked() forces the visual state to match. | |
| private void SelectFocusedOption(View root) | |
| { | |
| var selector = FindFocusedOptionSelector(root); | |
| if (selector is not { Focused: CheckBox focused }) | |
| { | |
| return; | |
| } | |
| var targetValue = selector.GetCheckBoxValue(focused); | |
| if (selector.Value != targetValue) | |
| { | |
| selector.Value = targetValue; | |
| } | |
| else | |
| { | |
| // Value matches but CheckBox may be unchecked visually | |
| selector.UpdateChecked(); | |
| } | |
| } | |
| // Recursive tree walk to find the OptionSelector that currently has focus. | |
| // HasFocus propagates up the hierarchy, so the OptionSelector itself reports | |
| // HasFocus=true when any child CheckBox is focused. | |
| private static OptionSelector? FindFocusedOptionSelector(View root) | |
| { | |
| foreach (var child in root.SubViews) | |
| { | |
| if (child is OptionSelector { HasFocus: true } selector) | |
| { | |
| return selector; | |
| } | |
| if (FindFocusedOptionSelector(child) is { } found) | |
| { | |
| return found; | |
| } | |
| } | |
| return null; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment