mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 16:37:27 +00:00
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.
237 lines
11 KiB
Python
237 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
from . import BaseTest
|
|
|
|
|
|
class TestCommandPalette(BaseTest):
|
|
|
|
def test_collect_keys_data(self):
|
|
from kittens.command_palette.main import collect_keys_data
|
|
from kitty.actions import groups
|
|
opts = self.set_options()
|
|
data = collect_keys_data(opts)
|
|
self.assertIn('modes', data)
|
|
self.assertIn('mouse', data)
|
|
self.assertIn('', data['modes'], 'Default keyboard mode should be present')
|
|
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 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
|
|
for cat_name, bindings in default_mode.items():
|
|
self.assertIsInstance(bindings, list)
|
|
for b in bindings:
|
|
self.assertIn('key', b)
|
|
self.assertIn('action', b)
|
|
self.assertIn('action_display', b)
|
|
self.assertIn('definition', b)
|
|
self.assertIn('help', b)
|
|
self.assertIn('long_help', b)
|
|
self.assertIsInstance(b['key'], str)
|
|
self.assertIsInstance(b['action'], str)
|
|
# key may be empty for unmapped actions; action must always be non-empty
|
|
self.assertTrue(len(b['action']) > 0)
|
|
# Mouse mappings
|
|
self.assertIsInstance(data['mouse'], list)
|
|
for b in data['mouse']:
|
|
self.assertIn('key', b)
|
|
self.assertIn('action', b)
|
|
self.assertIn('action_display', b)
|
|
|
|
def test_collect_keys_categories_ordered(self):
|
|
from kittens.command_palette.main import collect_keys_data
|
|
from kitty.actions import groups
|
|
opts = self.set_options()
|
|
data = collect_keys_data(opts)
|
|
default_mode = data['modes']['']
|
|
cat_names = list(default_mode.keys())
|
|
group_titles = list(groups.values())
|
|
# Categories should appear in the same order as defined in groups
|
|
indices = []
|
|
for cat in cat_names:
|
|
if cat in group_titles:
|
|
indices.append(group_titles.index(cat))
|
|
self.ae(indices, sorted(indices), 'Categories should be ordered according to groups dict')
|
|
|
|
def test_collect_keys_bindings_sorted(self):
|
|
from kittens.command_palette.main import collect_keys_data
|
|
opts = self.set_options()
|
|
data = collect_keys_data(opts)
|
|
# Within each category, mapped entries (non-empty key) come first sorted by key,
|
|
# then unmapped entries (empty key) sorted by action name.
|
|
for cat_name, bindings in data['modes'][''].items():
|
|
seen_unmapped = False
|
|
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'
|
|
)
|
|
|
|
def test_collect_keys_has_help_text(self):
|
|
from kittens.command_palette.main import collect_keys_data
|
|
opts = self.set_options()
|
|
data = collect_keys_data(opts)
|
|
# At least some bindings should have help text
|
|
has_help = False
|
|
for cat_name, bindings in data['modes'][''].items():
|
|
for b in bindings:
|
|
if b['help']:
|
|
has_help = True
|
|
break
|
|
if has_help:
|
|
break
|
|
self.assertTrue(has_help, 'At least some bindings should have help text')
|
|
|
|
def test_ordering_arrays_present(self):
|
|
from kittens.command_palette.main import collect_keys_data
|
|
opts = self.set_options()
|
|
data = collect_keys_data(opts)
|
|
# mode_order should list all modes
|
|
self.assertIn('mode_order', data)
|
|
self.assertIsInstance(data['mode_order'], list)
|
|
self.ae(set(data['mode_order']), set(data['modes'].keys()))
|
|
# category_order should list categories for each mode
|
|
self.assertIn('category_order', data)
|
|
self.assertIsInstance(data['category_order'], dict)
|
|
for mode_name in data['modes']:
|
|
self.assertIn(mode_name, data['category_order'])
|
|
self.ae(
|
|
set(data['category_order'][mode_name]),
|
|
set(data['modes'][mode_name].keys()),
|
|
f'category_order for mode {mode_name!r} should match modes keys',
|
|
)
|
|
|
|
def test_always_includes_unmapped_actions(self):
|
|
from kittens.command_palette.main import collect_keys_data
|
|
opts = self.set_options()
|
|
data = collect_keys_data(opts)
|
|
# Unmapped actions (empty key) are always included
|
|
found_unmapped = False
|
|
for cats in data['modes'].values():
|
|
for bindings in cats.values():
|
|
for b in bindings:
|
|
if b['key'] == '':
|
|
found_unmapped = True
|
|
# Unmapped actions must still have action and definition
|
|
self.assertTrue(len(b['action']) > 0)
|
|
self.assertTrue(len(b['definition']) > 0)
|
|
break
|
|
self.assertTrue(found_unmapped, 'Expected at least one unmapped action to always be present')
|
|
|
|
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)
|
|
|
|
# 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:
|
|
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
|