diff --git a/sqlit/core/state_base.py b/sqlit/core/state_base.py index d41197bc..139bf9fb 100644 --- a/sqlit/core/state_base.py +++ b/sqlit/core/state_base.py @@ -130,7 +130,10 @@ def allows( help_key=help_key or key, help_description=help, ) - if key and label: + # A label means the author wants this in the footer. The display + # key can be omitted here and resolved at render time via the + # global keymap (see ActionSpec.get_display_binding). + if label: if right: self._right_bindings.append(action_name) else: diff --git a/sqlit/domains/explorer/state/tree_focused.py b/sqlit/domains/explorer/state/tree_focused.py index 99731094..6fd292bb 100644 --- a/sqlit/domains/explorer/state/tree_focused.py +++ b/sqlit/domains/explorer/state/tree_focused.py @@ -18,7 +18,12 @@ def _setup_actions(self) -> None: self.allows("tree_cursor_down") # vim j self.allows("tree_cursor_up") # vim k self.allows("tree_filter", help="Filter items") - self.allows("enter_tree_visual_mode", label="Visual", help="Enter visual selection mode") + self.allows( + "enter_tree_visual_mode", + lambda app: app.tree_node_kind is not None, + label="Visual", + help="Enter visual selection mode", + ) def is_active(self, app: InputContext) -> bool: return app.focus == "explorer" and not app.tree_filter_active diff --git a/tests/ui/keybindings/test_state_machine.py b/tests/ui/keybindings/test_state_machine.py index 6bcf7de2..72569100 100644 --- a/tests/ui/keybindings/test_state_machine.py +++ b/tests/ui/keybindings/test_state_machine.py @@ -177,3 +177,39 @@ def test_collapse_all_only_in_tree_mode(self): value_view_tree_mode=False, ) assert sm.check_action(ctx, "collapse_all_json_nodes") is False + + +class TestEmptyExplorerFooter: + """Empty-state footer must guide a first-time user toward `n` for New.""" + + def test_new_connection_in_footer_when_explorer_focused_empty(self): + sm = UIStateMachine() + ctx = make_context(focus="explorer", tree_node_kind=None) + left, _right = sm.get_display_bindings(ctx) + actions = [b.action for b in left] + assert "new_connection" in actions, ( + "First-time users with no connections must see `n` for New " + "in the footer when the explorer is focused." + ) + + def test_refresh_tree_in_footer_when_explorer_focused_empty(self): + sm = UIStateMachine() + ctx = make_context(focus="explorer", tree_node_kind=None) + left, _right = sm.get_display_bindings(ctx) + actions = [b.action for b in left] + assert "refresh_tree" in actions + + def test_visual_mode_hidden_from_footer_when_tree_empty(self): + """Visual selection mode only makes sense when there's something to select.""" + sm = UIStateMachine() + ctx = make_context(focus="explorer", tree_node_kind=None) + left, _right = sm.get_display_bindings(ctx) + actions = [b.action for b in left] + assert "enter_tree_visual_mode" not in actions + + def test_visual_mode_shown_in_footer_when_node_highlighted(self): + sm = UIStateMachine() + ctx = make_context(focus="explorer", tree_node_kind="connection") + left, _right = sm.get_display_bindings(ctx) + actions = [b.action for b in left] + assert "enter_tree_visual_mode" in actions