dissolve app/ into repo root (D-056)
Single Flutter package at the repo root. All code, tests, assets, and platform directories moved from app/ to root. Package renamed from clide_app to clide — all imports rewritten. Merged pubspec combines core (ffi) and app (flutter, yaml, xterm) dependencies. Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon lifecycle. 317 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Key combo: modifiers + primary key. Canonicalized on construction
|
||||
/// (modifiers sorted, lowercased) so equality works for lookup keys.
|
||||
@immutable
|
||||
class Keybinding {
|
||||
Keybinding({required Set<String> modifiers, required String key})
|
||||
: modifiers = _canonModifiers(modifiers),
|
||||
key = key.toLowerCase();
|
||||
|
||||
final List<String> modifiers;
|
||||
final String key;
|
||||
|
||||
static List<String> _canonModifiers(Set<String> m) {
|
||||
final normalized = m.map((s) => s.toLowerCase()).toSet().toList()..sort();
|
||||
return List.unmodifiable(normalized);
|
||||
}
|
||||
|
||||
/// Parse "ctrl+shift+g", "cmd+k", "alt+f4".
|
||||
static Keybinding parse(String spec) {
|
||||
if (spec.trim().isEmpty) {
|
||||
throw ArgumentError('empty keybinding');
|
||||
}
|
||||
final parts = spec.split('+').map((s) => s.trim()).toList();
|
||||
final key = parts.removeLast();
|
||||
if (key.isEmpty) {
|
||||
throw ArgumentError('keybinding is missing a key: "$spec"');
|
||||
}
|
||||
return Keybinding(modifiers: parts.toSet(), key: key);
|
||||
}
|
||||
|
||||
String get canonical {
|
||||
if (modifiers.isEmpty) return key;
|
||||
return '${modifiers.join('+')}+$key';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is Keybinding &&
|
||||
other.key == key &&
|
||||
listEquals(other.modifiers, modifiers);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(key, Object.hashAll(modifiers));
|
||||
|
||||
@override
|
||||
String toString() => 'Keybinding($canonical)';
|
||||
}
|
||||
|
||||
class KeybindingResolver {
|
||||
final Map<Keybinding, String> _bindings = {};
|
||||
|
||||
void bind(Keybinding b, String commandId) {
|
||||
_bindings[b] = commandId;
|
||||
}
|
||||
|
||||
void unbind(Keybinding b) {
|
||||
_bindings.remove(b);
|
||||
}
|
||||
|
||||
String? commandFor(Keybinding b) => _bindings[b];
|
||||
|
||||
Iterable<MapEntry<Keybinding, String>> get entries => _bindings.entries;
|
||||
|
||||
/// Map a Flutter [KeyEvent] to a [Keybinding] suitable for lookup.
|
||||
static Keybinding? fromKeyEvent(KeyEvent event, HardwareKeyboard keyboard) {
|
||||
if (event is! KeyDownEvent) return null;
|
||||
final label = event.logicalKey.keyLabel;
|
||||
if (label.isEmpty) return null;
|
||||
final mods = <String>{};
|
||||
if (keyboard.isControlPressed) mods.add('ctrl');
|
||||
if (keyboard.isShiftPressed) mods.add('shift');
|
||||
if (keyboard.isAltPressed) mods.add('alt');
|
||||
if (keyboard.isMetaPressed) mods.add('cmd');
|
||||
return Keybinding(modifiers: mods, key: label);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:clide/extension/src/contribution.dart';
|
||||
import 'package:clide/kernel/src/commands/registry.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class PaletteController extends ChangeNotifier {
|
||||
PaletteController(this._registry);
|
||||
|
||||
final CommandRegistry _registry;
|
||||
|
||||
bool _open = false;
|
||||
String _filter = '';
|
||||
|
||||
bool get isOpen => _open;
|
||||
String get filter => _filter;
|
||||
|
||||
void open() {
|
||||
if (_open) return;
|
||||
_open = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (!_open) return;
|
||||
_open = false;
|
||||
_filter = '';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggle() => _open ? close() : open();
|
||||
|
||||
void setFilter(String f) {
|
||||
if (_filter == f) return;
|
||||
_filter = f;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<CommandContribution> filtered() {
|
||||
if (_filter.isEmpty) return _registry.all.toList();
|
||||
final q = _filter.toLowerCase();
|
||||
return _registry.all.where((c) {
|
||||
final haystack = (c.title ?? c.command).toLowerCase();
|
||||
return haystack.contains(q);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<void> invoke(String command) async {
|
||||
close();
|
||||
await _registry.execute(command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:clide/clide.dart';
|
||||
import 'package:clide/extension/src/contribution.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class CommandRegistry extends ChangeNotifier {
|
||||
final Map<String, CommandContribution> _byCommand = {};
|
||||
|
||||
void register(CommandContribution cmd) {
|
||||
_byCommand[cmd.command] = cmd;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void unregister(String command) {
|
||||
if (_byCommand.remove(command) != null) notifyListeners();
|
||||
}
|
||||
|
||||
Iterable<CommandContribution> get all => _byCommand.values;
|
||||
CommandContribution? get(String command) => _byCommand[command];
|
||||
|
||||
Future<IpcResponse> execute(
|
||||
String command, {
|
||||
List<String> args = const [],
|
||||
}) async {
|
||||
final c = _byCommand[command];
|
||||
if (c == null) {
|
||||
return IpcResponse.err(
|
||||
id: '',
|
||||
error: IpcError(
|
||||
code: IpcExitCode.notFound,
|
||||
kind: IpcErrorKind.notFound,
|
||||
message: 'no such command: $command',
|
||||
),
|
||||
);
|
||||
}
|
||||
return c.run(args);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user