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
+179
View File
@@ -0,0 +1,179 @@
import 'package:clide/extension/src/contribution.dart';
import 'package:clide/kernel/src/panels/registry.dart';
import 'package:clide/kernel/src/panels/slot_id.dart';
import 'package:flutter/foundation.dart';
class LayoutArrangement extends ChangeNotifier {
LayoutArrangement();
final Map<SlotId, _SlotState> _state = {};
Map<SlotId, _SlotState>? _focusModeSnapshot;
SlotId? _focusModeSlot;
bool _editorOpen = false;
double _editorRatio = 0.35;
void applyPreset(LayoutPresetContribution preset) {
_state.clear();
_focusModeSnapshot = null;
_focusModeSlot = null;
for (final slot in preset.slots) {
_state[slot.slot] = _SlotState(
position: slot.position,
size: slot.defaultSize,
minSize: slot.minSize,
maxSize: slot.maxSize,
visible: slot.visible,
);
}
notifyListeners();
}
Iterable<SlotId> get slotsInOrder => _state.keys;
SlotPosition? positionOf(SlotId id) => _state[id]?.position;
double? sizeOf(SlotId id) => _state[id]?.size;
double? minSizeOf(SlotId id) => _state[id]?.minSize;
double? maxSizeOf(SlotId id) => _state[id]?.maxSize;
bool isVisible(SlotId id) => _state[id]?.visible ?? false;
bool isCollapsed(SlotId id) => _state[id]?.collapsed ?? false;
bool get isInFocusMode => _focusModeSlot != null;
SlotId? get focusModeSlot => _focusModeSlot;
bool get editorOpen => _editorOpen;
double get editorRatio => _editorRatio;
void setSize(SlotId id, double size) {
final s = _state[id];
if (s == null) return;
final clamped = size.clamp(s.minSize ?? 0, s.maxSize ?? double.infinity).toDouble();
if (s.size == clamped) return;
_state[id] = s.copyWith(size: clamped);
notifyListeners();
}
void setVisible(SlotId id, bool visible) {
final s = _state[id];
if (s == null || s.visible == visible) return;
_state[id] = s.copyWith(visible: visible);
notifyListeners();
}
void setCollapsed(SlotId id, bool collapsed) {
final s = _state[id];
if (s == null || s.collapsed == collapsed) return;
_state[id] = s.copyWith(collapsed: collapsed);
notifyListeners();
}
void toggleCollapsed(SlotId id) {
final s = _state[id];
if (s == null) return;
_state[id] = s.copyWith(collapsed: !s.collapsed);
notifyListeners();
}
void enterFocusMode(SlotId slot) {
if (_focusModeSlot != null) return;
_focusModeSnapshot = {for (final e in _state.entries) e.key: e.value};
_focusModeSlot = slot;
for (final id in _state.keys) {
if (id == slot) {
_state[id] = _state[id]!.copyWith(visible: true, collapsed: false);
} else {
_state[id] = _state[id]!.copyWith(visible: false);
}
}
notifyListeners();
}
void exitFocusMode() {
final snap = _focusModeSnapshot;
if (snap == null) return;
_state.clear();
_state.addAll(snap);
_focusModeSnapshot = null;
_focusModeSlot = null;
notifyListeners();
}
void toggleFocusMode(SlotId slot) {
if (_focusModeSlot != null) {
exitFocusMode();
} else {
enterFocusMode(slot);
}
}
void openEditor() {
if (_editorOpen) return;
_editorOpen = true;
notifyListeners();
}
void closeEditor() {
if (!_editorOpen) return;
_editorOpen = false;
notifyListeners();
}
void toggleEditor() {
_editorOpen = !_editorOpen;
notifyListeners();
}
void setEditorRatio(double ratio) {
final clamped = ratio.clamp(0.15, 0.70);
if (_editorRatio == clamped) return;
_editorRatio = clamped;
notifyListeners();
}
void registerSlotsInto(PanelRegistry registry, LayoutPresetContribution preset) {
for (final slot in preset.slots) {
registry.registerSlot(SlotDefinition(
id: slot.slot,
position: slot.position,
defaultSize: slot.defaultSize,
minSize: slot.minSize,
maxSize: slot.maxSize,
));
}
}
}
class _SlotState {
const _SlotState({
required this.position,
this.size,
this.minSize,
this.maxSize,
this.visible = true,
this.collapsed = false,
});
final SlotPosition position;
final double? size;
final double? minSize;
final double? maxSize;
final bool visible;
final bool collapsed;
_SlotState copyWith({
SlotPosition? position,
double? size,
double? minSize,
double? maxSize,
bool? visible,
bool? collapsed,
}) {
return _SlotState(
position: position ?? this.position,
size: size ?? this.size,
minSize: minSize ?? this.minSize,
maxSize: maxSize ?? this.maxSize,
visible: visible ?? this.visible,
collapsed: collapsed ?? this.collapsed,
);
}
}
+75
View File
@@ -0,0 +1,75 @@
import 'package:clide/kernel/src/panels/arrangement.dart';
import 'package:clide/kernel/src/panels/slot_id.dart';
import 'package:clide/kernel/src/theme/controller.dart';
import 'package:flutter/widgets.dart';
/// A 4-px draggable splitter that adjusts the size of [slot] in the
/// given [arrangement]. Slot hosts wrap this around their edges to make
/// the three-column layout resizable.
class DragResizeHandle extends StatefulWidget {
const DragResizeHandle({
super.key,
required this.arrangement,
required this.slot,
required this.axis,
this.thickness = 4.0,
});
final LayoutArrangement arrangement;
final SlotId slot;
final Axis axis;
final double thickness;
@override
State<DragResizeHandle> createState() => _DragResizeHandleState();
}
class _DragResizeHandleState extends State<DragResizeHandle> {
bool _hovered = false;
double? _dragStartSize;
Offset? _dragStartPointer;
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
final color = _hovered ? tokens.panelActiveBorder : tokens.panelBorder;
return MouseRegion(
cursor: widget.axis == Axis.horizontal
? SystemMouseCursors.resizeColumn
: SystemMouseCursors.resizeRow,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: Listener(
onPointerDown: _onDown,
onPointerMove: _onMove,
onPointerUp: _onUp,
child: Container(
width: widget.axis == Axis.horizontal ? widget.thickness : null,
height: widget.axis == Axis.vertical ? widget.thickness : null,
color: color,
),
),
);
}
void _onDown(PointerDownEvent e) {
_dragStartSize = widget.arrangement.sizeOf(widget.slot);
_dragStartPointer = e.position;
}
void _onMove(PointerMoveEvent e) {
final start = _dragStartSize;
final startPt = _dragStartPointer;
if (start == null || startPt == null) return;
final delta = widget.axis == Axis.horizontal
? e.position.dx - startPt.dx
: e.position.dy - startPt.dy;
widget.arrangement.setSize(widget.slot, start + delta);
}
void _onUp(PointerUpEvent _) {
_dragStartSize = null;
_dragStartPointer = null;
}
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:clide/extension/src/contribution.dart';
import 'package:clide/kernel/src/panels/slot_id.dart';
/// Canonical "three-column + statusbar" preset — the default-layout
/// extension contributes this at Tier 0. Split out so tests and the
/// default-layout extension share one source of truth.
///
/// Columns (px):
/// sidebar 240 (drag 180400)
/// center flex (workspace on top, statusbar below)
/// context 280 (drag 220420)
/// statusbar 26 (fixed height strip)
LayoutPresetContribution classicPreset() => const LayoutPresetContribution(
id: 'builtin.default-layout.classic',
displayName: 'Classic',
slots: [
LayoutSlot(
slot: Slots.sidebar,
position: SlotPosition.left,
defaultSize: 240,
minSize: 180,
maxSize: 400,
),
LayoutSlot(
slot: Slots.workspace,
position: SlotPosition.center,
),
LayoutSlot(
slot: Slots.contextPanel,
position: SlotPosition.right,
defaultSize: 280,
minSize: 220,
maxSize: 420,
),
LayoutSlot(
slot: Slots.statusbar,
position: SlotPosition.bottom,
defaultSize: 26,
minSize: 26,
maxSize: 26,
),
],
);
+87
View File
@@ -0,0 +1,87 @@
import 'package:clide/extension/src/contribution.dart';
import 'package:clide/kernel/src/panels/slot_id.dart';
import 'package:flutter/foundation.dart';
@immutable
class SlotDefinition {
const SlotDefinition({
required this.id,
required this.position,
this.defaultSize,
this.minSize,
this.maxSize,
});
final SlotId id;
final SlotPosition position;
final double? defaultSize;
final double? minSize;
final double? maxSize;
}
class PanelRegistry extends ChangeNotifier {
final Map<SlotId, SlotDefinition> _defs = {};
final Map<SlotId, List<ContributionPoint>> _mounts = {};
final Map<SlotId, String?> _activeTab = {};
void registerSlot(SlotDefinition def) {
_defs[def.id] = def;
_mounts.putIfAbsent(def.id, () => <ContributionPoint>[]);
notifyListeners();
}
void contribute(ContributionPoint point) {
final slot = point.slot;
if (slot == null) return; // non-slot contributions go elsewhere
final list = _mounts.putIfAbsent(slot, () => <ContributionPoint>[]);
list.add(point);
list.sort((a, b) => _priority(a).compareTo(_priority(b)));
// first tab-contribution in the sidebar/workspace/context becomes the
// default active tab until the user picks another
if (_activeTab[slot] == null && point is TabContribution) {
_activeTab[slot] = point.id;
}
notifyListeners();
}
void uncontribute(String contributionId) {
for (final entry in _mounts.entries) {
final before = entry.value.length;
entry.value.removeWhere((c) => c.id == contributionId);
if (entry.value.length != before) {
if (_activeTab[entry.key] == contributionId) {
_activeTab[entry.key] =
entry.value.whereType<TabContribution>().isEmpty
? null
: entry.value.whereType<TabContribution>().first.id;
}
}
}
notifyListeners();
}
Iterable<SlotDefinition> get slots => _defs.values;
SlotDefinition? definitionFor(SlotId id) => _defs[id];
List<ContributionPoint> contributionsFor(SlotId id) =>
List.unmodifiable(_mounts[id] ?? const []);
List<TabContribution> tabsFor(SlotId id) =>
contributionsFor(id).whereType<TabContribution>().toList();
String? activeTabIn(SlotId id) => _activeTab[id];
void activateTab(SlotId id, String tabId) {
if (_activeTab[id] == tabId) return;
_activeTab[id] = tabId;
notifyListeners();
}
int _priority(ContributionPoint p) {
if (p is TabContribution) return p.priority;
if (p is StatusItemContribution) return p.priority;
if (p is ToolbarButtonContribution) return p.priority;
if (p is TrayItemContribution) return p.priority;
return 0;
}
}
+31
View File
@@ -0,0 +1,31 @@
import 'package:flutter/foundation.dart';
@immutable
class SlotId {
const SlotId(this.value);
final String value;
@override
bool operator ==(Object other) => other is SlotId && other.value == value;
@override
int get hashCode => value.hashCode;
@override
String toString() => 'SlotId($value)';
}
/// Kernel-reserved slot ids. Extensions can declare new slots; these are
/// the ones the default layout presets and the kernel services target.
abstract class Slots {
static const sidebar = SlotId('sidebar');
static const workspace = SlotId('workspace');
static const contextPanel = SlotId('context');
static const statusbar = SlotId('statusbar');
static const toolbar = SlotId('toolbar.main');
static const commandPalette = SlotId('commandPalette');
static const tray = SlotId('tray');
static const fullscreen = SlotId('fullscreen');
}
enum SlotPosition { left, right, top, bottom, center, float, popout }