dissolve app/ into repo root (D-056)

Single Flutter package at the repo root. All code, tests, assets,
and platform directories moved from app/ to root. Package renamed
from clide_app to clide — all imports rewritten. Merged pubspec
combines core (ffi) and app (flutter, yaml, xterm) dependencies.
Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon
lifecycle. 317 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 00:37:20 +02:00
co-authored by Claude Opus 4.6
parent a526c5b9b7
commit 46329700d5
394 changed files with 978 additions and 1090 deletions
+108
View File
@@ -0,0 +1,108 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
typedef DialogBuilder<T> = Widget Function(
BuildContext context,
void Function([T? result]) dismiss,
);
/// Single-at-a-time modal router.
///
/// Extensions call [show] with a builder; the root widget (installed by
/// [DialogHost]) listens and renders the current dialog over a dimmed
/// backdrop. Only one dialog is active at a time — a second [show] call
/// while one is open awaits until the first dismisses.
class DialogRouter extends ChangeNotifier {
DialogBuilder<Object?>? _current;
Completer<Object?>? _completer;
final List<_Queued> _queue = [];
DialogBuilder<Object?>? get current => _current;
bool get isOpen => _current != null;
Future<T?> show<T extends Object>(DialogBuilder<T> builder) {
final completer = Completer<T?>();
final wrapped = _wrap<T>(builder);
if (_current == null) {
_current = wrapped;
_completer = Completer<Object?>();
// forward our generic completer to the typed one
_completer!.future.then((v) {
if (!completer.isCompleted) completer.complete(v as T?);
});
notifyListeners();
} else {
_queue.add(_Queued(wrapped, completer));
}
return completer.future;
}
void dismiss([Object? result]) {
if (_current == null) return;
final c = _completer;
_current = null;
_completer = null;
if (c != null && !c.isCompleted) c.complete(result);
if (_queue.isNotEmpty) {
final next = _queue.removeAt(0);
_current = next.builder;
_completer = Completer<Object?>();
_completer!.future.then((v) {
if (!next.completer.isCompleted) next.completer.complete(v);
});
}
notifyListeners();
}
DialogBuilder<Object?> _wrap<T>(DialogBuilder<T> builder) {
return (ctx, dismiss) => builder(ctx, ([T? v]) => dismiss(v));
}
}
class _Queued {
_Queued(this.builder, this.completer);
final DialogBuilder<Object?> builder;
// ignore: strict_raw_type
final Completer completer;
}
/// Hosts the current dialog from [DialogRouter]. Place high in the tree
/// (inside the WidgetsApp) so dialogs overlay every other surface.
class DialogHost extends StatelessWidget {
const DialogHost({
super.key,
required this.router,
required this.child,
this.backdropColor = const Color(0xC0000000),
});
final DialogRouter router;
final Widget child;
final Color backdropColor;
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
child,
ListenableBuilder(
listenable: router,
builder: (ctx, _) {
final b = router.current;
if (b == null) return const SizedBox.shrink();
return Positioned.fill(
child: ColoredBox(
color: backdropColor,
child: Center(
child: b(ctx, router.dismiss),
),
),
);
},
),
],
);
}
}