import os
with open('lib/core/theme/theme_registry_service.dart', 'r') as f:
content = f.read()
# 1. Imports
target_imports = """import 'theme_metadata.dart';
import 'theme_paths.dart';"""
new_imports = """import 'theme_metadata.dart';
import 'theme_paths.dart';
import '../extensions/local_extension_registry.dart';
import '../extensions/extension_paths.dart';
import '../extensions/models/extension_manifest.dart';
import '../extensions/models/extension_type.dart';"""
content = content.replace(target_imports, new_imports)
# 2. Add flag
target_flag = """ final List _bundledThemeAssetFiles;
final _ThemeLruCache _themeCache;
int _themeParseCount = 0;"""
new_flag = """ final List _bundledThemeAssetFiles;
final _ThemeLruCache _themeCache;
int _themeParseCount = 0;
bool _hasMigratedThemes = false;"""
content = content.replace(target_flag, new_flag)
# 3. loadThemeDefinitions
target_load = """ Future loadThemeDefinitions() async {
final definitions = [];
await _loadBuiltinAssetDefinitions(definitions);
await _scanDirectory(
await _userThemesDirectory(),
ThemeSource.filesystem,
definitions,
);
await _scanDirectory(
await _importedThemesDirectory(),
ThemeSource.imported,
definitions,
);
final legacy = await _legacyImportedDefinition();
if (legacy != null) {
definitions.removeWhere(
(definition) =>
definition.path == legacy.path &&
definition.source != ThemeSource.legacyImported,
);
definitions.add(legacy);
}
definitions.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return List.unmodifiable(definitions);
}"""
new_load = """ Future loadThemeDefinitions() async {
final definitions = [];
await _loadBuiltinAssetDefinitions(definitions);
if (!_hasMigratedThemes) {
await _migrateLegacyThemesToExtensions();
_hasMigratedThemes = true;
}
await LocalExtensionRegistry.instance.load();
for (final manifest in LocalExtensionRegistry.instance.manifests) {
if (manifest.type != ExtensionType.theme) continue;
final installPath = manifest.installPath;
final mainFile = manifest.main;
if (installPath == null || mainFile == null) continue;
final file = File(p.join(installPath, mainFile));
if (!await file.exists()) continue;
final definition = await _definitionFromFile(file, ThemeSource.filesystem);
if (definition != null) {
definitions.add(definition);
}
}
final legacy = await _legacyImportedDefinition();
if (legacy != null) {
definitions.removeWhere(
(definition) =>
definition.path == legacy.path &&
definition.source != ThemeSource.legacyImported,
);
definitions.add(legacy);
}
definitions.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return List.unmodifiable(definitions);
}
Future _migrateLegacyThemesToExtensions() async {
final dirs = [
await _userThemesDirectory(),
await _importedThemesDirectory(),
];
final extensionsDir = await ExtensionPaths.ensureExtensionsDirectory();
for (final directory in dirs) {
if (!await directory.exists()) continue;
await for (final entity in directory.list(followLinks: false)) {
if (entity is! File) continue;
if (p.basename(entity.path) == ThemeImportService.storedFileName) continue;
final ext = p.extension(entity.path).toLowerCase();
if (ext != '.json' && ext != '.jsonc') continue;
try {
final definition = await _definitionFromFile(entity, ThemeSource.filesystem);
if (definition == null) continue;
final slug = ThemeImportService.slugifyThemeName(definition.name);
var finalExtDir = Directory(p.join(extensionsDir.path, slug));
var counter = 2;
while (await finalExtDir.exists()) {
finalExtDir = Directory(p.join(extensionsDir.path, '$slug-$counter'));
counter++;
}
await finalExtDir.create(recursive: true);
final themeFile = File(p.join(finalExtDir.path, 'theme.json'));
await entity.copy(themeFile.path);
final manifest = ExtensionManifest(
id: definition.id,
name: definition.name,
version: '1.0.0',
publisher: 'Unknown',
type: ExtensionType.theme,
engines: const {'querya_desktop': '*'},
main: 'theme.json',
description: 'Migrated custom theme',
);
final manifestFile = File(p.join(finalExtDir.path, 'manifest.json'));
await manifestFile.writeAsString(jsonEncode(manifest.toJson()));
await entity.delete(); // Delete old file to complete migration
} catch (_) {}
}
}
// Reload extensions since we might have added new ones
await LocalExtensionRegistry.instance.reload();
}"""
content = content.replace(target_load, new_load)
# 4. importThemeFile
target_import = """ /// Validates [sourcePath], copies into the user themes directory, and returns
/// the scanned [ThemeDefinition].
Future importThemeFile(String sourcePath) async {
try {
final source = File(sourcePath);
if (!await source.exists()) {
return const ThemeDefinitionImportFailure('Theme file not found.');
}
final raw = await source.readAsString();
final hash = _contentHash(raw);
final json = _decodeRoot(raw);
if (json == null) {
return const ThemeDefinitionImportFailure('Invalid JSON.');
}
final themesDir = await _userThemesDirectory();
if (!await themesDir.exists()) {
await themesDir.create(recursive: true);
}
final schema = json['schema']?.toString();
late final String logicalId;
late final String preferredBaseName;
late String contentToWrite;
if (schema == queryaThemeSchemaV1) {
final manifest = QueryaThemeManifest.fromJsonString(raw);
logicalId = manifest.id;
preferredBaseName = ThemeImportService.safeThemeFileBase(manifest.id);
contentToWrite = raw;
} else {
final manifest = VsCodeThemeManifest.fromJsonString(raw);
if (manifest.colors.isEmpty) {
return const ThemeDefinitionImportFailure(
'Theme file has no "colors" section to import.',
);
}
final displayName = manifest.name?.trim().isNotEmpty == true
? manifest.name!.trim()
: p.basenameWithoutExtension(sourcePath);
preferredBaseName = ThemeImportService.slugifyThemeName(displayName);
logicalId = preferredBaseName;
contentToWrite = raw;
}
var resolved = await _resolveImportDestination(
themesDir: themesDir,
hash: hash,
logicalId: logicalId,
preferredBaseName: preferredBaseName,
);
if (!resolved.reused &&
schema == queryaThemeSchemaV1 &&
resolved.renamedId != null) {
contentToWrite = _rewriteCustomThemeId(raw, resolved.renamedId!);
}
if (!resolved.reused) {
await resolved.file.writeAsString(contentToWrite);
}
final definition =
await _definitionFromFile(resolved.file, ThemeSource.filesystem);
if (definition == null) {
return const ThemeDefinitionImportFailure(
'Failed to index imported theme.',
);
}
return ThemeDefinitionImportSuccess(
definition: definition,
reusedExisting: resolved.reused,
);
} on QueryaThemeManifestParseException catch (e) {
return ThemeDefinitionImportFailure(e.message);
} on VsCodeThemeParseException catch (e) {
return ThemeDefinitionImportFailure(e.message);
} on IOException catch (e) {
return ThemeDefinitionImportFailure(e.toString());
} on Object catch (e) {
return ThemeDefinitionImportFailure(e.toString());
}
}"""
new_import = """ /// Validates [sourcePath], creates an extension directory, and returns
/// the scanned [ThemeDefinition].
Future importThemeFile(String sourcePath) async {
try {
final source = File(sourcePath);
if (!await source.exists()) {
return const ThemeDefinitionImportFailure('Theme file not found.');
}
final raw = await source.readAsString();
final json = _decodeRoot(raw);
if (json == null) {
return const ThemeDefinitionImportFailure('Invalid JSON.');
}
final extensionsDir = await ExtensionPaths.ensureExtensionsDirectory();
final schema = json['schema']?.toString();
late final String logicalId;
late final String preferredBaseName;
late String contentToWrite;
if (schema == queryaThemeSchemaV1) {
final manifest = QueryaThemeManifest.fromJsonString(raw);
logicalId = manifest.id;
preferredBaseName = ThemeImportService.safeThemeFileBase(manifest.id);
contentToWrite = raw;
} else {
final manifest = VsCodeThemeManifest.fromJsonString(raw);
if (manifest.colors.isEmpty) {
return const ThemeDefinitionImportFailure(
'Theme file has no "colors" section to import.',
);
}
final displayName = manifest.name?.trim().isNotEmpty == true
? manifest.name!.trim()
: p.basenameWithoutExtension(sourcePath);
preferredBaseName = ThemeImportService.slugifyThemeName(displayName);
logicalId = preferredBaseName;
contentToWrite = raw;
}
await LocalExtensionRegistry.instance.load();
bool reused = false;
String? existingInstallPath;
for (final extManifest in LocalExtensionRegistry.instance.manifests) {
if (extManifest.type == ExtensionType.theme && extManifest.id == logicalId) {
reused = true;
existingInstallPath = extManifest.installPath;
break;
}
}
File resolvedFile;
if (reused && existingInstallPath != null) {
resolvedFile = File(p.join(existingInstallPath, 'theme.json'));
} else {
var finalExtDir = Directory(p.join(extensionsDir.path, preferredBaseName));
var counter = 2;
while (await finalExtDir.exists()) {
finalExtDir = Directory(p.join(extensionsDir.path, '$preferredBaseName-$counter'));
counter++;
}
await finalExtDir.create(recursive: true);
resolvedFile = File(p.join(finalExtDir.path, 'theme.json'));
await resolvedFile.writeAsString(contentToWrite);
final newManifest = ExtensionManifest(
id: logicalId,
name: preferredBaseName,
version: '1.0.0',
publisher: 'Unknown',
type: ExtensionType.theme,
engines: const {'querya_desktop': '*'},
main: 'theme.json',
description: 'Imported custom theme',
);
final manifestFile = File(p.join(finalExtDir.path, 'manifest.json'));
await manifestFile.writeAsString(jsonEncode(newManifest.toJson()));
await LocalExtensionRegistry.instance.reload();
}
final definition = await _definitionFromFile(resolvedFile, ThemeSource.filesystem);
if (definition == null) {
return const ThemeDefinitionImportFailure('Failed to index imported theme.');
}
return ThemeDefinitionImportSuccess(
definition: definition,
reusedExisting: reused,
);
} on QueryaThemeManifestParseException catch (e) {
return ThemeDefinitionImportFailure(e.message);
} on VsCodeThemeParseException catch (e) {
return ThemeDefinitionImportFailure(e.message);
} on IOException catch (e) {
return ThemeDefinitionImportFailure(e.toString());
} on Object catch (e) {
return ThemeDefinitionImportFailure(e.toString());
}
}"""
content = content.replace(target_import, new_import)
# 5. Remove unused methods using python block extracting instead of regex
# Or just simple replace
import re
to_remove = [
(r' Future _scanDirectory\(', r'^\s*\}\s*$', 3),
(r' Future _resolveImportDestination\(', r'^\s*\}\s*$', 3),
(r' Future _nextRenamedThemeId\(', r'^\s*\}\s*$', 3),
(r' Future _themeIdExists\(', r'^\s*\}\s*$', 3),
(r' Future _nextAvailableThemeFile\(', r'^\s*\}\s*$', 3),
(r' String _rewriteCustomThemeId\(', r'^\s*\}\s*$', 3),
(r'class _ResolvedImportDestination \{', r'^\}\s*$', 0),
]
lines = content.split('\n')
for start_regex, end_regex, indent in to_remove:
start_idx = -1
end_idx = -1
for i, line in enumerate(lines):
if re.search(start_regex, line):
start_idx = i
break
if start_idx != -1:
# find matching closing brace
brace_count = 0
started = False
for i in range(start_idx, len(lines)):
brace_count += lines[i].count('{')
brace_count -= lines[i].count('}')
if '{' in lines[i]:
started = True
if started and brace_count == 0:
end_idx = i
break
if end_idx != -1:
lines[start_idx:end_idx+1] = []
content = '\n'.join(lines)
with open('lib/core/theme/theme_registry_service.dart', 'w') as f:
f.write(content)
print("Patch applied successfully.")