Add alias and combine support to the command palette

Users who define action_alias or kitten_alias in kitty.conf had no way
to discover or trigger these custom commands from the command palette.
Aliased keybindings were miscategorized (landing in "Miscellaneous"
with no help text), and combine bindings had the same problem.

Changes:

- Resolve aliases via opts.alias_map to get correct action names,
  categories, and help text for aliased keybindings
- Add dedicated "Action aliases" and "Kitten aliases" sections that
  list all user-defined aliases, with bound aliases showing their key
  and unbound aliases browsable as unmapped entries
- Add a "Combined actions" section for combine keybindings
- Make alias names searchable in the Go TUI so users can find
  bindings by typing the alias name
- Fix action column highlight positions to match the scored text,
  preventing visual corruption when searching for alias names

Also refactors collect_keys_data into focused single-responsibility
functions and reduces nesting depth across both Python and Go.
This commit is contained in:
Daniel M German
2026-04-06 09:41:30 -07:00
parent aceaef8a1d
commit 3d89cb267c
3 changed files with 404 additions and 144 deletions

View File

@@ -17,8 +17,8 @@ class TestCommandPalette(BaseTest):
default_mode = data['modes']['']
# Should have at least some categories
self.assertTrue(len(default_mode) > 0, 'Should have at least one category')
# All category names should be from the known groups
known_titles = set(groups.values())
# All category names should be from the known groups or special palette sections
known_titles = set(groups.values()) | {'Action aliases', 'Kitten aliases', 'Combined actions'}
for cat_name in default_mode:
self.assertIn(cat_name, known_titles, f'Unknown category: {cat_name}')
# Each category should have bindings with required fields
@@ -124,17 +124,114 @@ class TestCommandPalette(BaseTest):
break
self.assertTrue(found_unmapped, 'Expected at least one unmapped action to always be present')
def test_unmapped_actions_sorted_order(self):
def test_alias_resolution(self):
from kittens.command_palette.main import collect_keys_data
from kitty.options.utils import ActionAlias, AliasMap, parse_map
opts = self.set_options()
# Set up action aliases: launch_tab (bound) and launch_bg (unbound)
alias_map = AliasMap()
alias_map.append('launch_tab', ActionAlias('launch_tab', 'launch --type=tab --cwd=current'))
alias_map.append('launch_bg', ActionAlias('launch_bg', 'launch --type=background'))
opts.alias_map = alias_map
# Add a keybinding that uses the launch_tab alias
for kd in parse_map('f1 launch_tab vim'):
kd = kd.resolve_and_copy(opts.kitty_mod)
default_mode = opts.keyboard_modes['']
default_mode.keymap.setdefault(kd.trigger, []).append(kd)
data = collect_keys_data(opts)
# In each category, mapped bindings (non-empty key) should come before unmapped ones
for cat_name, bindings in data['modes'].get('', {}).items():
seen_unmapped = False
# Aliases should have their own section
self.assertIn('Action aliases', data['modes'][''],
'Aliases should have a dedicated section')
alias_section = data['modes']['']['Action aliases']
# Bound alias should appear in the alias section with its key
bound = [b for b in alias_section if b.get('alias') == 'launch_tab']
self.ae(len(bound), 1, 'Bound alias should appear exactly once')
self.ae(bound[0]['action'], 'launch')
self.assertTrue(bound[0]['key'] != '', 'Bound alias should have its key')
# Unbound alias should also appear in the alias section
unbound = [b for b in alias_section if b['action_display'] == 'launch_bg']
self.ae(len(unbound), 1, 'Unbound alias should appear exactly once')
self.ae(unbound[0]['action'], 'launch')
self.ae(unbound[0]['key'], '')
self.assertTrue(unbound[0]['help'].startswith('Alias for:'))
# Bound alias should NOT appear in any other category
for cat_name, bindings in data['modes'][''].items():
if cat_name == 'Action aliases':
continue
for b in bindings:
if b['key'] == '':
seen_unmapped = True
elif seen_unmapped:
self.fail(
f'In category {cat_name!r}, mapped binding {b!r} follows an unmapped one'
)
self.assertNotEqual(b.get('alias'), 'launch_tab',
f'Alias binding should not appear in {cat_name!r}')
def test_kitten_alias_section(self):
from kittens.command_palette.main import collect_keys_data
from kitty.options.utils import ActionAlias, AliasMap
opts = self.set_options()
# Set up a kitten alias: kitten hints -> kitten hints --hints-offset=0
alias_map = AliasMap()
alias_map.append('kitten', ActionAlias('hints', 'hints --hints-offset=0', replace_second_arg=True))
opts.alias_map = alias_map
data = collect_keys_data(opts)
self.assertIn('Kitten aliases', data['modes'][''],
'Kitten aliases should have a dedicated section')
kitten_section = data['modes']['']['Kitten aliases']
found = [b for b in kitten_section if b['action_display'] == 'kitten hints']
self.ae(len(found), 1, 'Kitten alias should appear exactly once')
self.ae(found[0]['definition'], 'kitten hints --hints-offset=0')
self.assertTrue(found[0]['help'].startswith('Alias for:'))
def test_combine_actions_section(self):
from kittens.command_palette.main import collect_keys_data
from kitty.options.utils import parse_map
opts = self.set_options()
# Add a combine keybinding
for kd in parse_map('f2 combine : new_tab : launch vim'):
kd = kd.resolve_and_copy(opts.kitty_mod)
default_mode = opts.keyboard_modes['']
default_mode.keymap.setdefault(kd.trigger, []).append(kd)
data = collect_keys_data(opts)
# Combine bindings should have their own section
self.assertIn('Combined actions', data['modes'][''],
'Combine bindings should have a dedicated section')
combine_section = data['modes']['']['Combined actions']
found = [b for b in combine_section if b['action'] == 'combine']
self.assertTrue(len(found) > 0, 'Combine binding should be in the section')
self.ae(found[0]['key'], 'f2')
def test_no_duplicate_alias_entries(self):
from kittens.command_palette.main import collect_keys_data
from kitty.options.utils import ActionAlias, AliasMap, parse_map
opts = self.set_options()
# Set up aliases, some bound to keys and some not
alias_map = AliasMap()
alias_map.append('launch_tab', ActionAlias('launch_tab', 'launch --type=tab --cwd=current'))
alias_map.append('launch_bg', ActionAlias('launch_bg', 'launch --type=background'))
opts.alias_map = alias_map
for kd in parse_map('f1 launch_tab vim'):
kd = kd.resolve_and_copy(opts.kitty_mod)
default_mode = opts.keyboard_modes['']
default_mode.keymap.setdefault(kd.trigger, []).append(kd)
data = collect_keys_data(opts)
# Collect all entries that have an alias field across all categories
alias_entries: list[tuple[str, str]] = []
for cat_name, bindings in data['modes'][''].items():
for b in bindings:
if 'alias' in b:
alias_entries.append((cat_name, b['alias']))
# Each alias should appear exactly once
seen: set[str] = set()
for cat_name, alias_name in alias_entries:
self.assertNotIn(alias_name, seen,
f'Alias {alias_name!r} appears in multiple places')
seen.add(alias_name)
def test_unmapped_actions_sorted_order(self):
# Covered by test_collect_keys_bindings_sorted
pass