layer-wizard/lib/confirmation_marker.dart
Philip Peterson e0cbf8ae20 Initial commit: Layer Wizard
A GUI installer wizard for permanent rpm-ostree package layers on
bootc/image-based systems. Walks through: welcome/current-version check,
package name entry with existence check, confirmation, then a streamed
rpm-ostree install via pkexec. On success, writes a pending-confirmation
marker and installs an autostart entry so the next login shows a
"did this update work?" screen with a one-click rollback (bootc rollback
--apply) if not.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-06 05:25:05 -08:00

88 lines
2.6 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'ostree_service.dart';
class PendingConfirmation {
final String packageName;
final DeploymentInfo before;
final DateTime installedAt;
PendingConfirmation({
required this.packageName,
required this.before,
required this.installedAt,
});
}
class ConfirmationMarker {
static String get _stateDir => '${Platform.environment['HOME']}/.local/state/layer_wizard';
static String get _markerPath => '$_stateDir/pending_confirmation.json';
static String get _autostartPath =>
'${Platform.environment['HOME']}/.config/autostart/com.ironmagma.layer_wizard.desktop';
static Future<void> write(String packageName, DeploymentInfo before) async {
final dir = Directory(_stateDir);
if (!await dir.exists()) {
await dir.create(recursive: true);
}
final json = jsonEncode({
'packageName': packageName,
'before': before.toJson(),
'installedAt': DateTime.now().toIso8601String(),
});
await File(_markerPath).writeAsString(json);
}
static Future<PendingConfirmation?> read() async {
final file = File(_markerPath);
if (!await file.exists()) return null;
try {
final data = jsonDecode(await file.readAsString()) as Map<String, dynamic>;
return PendingConfirmation(
packageName: data['packageName'] as String,
before: DeploymentInfo.fromJson(data['before'] as Map<String, dynamic>),
installedAt: DateTime.parse(data['installedAt'] as String),
);
} catch (_) {
// Corrupt/unreadable marker — treat as if nothing is pending rather
// than getting the user stuck on a confirmation screen we can't render.
return null;
}
}
static Future<void> clear() async {
final file = File(_markerPath);
if (await file.exists()) {
await file.delete();
}
}
/// Installs an autostart entry pointing at the currently-running binary,
/// so the confirmation screen is shown automatically right after the next
/// login. Idempotent.
static Future<void> installAutostart() async {
final dir = Directory('${Platform.environment['HOME']}/.config/autostart');
if (!await dir.exists()) {
await dir.create(recursive: true);
}
final exe = Platform.resolvedExecutable;
final contents = '''
[Desktop Entry]
Type=Application
Name=Layer Wizard Confirmation
Comment=Confirm a pending permanent package layer
Exec=$exe
X-GNOME-Autostart-enabled=true
NoDisplay=true
''';
await File(_autostartPath).writeAsString(contents);
}
static Future<void> removeAutostart() async {
final file = File(_autostartPath);
if (await file.exists()) {
await file.delete();
}
}
}