scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
209 lines
6.2 KiB
Dart
209 lines
6.2 KiB
Dart
/// Generates lib/data/hid_key_labels.dart from the curated offline HID catalog.
|
|
///
|
|
/// Usage:
|
|
/// dart run scripts/codegen/generate_hid_key_labels.dart [input.json] [output.dart]
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
const defaultHidKeyLabelsInput = 'scripts/codegen/data/hid_key_labels.json';
|
|
const defaultHidKeyLabelsOutput = 'lib/data/hid_key_labels.dart';
|
|
|
|
final class HidKeyLabel {
|
|
const HidKeyLabel({required this.id, required this.label});
|
|
|
|
final String id;
|
|
final String label;
|
|
|
|
int get numericId => int.parse(id, radix: 16);
|
|
}
|
|
|
|
final class HidKeyGroup {
|
|
const HidKeyGroup({required this.name, required this.keys});
|
|
|
|
final String name;
|
|
final List<HidKeyLabel> keys;
|
|
}
|
|
|
|
final class HidKeyCatalog {
|
|
const HidKeyCatalog({required this.groups});
|
|
|
|
final List<HidKeyGroup> groups;
|
|
}
|
|
|
|
typedef AtomicFileWriter = Future<void> Function(String path, String contents);
|
|
|
|
HidKeyCatalog parseHidKeyLabelsCatalog(String source) {
|
|
final Object? decoded;
|
|
try {
|
|
decoded = jsonDecode(source);
|
|
} on FormatException catch (error) {
|
|
throw FormatException('Invalid HID catalog JSON: ${error.message}');
|
|
}
|
|
|
|
final root = _expectMap(decoded, 'catalog');
|
|
if (root['schemaVersion'] != 1) {
|
|
throw const FormatException('HID catalog schemaVersion must be 1');
|
|
}
|
|
|
|
final rawGroups = _expectList(root['groups'], 'groups');
|
|
if (rawGroups.isEmpty) {
|
|
throw const FormatException('HID catalog groups must not be empty');
|
|
}
|
|
|
|
final groups = <HidKeyGroup>[];
|
|
final groupNames = <String>{};
|
|
final ids = <String>{};
|
|
var previousId = -1;
|
|
|
|
for (var groupIndex = 0; groupIndex < rawGroups.length; groupIndex++) {
|
|
final rawGroup = _expectMap(rawGroups[groupIndex], 'groups[$groupIndex]');
|
|
final name = _expectNonemptyString(rawGroup['name'], 'groups[$groupIndex].name');
|
|
if (!groupNames.add(name)) {
|
|
throw FormatException('Duplicate HID group name: $name');
|
|
}
|
|
|
|
final rawKeys = _expectList(rawGroup['keys'], 'groups[$groupIndex].keys');
|
|
if (rawKeys.isEmpty) {
|
|
throw FormatException('HID group "$name" must contain at least one key');
|
|
}
|
|
|
|
final keys = <HidKeyLabel>[];
|
|
for (var keyIndex = 0; keyIndex < rawKeys.length; keyIndex++) {
|
|
final path = 'groups[$groupIndex].keys[$keyIndex]';
|
|
final rawKey = _expectMap(rawKeys[keyIndex], path);
|
|
final id = _expectString(rawKey['id'], '$path.id');
|
|
if (!RegExp(r'^[0-9a-f]{8}$').hasMatch(id)) {
|
|
throw FormatException('$path.id must be exactly eight lowercase hexadecimal digits');
|
|
}
|
|
if (!ids.add(id)) {
|
|
throw FormatException('Duplicate HID key ID: $id');
|
|
}
|
|
|
|
final numericId = int.parse(id, radix: 16);
|
|
if (numericId <= previousId) {
|
|
throw FormatException('HID key IDs must be in strictly increasing numeric order: $id');
|
|
}
|
|
previousId = numericId;
|
|
|
|
final label = _expectNonemptyString(rawKey['label'], '$path.label');
|
|
keys.add(HidKeyLabel(id: id, label: label));
|
|
}
|
|
groups.add(HidKeyGroup(name: name, keys: List.unmodifiable(keys)));
|
|
}
|
|
|
|
return HidKeyCatalog(groups: List.unmodifiable(groups));
|
|
}
|
|
|
|
String renderHidKeyLabels(HidKeyCatalog catalog) {
|
|
final output = StringBuffer()
|
|
..writeln(
|
|
'// Generated by dart run scripts/codegen/generate_hid_key_labels.dart from '
|
|
'scripts/codegen/data/hid_key_labels.json; do not edit by hand.',
|
|
)
|
|
..writeln()
|
|
..writeln('/// Human-readable labels for physical keyboard keys, keyed by USB HID usage code.')
|
|
..writeln('const hidKeyLabels = <int, String>{');
|
|
|
|
for (final group in catalog.groups) {
|
|
output.writeln(' // ${group.name}');
|
|
for (final key in group.keys) {
|
|
output.writeln(' 0x${key.id}: ${_dartString(key.label)},');
|
|
}
|
|
}
|
|
|
|
output.writeln('};');
|
|
return output.toString();
|
|
}
|
|
|
|
Future<void> writeFileAtomically(String path, String contents) async {
|
|
final output = File(path);
|
|
await output.parent.create(recursive: true);
|
|
final temporary = File('$path.tmp.$pid.${DateTime.now().microsecondsSinceEpoch}');
|
|
try {
|
|
await temporary.writeAsString(contents, flush: true);
|
|
await temporary.rename(path);
|
|
} finally {
|
|
if (await temporary.exists()) {
|
|
await temporary.delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> generateHidKeyLabels(
|
|
String inputPath,
|
|
String outputPath, {
|
|
AtomicFileWriter writer = writeFileAtomically,
|
|
}) async {
|
|
final source = await File(inputPath).readAsString();
|
|
final catalog = parseHidKeyLabelsCatalog(source);
|
|
final rendered = renderHidKeyLabels(catalog);
|
|
await writer(outputPath, rendered);
|
|
}
|
|
|
|
Future<void> main(List<String> arguments) async {
|
|
if (arguments.length > 2) {
|
|
stderr.writeln('Usage: dart run scripts/codegen/generate_hid_key_labels.dart [input.json] [output.dart]');
|
|
exitCode = 64;
|
|
return;
|
|
}
|
|
|
|
final inputPath = arguments.isEmpty ? defaultHidKeyLabelsInput : arguments[0];
|
|
final outputPath = arguments.length < 2 ? defaultHidKeyLabelsOutput : arguments[1];
|
|
await generateHidKeyLabels(inputPath, outputPath);
|
|
}
|
|
|
|
Map<String, Object?> _expectMap(Object? value, String path) {
|
|
if (value is! Map<String, Object?>) {
|
|
throw FormatException('$path must be a JSON object');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
List<Object?> _expectList(Object? value, String path) {
|
|
if (value is! List<Object?>) {
|
|
throw FormatException('$path must be a JSON array');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
String _expectString(Object? value, String path) {
|
|
if (value is! String) {
|
|
throw FormatException('$path must be a string');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
String _expectNonemptyString(Object? value, String path) {
|
|
final string = _expectString(value, path);
|
|
if (string.trim().isEmpty) {
|
|
throw FormatException('$path must not be empty');
|
|
}
|
|
return string;
|
|
}
|
|
|
|
String _dartString(String value) {
|
|
if (value == r'\') return r"r'\'";
|
|
|
|
if (value.contains("'")) {
|
|
final escaped = value
|
|
.replaceAll(r'\', r'\\')
|
|
.replaceAll('"', r'\"')
|
|
.replaceAll(r'$', r'\$')
|
|
.replaceAll('\r', r'\r')
|
|
.replaceAll('\n', r'\n')
|
|
.replaceAll('\t', r'\t');
|
|
return '"$escaped"';
|
|
}
|
|
|
|
final escaped = value
|
|
.replaceAll(r'\', r'\\')
|
|
.replaceAll("'", r"\'")
|
|
.replaceAll(r'$', r'\$')
|
|
.replaceAll('\r', r'\r')
|
|
.replaceAll('\n', r'\n')
|
|
.replaceAll('\t', r'\t');
|
|
return "'$escaped'";
|
|
}
|