import 'dart:io';
const List<String> appstreamCategories = [
'AudioVideo',
'Development',
'Education',
'Game',
'Graphics',
'Network',
'Office',
'Science',
'Settings',
'System',
'Utility',
];
class CategoryIndex {
final Map<String, Set<String>> appstream;
final Map<String, Set<String>> comps;
CategoryIndex({required this.appstream, required this.comps});
Set<String> tagsFor(String packageName) {
return {...(appstream[packageName] ?? const {}), ...(comps[packageName] ?? const {})};
}
bool hasTag(String packageName, String tag) => tagsFor(packageName).contains(tag);
Set<String> get allCategoryLabels =>
{...appstream.values.expand((s) => s), ...comps.values.expand((s) => s)};
}
class CategoryService {
static Future<bool> isFullCatalogAvailable() async {
final result = await Process.run('rpm', ['-q', 'PackageKit']);
return result.exitCode == 0;
}
static Future<Map<String, Set<String>>> buildAppStreamIndex() async {
final index = <String, Set<String>>{};
final futures = appstreamCategories.map((category) async {
final result = await Process.run(
'appstreamcli',
['list-categories', '--details', category],
);
if (result.exitCode != 0) return;
for (final block in (result.stdout as String).split('\nIdentifier:')) {
final match = RegExp(r'^Package:\s*(\S+)', multiLine: true).firstMatch(block);
if (match != null) {
index.putIfAbsent(match.group(1)!, () => <String>{}).add(category);
}
}
});
await Future.wait(futures);
return index;
}
static Future<Map<String, Set<String>>> buildCompsIndex() async {
final listResult = await Process.run('dnf', ['group', 'list']);
if (listResult.exitCode != 0) {
throw Exception('dnf group list failed: ${listResult.stderr}');
}
final groupIds = <String>[];
for (final line in (listResult.stdout as String).split('\n')) {
final trimmed = line.trim();
if (trimmed.isEmpty) continue;
final firstToken = trimmed.split(RegExp(r'\s+')).first;
if (firstToken == 'ID') continue;
if (!RegExp(r'^[a-z0-9][a-z0-9-]*$').hasMatch(firstToken)) continue;
groupIds.add(firstToken);
}
final index = <String, Set<String>>{};
final futures = groupIds.map((id) async {
final infoResult = await Process.run('dnf', ['group', 'info', id]);
if (infoResult.exitCode != 0) return;
final lines = (infoResult.stdout as String).split('\n');
String? groupName;
bool inPackagesSection = false;
const packageHeaders = {'Mandatory packages', 'Default packages', 'Optional packages'};
for (final line in lines) {
final idx = line.indexOf(':');
if (idx == -1) continue;
final key = line.substring(0, idx).trim();
final value = line.substring(idx + 1).trim();
if (key == 'Name') {
groupName = value;
} else if (packageHeaders.contains(key)) {
inPackagesSection = true;
if (value.isNotEmpty) index.putIfAbsent(value, () => <String>{}).add(groupName ?? id);
} else if (key.isEmpty && inPackagesSection) {
if (value.isNotEmpty) index.putIfAbsent(value, () => <String>{}).add(groupName ?? id);
} else {
inPackagesSection = false;
}
}
});
await Future.wait(futures);
return index;
}
static Future<CategoryIndex> build() async {
final results = await Future.wait([buildAppStreamIndex(), buildCompsIndex()]);
return CategoryIndex(appstream: results[0], comps: results[1]);
}
}