add Zed-style application menu bar: File / View / Help (T-48)
A custom in-window menu bar in the hat (no native menu, D-7), built from the command registry so it stays in sync and satisfies D-6 parity. - Menu model + hybrid resolver (menu_model.dart): a curated File/View/Help tree where a MenuAutoFill node sweeps in unplaced view.* commands; titles + keybindings come from the registry/keymap; unregistered or enabledWhen-false items render disabled (greyed), never hidden. - Widgets: MenuBar row in the hat (chrome tokens), anchored MenuDropdown overlay (dropdown tokens), two-column MenuItemRow with inline keybinding. - Full keyboard: Alt+mnemonic opens (hook in _RootShell._onKey), arrows navigate, Enter activates, Esc closes, Left/Right switch menus. - Commands: file.openFolder / file.newWindow / file.closeWorkspace / help.about, registered by MenuBarExtension(services:). File logic lifted out of the project switcher into FileActions (one source of truth; the switcher now dispatches the commands). Ctrl+O / Ctrl+Shift+N are now real keybindings in default.yaml. - Help → About: version/commit/date/repo from build-info + the bundled dependency licenses parsed from assets/licenses.yaml. Edit/Selection menus are deferred to T-271/T-272 (need focused-surface command routing). Tests: resolver + controller + licenses parse (pure); menu-bar widget (open/close/execute/disabled/Esc/arrow/Enter/Left-Right); FileActions + Open dialog; app-level Alt+F, non-repo dialog, and closeWorkspace. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/// Loads the bundled `assets/licenses.yaml` manifest for the About dialog
|
||||
/// (T-48 / D-42). The runtime `dependencies:` list is what the About screen
|
||||
/// renders; `self:` carries clide's own license.
|
||||
library;
|
||||
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class SelfEntry {
|
||||
const SelfEntry({required this.name, required this.version, required this.license});
|
||||
final String name;
|
||||
final String version;
|
||||
final String license;
|
||||
}
|
||||
|
||||
class DepEntry {
|
||||
const DepEntry({required this.name, required this.version, required this.license});
|
||||
final String name;
|
||||
final String version;
|
||||
final String license;
|
||||
}
|
||||
|
||||
class LicensesManifest {
|
||||
const LicensesManifest({required this.self, required this.dependencies});
|
||||
final SelfEntry self;
|
||||
final List<DepEntry> dependencies;
|
||||
}
|
||||
|
||||
/// Parse the licenses-manifest YAML text. Pure (no asset bundle) so it's
|
||||
/// directly unit-testable. Missing fields degrade to '—' rather than throwing.
|
||||
LicensesManifest parseLicenses(String yamlText) {
|
||||
final doc = loadYaml(yamlText);
|
||||
final map = doc is YamlMap ? doc : const {};
|
||||
String s(Object? v) => v == null ? '—' : '$v';
|
||||
|
||||
final selfRaw = map['self'];
|
||||
final selfMap = selfRaw is YamlMap ? selfRaw : const {};
|
||||
final self = SelfEntry(name: s(selfMap['name']), version: s(selfMap['version']), license: s(selfMap['license']));
|
||||
|
||||
final deps = <DepEntry>[];
|
||||
final list = map['dependencies'];
|
||||
if (list is YamlList) {
|
||||
for (final e in list) {
|
||||
if (e is YamlMap) {
|
||||
deps.add(DepEntry(name: s(e['name']), version: s(e['version']), license: s(e['license'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
return LicensesManifest(self: self, dependencies: deps);
|
||||
}
|
||||
|
||||
LicensesManifest? _cache;
|
||||
|
||||
/// Load + parse the bundled manifest, cached after the first read.
|
||||
Future<LicensesManifest> loadLicenses() async {
|
||||
final cached = _cache;
|
||||
if (cached != null) return cached;
|
||||
final raw = await rootBundle.loadString('assets/licenses.yaml');
|
||||
return _cache = parseLicenses(raw);
|
||||
}
|
||||
Reference in New Issue
Block a user