Skip to content

Configure Navigation Keys

ListViewWidget and TreeViewWidget accept an item navigation preset. The default is lazygit-style movement with j/k and arrow-key aliases, but without count prefixes. Apps can provide another preset without subclassing widgets.

import lazy_cuh as lc
from lazy_cuh.widgets import ListViewWidget
preset = lc.presets.navigation.lazygit()
yield ListViewWidget(model, navigation=preset, id="items")

The preset bundles the runtime keymap and matching keybar hints:

preset.keymap
preset.keybar_hints

Use lazy_vim() when a surface should accept counted movement such as 5j. Line-number mode is separate view state; choosing a movement preset does not toggle line numbers.

vim_preset = lc.presets.navigation.lazy_vim(include_select=True)
tree.set_navigation(vim_preset)
list_view.set_navigation(vim_preset)

For lower-level control, pass a raw NavigableKeymap as keymap=....

from lazy_cuh.widgets import NavigableKeymap
keymap = NavigableKeymap(
down=("n",),
up=("p",),
first_prefix=("f",),
first=("f",),
last=("end",),
select=("o",),
)

This maps:

  • n to move down
  • p to move up
  • f f to jump to the first item
  • end to jump to the last item
  • o to select the current item

Use ItemActionMessage when the app needs semantic item actions.

from lazy_cuh.widgets import ItemActionMessage, NavigableAction
def on_item_action_message(self, message: ItemActionMessage) -> None:
if message.action == NavigableAction.SELECT:
self.open_item(message.item_id)

ItemSelectedMessage is still emitted for selection compatibility, but ItemActionMessage is the better fit when an app wants one event shape for select, expand, collapse, and toggle.

Action bindings may use key specs for configurable leader-style keys. Resolution and display are separate: KeyAliasMap expands a named leader token for input handling, while KeyDisplayMap controls how the same binding appears in keybars and help text.

import lazy_cuh as lc
actions = lc.inputs.ActionMap(
(
lc.inputs.ActionBinding.from_key_spec(
"<leader> h",
lc.inputs.ActionId.SELECT,
label="Pane left",
visibility=lc.inputs.HintVisibility.COMPACT,
),
)
)
aliases = lc.inputs.KeyAliasMap.from_mapping({"leader": ("z",)})
key_display = lc.inputs.KeyDisplayMap(aliases=aliases)
spec = lc.keybar.from_actions(actions, key_display=key_display)

That keybar renders Pane left: z h. Without a configured display map, the same binding renders symbolically as Pane left: <leader> h.

When multiple keys mean the same thing, keep them on one binding with ActionBinding.from_keys(...). That produces one help row such as Left: h/left instead of duplicate rows for the same action.

Count prefixes are opt-in. The framework does not reserve number keys globally. Use VIM_NAVIGATION_KEYS only for the scopes that should treat leading digits as counts, and use SIMPLE_KEYS where bare numbers should remain normal bindings.

import lazy_cuh as lc
registry = lc.inputs.BindingRegistry(
bindings=(
lc.inputs.ScopedBinding(
lc.inputs.ActionBinding.from_key("1", lc.inputs.ActionId.SELECT),
context=lc.inputs.BindingContext(panel="tabs"),
profile=lc.inputs.SIMPLE_KEYS,
),
lc.inputs.ScopedBinding(
lc.inputs.ActionBinding.from_key("j", lc.inputs.ActionId.SELECT),
context=lc.inputs.BindingContext(widget="tree"),
profile=lc.inputs.VIM_NAVIGATION_KEYS,
),
)
)
diagnostics = registry.validate()

With VIM_NAVIGATION_KEYS, a bare 1 binding reports a warning because it may conflict with count input such as 10j. Leader sequences such as z 1 are still valid because the digit is no longer a bare leading count.

Validation is explicit. The framework does not automatically validate every registry during app startup. Run BindingRegistry.validate() in tests, CI, or a development-only startup check, then decide whether warnings should fail your app.

diagnostics = registry.validate()
for line in lc.inputs.format_binding_diagnostics(diagnostics):
print(line)

Diagnostics include duplicate bindings, global/local override conflicts, ambiguous sequence prefixes, unknown symbolic leader tokens, and bare digit bindings inside count-enabled scopes.

Use collision_policy=CollisionPolicy.WARN when duplicate/override/prefix diagnostics should be warnings instead of errors. Use allow_override=True on a local ScopedBinding when a local key intentionally takes over a global key. Use CollisionPolicy.ALLOW_OVERRIDE only when that is the desired default for the whole registry.

The widget keymap decides how to navigate and which UI action happened. It does not decide what selecting an item means in your app. App controllers should translate item actions into commands when side effects are needed.