feat(settings): Keymap category — preset select (T-451)

The keybindings-ui extension contributes a Keymap SettingsCategory: a preset
select (Default / Vim / VS Code / JetBrains) reading the active preset from
kKeymapPresetSetting. Picking one routes through a new schema affordance —
SettingsField.applyCommandPrefix — running `keymap.preset.<value>`, which calls
KeymapService.setPreset to persist and reload the layered keymap live. The
prefix path keeps the engine generic: settings a subsystem applies via a
command (rather than a bare key write) declare the prefix; the scope tag and
current-value read still use the key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:51:14 +02:00
co-authored by Claude Opus 4.8
parent 7aab93e776
commit 727fe8fdd4
8 changed files with 118 additions and 1 deletions
@@ -42,5 +42,39 @@ class KeybindingsUiExtension extends ClideExtension {
return IpcResponse.ok(id: '', data: {'preset': entry.key});
},
),
// Keymap settings category (T-451). The select reads the active preset
// from kKeymapPresetSetting; picking one runs `keymap.preset.<value>`
// (applyCommandPrefix), which calls KeymapService.setPreset — persisting
// and reloading the layered keymap live.
const SettingsCategoryContribution(
id: 'keymap',
category: SettingsCategory(
id: 'keymap',
title: 'Keymap',
iconName: 'keyboard',
priority: 20,
sections: [
SettingsSection(
label: 'Preset',
fields: [
SettingsField(
key: kKeymapPresetSetting,
kind: SettingsFieldKind.select,
label: 'Active preset',
help: 'Keyboard layout for the whole app.',
defaultValue: 'default',
applyCommandPrefix: 'keymap.preset.',
options: [
SettingsOption(value: 'default', label: 'Default'),
SettingsOption(value: 'vim', label: 'Vim'),
SettingsOption(value: 'vscode', label: 'VS Code'),
SettingsOption(value: 'jetbrains', label: 'JetBrains'),
],
),
],
),
],
),
),
];
}
@@ -206,7 +206,19 @@ class _Control extends StatelessWidget {
case SettingsFieldKind.toggle:
return _ToggleControl(checked: value == true, onChanged: _set);
case SettingsFieldKind.select:
return _SelectControl(field: field, value: value?.toString(), onPick: _set);
return _SelectControl(
field: field,
value: value?.toString(),
onPick: (v) {
final prefix = field.applyCommandPrefix;
if (prefix != null) {
// Value selects a command (the subsystem applies + persists).
ClideKernel.of(context).commands.execute('$prefix$v');
} else {
_set(v);
}
},
);
case SettingsFieldKind.text:
return _EditControl(field: field, value: value?.toString() ?? '', numeric: false, onCommit: _set);
case SettingsFieldKind.number:
+8
View File
@@ -52,6 +52,7 @@ class SettingsField {
this.min,
this.max,
this.fileCommand,
this.applyCommandPrefix,
});
final String key;
@@ -73,6 +74,13 @@ class SettingsField {
/// For [SettingsFieldKind.file]: the command id the row's button invokes.
final String? fileCommand;
/// For [SettingsFieldKind.select]: when set, picking option `<value>` runs
/// the command `<applyCommandPrefix><value>` instead of writing [key]
/// directly — for settings a subsystem applies via a command (and only then
/// persists). The current value is still read from [key], so the scope tag
/// and selection still work. Example: `'keymap.preset.'` → `keymap.preset.vim`.
final String? applyCommandPrefix;
}
/// A carded group of fields (surface.md "sectioned cards"). [label] is the