dart format whole tree

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-03 21:51:59 +02:00
co-authored by Claude
parent c436e72c5c
commit 9c7ec008dc
102 changed files with 1176 additions and 1282 deletions
+167 -167
View File
File diff suppressed because it is too large Load Diff
+166 -136
View File
@@ -11,171 +11,201 @@ import 'package:flutter/material.dart';
class ClideTheme { class ClideTheme {
// ─── palette ────────────────────────────────────────────────────────── // ─── palette ──────────────────────────────────────────────────────────
static const _bg = Color(0xFF20202C); // page / editor canvas static const _bg = Color(0xFF20202C); // page / editor canvas
static const _bgSunken = Color(0xFF1A1A24); // sidebar, gutters static const _bgSunken = Color(0xFF1A1A24); // sidebar, gutters
static const _surface = Color(0xFF242838); // cards, table header, pills static const _surface = Color(0xFF242838); // cards, table header, pills
static const _surfaceHi = Color(0xFF2C3046); // hover, selected row static const _surfaceHi = Color(0xFF2C3046); // hover, selected row
static const _border = Color(0xFF343850); static const _border = Color(0xFF343850);
static const _borderHi = Color(0xFF3C445C); static const _borderHi = Color(0xFF3C445C);
static const _textHi = Color(0xFFE6E8F2); // primary text static const _textHi = Color(0xFFE6E8F2); // primary text
static const _text = Color(0xFFB1BBE3); // section titles, labels static const _text = Color(0xFFB1BBE3); // section titles, labels
static const _textDim = Color(0xFF78809C); // secondary static const _textDim = Color(0xFF78809C); // secondary
static const _textMute = Color(0xFF545C84); // small-caps labels static const _textMute = Color(0xFF545C84); // small-caps labels
static const _accent = Color(0xFF78A0F8); // periwinkle primary static const _accent = Color(0xFF78A0F8); // periwinkle primary
static const _accentPress = Color(0xFF6C90DC); static const _accentPress = Color(0xFF6C90DC);
static const _accentSoft = Color(0x2278A0F8); // 13% accent for fills static const _accentSoft = Color(0x2278A0F8); // 13% accent for fills
static const _ok = Color(0xFF7DD3A8); static const _ok = Color(0xFF7DD3A8);
static const _warn = Color(0xFFE6C370); static const _warn = Color(0xFFE6C370);
static const _err = Color(0xFFE87D7D); static const _err = Color(0xFFE87D7D);
static const _info = _accent; static const _info = _accent;
// syntax (Dart-biased) // syntax (Dart-biased)
static const _synKeyword = Color(0xFFC792EA); // class, const, final static const _synKeyword = Color(0xFFC792EA); // class, const, final
static const _synType = Color(0xFF78A0F8); // Widget, BuildContext static const _synType = Color(0xFF78A0F8); // Widget, BuildContext
static const _synString = Color(0xFFA8D99B); static const _synString = Color(0xFFA8D99B);
static const _synNumber = Color(0xFFE6C370); static const _synNumber = Color(0xFFE6C370);
static const _synComment = Color(0xFF545C84); static const _synComment = Color(0xFF545C84);
static const _synMethod = Color(0xFF82B1FF); static const _synMethod = Color(0xFF82B1FF);
static const _synPunct = Color(0xFF78809C); static const _synPunct = Color(0xFF78809C);
// ─── exposed tokens ─────────────────────────────────────────────────── // ─── exposed tokens ───────────────────────────────────────────────────
static const tokens = ClideTokens( static const tokens = ClideTokens(
bg: _bg, bgSunken: _bgSunken, surface: _surface, surfaceHi: _surfaceHi, bg: _bg,
border: _border, borderHi: _borderHi, bgSunken: _bgSunken,
textHi: _textHi, text: _text, textDim: _textDim, textMute: _textMute, surface: _surface,
accent: _accent, accentPress: _accentPress, accentSoft: _accentSoft, surfaceHi: _surfaceHi,
ok: _ok, warn: _warn, err: _err, info: _info, border: _border,
synKeyword: _synKeyword, synType: _synType, synString: _synString, borderHi: _borderHi,
synNumber: _synNumber, synComment: _synComment, synMethod: _synMethod, textHi: _textHi,
text: _text,
textDim: _textDim,
textMute: _textMute,
accent: _accent,
accentPress: _accentPress,
accentSoft: _accentSoft,
ok: _ok,
warn: _warn,
err: _err,
info: _info,
synKeyword: _synKeyword,
synType: _synType,
synString: _synString,
synNumber: _synNumber,
synComment: _synComment,
synMethod: _synMethod,
synPunct: _synPunct, synPunct: _synPunct,
); );
// ─── ThemeData ──────────────────────────────────────────────────────── // ─── ThemeData ────────────────────────────────────────────────────────
static ThemeData get data => ThemeData( static ThemeData get data => ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.dark, brightness: Brightness.dark,
scaffoldBackgroundColor: _bg, scaffoldBackgroundColor: _bg,
canvasColor: _bg, canvasColor: _bg,
colorScheme: const ColorScheme.dark( colorScheme: const ColorScheme.dark(
brightness: Brightness.dark, brightness: Brightness.dark,
primary: _accent, primary: _accent,
onPrimary: Color(0xFF0D1020), onPrimary: Color(0xFF0D1020),
secondary: _synKeyword, secondary: _synKeyword,
onSecondary: Color(0xFF0D1020), onSecondary: Color(0xFF0D1020),
surface: _surface, surface: _surface,
onSurface: _textHi, onSurface: _textHi,
surfaceContainerHighest: _surfaceHi, surfaceContainerHighest: _surfaceHi,
outline: _border, outline: _border,
outlineVariant: _borderHi, outlineVariant: _borderHi,
error: _err, error: _err,
onError: Color(0xFF0D1020), onError: Color(0xFF0D1020),
), ),
textTheme: const TextTheme( textTheme: const TextTheme(
// Josefin Sans Light for display; JetBrains Mono for code/body. // Josefin Sans Light for display; JetBrains Mono for code/body.
displayLarge: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 48, height: 1.1, letterSpacing: 0.2, color: _textHi), displayLarge: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 48, height: 1.1, letterSpacing: 0.2, color: _textHi),
displayMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 34, height: 1.15, letterSpacing: 0.2, color: _textHi), displayMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 34, height: 1.15, letterSpacing: 0.2, color: _textHi),
headlineSmall: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 20, height: 1.2, letterSpacing: 0.2, color: _textHi), headlineSmall: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 20, height: 1.2, letterSpacing: 0.2, color: _textHi),
titleMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 14, height: 1.3, letterSpacing: 0.3, color: _text), titleMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 14, height: 1.3, letterSpacing: 0.3, color: _text),
labelSmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w500, fontSize: 10, height: 1.2, letterSpacing: 1.0, color: _textMute), labelSmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w500, fontSize: 10, height: 1.2, letterSpacing: 1.0, color: _textMute),
bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 12, height: 1.45, color: _textHi), bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 12, height: 1.45, color: _textHi),
bodySmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 11, height: 1.4, color: _text), bodySmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 11, height: 1.4, color: _text),
), ),
dividerTheme: const DividerThemeData(color: _border, thickness: 1, space: 1), dividerTheme: const DividerThemeData(color: _border, thickness: 1, space: 1),
cardTheme: CardThemeData( cardTheme: CardThemeData(
color: _surface, color: _surface,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: const BorderSide(color: _border), side: const BorderSide(color: _border),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
), ),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: _bgSunken, fillColor: _bgSunken,
hintStyle: const TextStyle(color: _textMute), hintStyle: const TextStyle(color: _textMute),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: _border), borderSide: const BorderSide(color: _border),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: _border), borderSide: const BorderSide(color: _border),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
borderSide: const BorderSide(color: _accent, width: 1.5), borderSide: const BorderSide(color: _accent, width: 1.5),
), ),
), ),
elevatedButtonTheme: ElevatedButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: _accent, backgroundColor: _accent,
foregroundColor: const Color(0xFF0D1020), foregroundColor: const Color(0xFF0D1020),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
textStyle: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 12, fontWeight: FontWeight.w500), textStyle: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 12, fontWeight: FontWeight.w500),
), ),
), ),
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: _accent, foregroundColor: _accent,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
textStyle: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 12), textStyle: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 12),
), ),
), ),
// Pill-style chips (matches the Projects row in the dashboard). // Pill-style chips (matches the Projects row in the dashboard).
chipTheme: ChipThemeData( chipTheme: ChipThemeData(
backgroundColor: _surface, backgroundColor: _surface,
side: const BorderSide(color: _borderHi), side: const BorderSide(color: _borderHi),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
labelStyle: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, color: _text), labelStyle: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, color: _text),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
), ),
iconTheme: const IconThemeData(color: _textDim, size: 14), iconTheme: const IconThemeData(color: _textDim, size: 14),
tooltipTheme: TooltipThemeData( tooltipTheme: TooltipThemeData(
decoration: BoxDecoration( decoration: BoxDecoration(
color: _surfaceHi, color: _surfaceHi,
border: Border.all(color: _borderHi), border: Border.all(color: _borderHi),
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
), ),
textStyle: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, color: _textHi), textStyle: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, color: _textHi),
), ),
scrollbarTheme: ScrollbarThemeData( scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStatePropertyAll(_border), thumbColor: WidgetStatePropertyAll(_border),
thickness: const WidgetStatePropertyAll(6), thickness: const WidgetStatePropertyAll(6),
radius: const Radius.circular(3), radius: const Radius.circular(3),
), ),
); );
} }
/// Raw color tokens — use when Material widgets can't carry the meaning. /// Raw color tokens — use when Material widgets can't carry the meaning.
class ClideTokens { class ClideTokens {
const ClideTokens({ const ClideTokens({
required this.bg, required this.bgSunken, required this.bg,
required this.surface, required this.surfaceHi, required this.bgSunken,
required this.border, required this.borderHi, required this.surface,
required this.textHi, required this.text, required this.surfaceHi,
required this.textDim, required this.textMute, required this.border,
required this.accent, required this.accentPress, required this.accentSoft, required this.borderHi,
required this.ok, required this.warn, required this.err, required this.info, required this.textHi,
required this.synKeyword, required this.synType, required this.synString, required this.text,
required this.synNumber, required this.synComment, required this.textDim,
required this.synMethod, required this.synPunct, required this.textMute,
required this.accent,
required this.accentPress,
required this.accentSoft,
required this.ok,
required this.warn,
required this.err,
required this.info,
required this.synKeyword,
required this.synType,
required this.synString,
required this.synNumber,
required this.synComment,
required this.synMethod,
required this.synPunct,
}); });
final Color bg, bgSunken, surface, surfaceHi; final Color bg, bgSunken, surface, surfaceHi;
final Color border, borderHi; final Color border, borderHi;
+75 -54
View File
@@ -6,69 +6,90 @@ import 'package:flutter/material.dart';
import 'clide_theme.dart' show ClideTokens; import 'clide_theme.dart' show ClideTokens;
class MidnightTheme { class MidnightTheme {
static const _bg = Color(0xFF1E1E1E); static const _bg = Color(0xFF1E1E1E);
static const _bgSunken = Color(0xFF181818); static const _bgSunken = Color(0xFF181818);
static const _surface = Color(0xFF252526); static const _surface = Color(0xFF252526);
static const _surfaceHi = Color(0xFF2D2D2E); static const _surfaceHi = Color(0xFF2D2D2E);
static const _border = Color(0xFF333333); static const _border = Color(0xFF333333);
static const _borderHi = Color(0xFF3F3F3F); static const _borderHi = Color(0xFF3F3F3F);
static const _textHi = Color(0xFFD4D4D4); static const _textHi = Color(0xFFD4D4D4);
static const _text = Color(0xFFBBBBBB); static const _text = Color(0xFFBBBBBB);
static const _textDim = Color(0xFF858585); static const _textDim = Color(0xFF858585);
static const _textMute = Color(0xFF6A6A6A); static const _textMute = Color(0xFF6A6A6A);
static const _accent = Color(0xFF569CD6); // muted azure static const _accent = Color(0xFF569CD6); // muted azure
static const _accentPress = Color(0xFF4785BD); static const _accentPress = Color(0xFF4785BD);
static const _accentSoft = Color(0x22569CD6); static const _accentSoft = Color(0x22569CD6);
static const _ok = Color(0xFF89D185); static const _ok = Color(0xFF89D185);
static const _warn = Color(0xFFD7BA7D); static const _warn = Color(0xFFD7BA7D);
static const _err = Color(0xFFF48771); static const _err = Color(0xFFF48771);
static const _info = _accent; static const _info = _accent;
static const _synKeyword = Color(0xFFC586C0); static const _synKeyword = Color(0xFFC586C0);
static const _synType = Color(0xFF4EC9B0); static const _synType = Color(0xFF4EC9B0);
static const _synString = Color(0xFFCE9178); static const _synString = Color(0xFFCE9178);
static const _synNumber = Color(0xFFB5CEA8); static const _synNumber = Color(0xFFB5CEA8);
static const _synComment = Color(0xFF6A9955); static const _synComment = Color(0xFF6A9955);
static const _synMethod = Color(0xFFDCDCAA); static const _synMethod = Color(0xFFDCDCAA);
static const _synPunct = Color(0xFF858585); static const _synPunct = Color(0xFF858585);
static const tokens = ClideTokens( static const tokens = ClideTokens(
bg: _bg, bgSunken: _bgSunken, surface: _surface, surfaceHi: _surfaceHi, bg: _bg,
border: _border, borderHi: _borderHi, bgSunken: _bgSunken,
textHi: _textHi, text: _text, textDim: _textDim, textMute: _textMute, surface: _surface,
accent: _accent, accentPress: _accentPress, accentSoft: _accentSoft, surfaceHi: _surfaceHi,
ok: _ok, warn: _warn, err: _err, info: _info, border: _border,
synKeyword: _synKeyword, synType: _synType, synString: _synString, borderHi: _borderHi,
synNumber: _synNumber, synComment: _synComment, synMethod: _synMethod, textHi: _textHi,
text: _text,
textDim: _textDim,
textMute: _textMute,
accent: _accent,
accentPress: _accentPress,
accentSoft: _accentSoft,
ok: _ok,
warn: _warn,
err: _err,
info: _info,
synKeyword: _synKeyword,
synType: _synType,
synString: _synString,
synNumber: _synNumber,
synComment: _synComment,
synMethod: _synMethod,
synPunct: _synPunct, synPunct: _synPunct,
); );
static ThemeData get data => ThemeData( static ThemeData get data => ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.dark, brightness: Brightness.dark,
scaffoldBackgroundColor: _bg, scaffoldBackgroundColor: _bg,
canvasColor: _bg, canvasColor: _bg,
colorScheme: const ColorScheme.dark( colorScheme: const ColorScheme.dark(
primary: _accent, onPrimary: Color(0xFF0B1220), primary: _accent,
secondary: _synKeyword, onSecondary: Color(0xFF0B1220), onPrimary: Color(0xFF0B1220),
surface: _surface, onSurface: _textHi, secondary: _synKeyword,
surfaceContainerHighest: _surfaceHi, onSecondary: Color(0xFF0B1220),
outline: _border, outlineVariant: _borderHi, surface: _surface,
error: _err, onError: Color(0xFF0B1220), onSurface: _textHi,
), surfaceContainerHighest: _surfaceHi,
textTheme: const TextTheme( outline: _border,
displayLarge: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 48, color: _textHi), outlineVariant: _borderHi,
displayMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 34, color: _textHi), error: _err,
headlineSmall: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 20, color: _textHi), onError: Color(0xFF0B1220),
titleMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 14, color: _text), ),
labelSmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w500, fontSize: 10, letterSpacing: 1.0, color: _textMute), textTheme: const TextTheme(
bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 12, height: 1.45, color: _textHi), displayLarge: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 48, color: _textHi),
bodySmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 11, color: _text), displayMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 34, color: _textHi),
), headlineSmall: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 20, color: _textHi),
dividerTheme: const DividerThemeData(color: _border, thickness: 1, space: 1), titleMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 14, color: _text),
iconTheme: const IconThemeData(color: _textDim, size: 14), labelSmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w500, fontSize: 10, letterSpacing: 1.0, color: _textMute),
); bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 12, height: 1.45, color: _textHi),
bodySmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 11, color: _text),
),
dividerTheme: const DividerThemeData(color: _border, thickness: 1, space: 1),
iconTheme: const IconThemeData(color: _textDim, size: 14),
);
} }
+75 -54
View File
@@ -7,70 +7,91 @@ import 'package:flutter/material.dart';
import 'clide_theme.dart' show ClideTokens; import 'clide_theme.dart' show ClideTokens;
class PaperTheme { class PaperTheme {
static const _bg = Color(0xFFF4F1EA); // paper static const _bg = Color(0xFFF4F1EA); // paper
static const _bgSunken = Color(0xFFECE7DB); // paper-2 static const _bgSunken = Color(0xFFECE7DB); // paper-2
static const _surface = Color(0xFFFBF8F1); static const _surface = Color(0xFFFBF8F1);
static const _surfaceHi = Color(0xFFECE7DB); static const _surfaceHi = Color(0xFFECE7DB);
static const _border = Color(0xFF1A1A1A); static const _border = Color(0xFF1A1A1A);
static const _borderHi = Color(0xFF4A4A4A); static const _borderHi = Color(0xFF4A4A4A);
static const _textHi = Color(0xFF1A1A1A); // ink static const _textHi = Color(0xFF1A1A1A); // ink
static const _text = Color(0xFF4A4A4A); // ink-2 static const _text = Color(0xFF4A4A4A); // ink-2
static const _textDim = Color(0xFF8A8A82); // ink-3 static const _textDim = Color(0xFF8A8A82); // ink-3
static const _textMute = Color(0xFFA8A89E); static const _textMute = Color(0xFFA8A89E);
static const _accent = Color(0xFFC14B2A); // red pencil static const _accent = Color(0xFFC14B2A); // red pencil
static const _accentPress = Color(0xFFA03D20); static const _accentPress = Color(0xFFA03D20);
static const _accentSoft = Color(0x22C14B2A); static const _accentSoft = Color(0x22C14B2A);
static const _ok = Color(0xFF2D8A52); static const _ok = Color(0xFF2D8A52);
static const _warn = Color(0xFFB88A2A); static const _warn = Color(0xFFB88A2A);
static const _err = Color(0xFFB03A2A); static const _err = Color(0xFFB03A2A);
static const _info = Color(0xFF2A6FC1); static const _info = Color(0xFF2A6FC1);
// Syntax tuned for cream paper — desaturated so code reads like print. // Syntax tuned for cream paper — desaturated so code reads like print.
static const _synKeyword = Color(0xFF7B3F8C); static const _synKeyword = Color(0xFF7B3F8C);
static const _synType = Color(0xFF2A6FC1); static const _synType = Color(0xFF2A6FC1);
static const _synString = Color(0xFF2D8A52); static const _synString = Color(0xFF2D8A52);
static const _synNumber = Color(0xFFB88A2A); static const _synNumber = Color(0xFFB88A2A);
static const _synComment = Color(0xFF8A8A82); static const _synComment = Color(0xFF8A8A82);
static const _synMethod = Color(0xFF1E5D9E); static const _synMethod = Color(0xFF1E5D9E);
static const _synPunct = Color(0xFF4A4A4A); static const _synPunct = Color(0xFF4A4A4A);
static const tokens = ClideTokens( static const tokens = ClideTokens(
bg: _bg, bgSunken: _bgSunken, surface: _surface, surfaceHi: _surfaceHi, bg: _bg,
border: _border, borderHi: _borderHi, bgSunken: _bgSunken,
textHi: _textHi, text: _text, textDim: _textDim, textMute: _textMute, surface: _surface,
accent: _accent, accentPress: _accentPress, accentSoft: _accentSoft, surfaceHi: _surfaceHi,
ok: _ok, warn: _warn, err: _err, info: _info, border: _border,
synKeyword: _synKeyword, synType: _synType, synString: _synString, borderHi: _borderHi,
synNumber: _synNumber, synComment: _synComment, synMethod: _synMethod, textHi: _textHi,
text: _text,
textDim: _textDim,
textMute: _textMute,
accent: _accent,
accentPress: _accentPress,
accentSoft: _accentSoft,
ok: _ok,
warn: _warn,
err: _err,
info: _info,
synKeyword: _synKeyword,
synType: _synType,
synString: _synString,
synNumber: _synNumber,
synComment: _synComment,
synMethod: _synMethod,
synPunct: _synPunct, synPunct: _synPunct,
); );
static ThemeData get data => ThemeData( static ThemeData get data => ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.light, brightness: Brightness.light,
scaffoldBackgroundColor: _bg, scaffoldBackgroundColor: _bg,
canvasColor: _bg, canvasColor: _bg,
colorScheme: const ColorScheme.light( colorScheme: const ColorScheme.light(
primary: _accent, onPrimary: Color(0xFFFBF8F1), primary: _accent,
secondary: _info, onSecondary: Color(0xFFFBF8F1), onPrimary: Color(0xFFFBF8F1),
surface: _surface, onSurface: _textHi, secondary: _info,
surfaceContainerHighest: _surfaceHi, onSecondary: Color(0xFFFBF8F1),
outline: _border, outlineVariant: _borderHi, surface: _surface,
error: _err, onError: Color(0xFFFBF8F1), onSurface: _textHi,
), surfaceContainerHighest: _surfaceHi,
textTheme: const TextTheme( outline: _border,
displayLarge: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 48, color: _textHi), outlineVariant: _borderHi,
displayMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 34, color: _textHi), error: _err,
headlineSmall: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 20, color: _textHi), onError: Color(0xFFFBF8F1),
titleMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 14, color: _text), ),
labelSmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w500, fontSize: 10, letterSpacing: 1.0, color: _textDim), textTheme: const TextTheme(
bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 12, height: 1.45, color: _textHi), displayLarge: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 48, color: _textHi),
bodySmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 11, color: _text), displayMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 34, color: _textHi),
), headlineSmall: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 20, color: _textHi),
dividerTheme: const DividerThemeData(color: _border, thickness: 1, space: 1), titleMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 14, color: _text),
iconTheme: const IconThemeData(color: _text, size: 14), labelSmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w500, fontSize: 10, letterSpacing: 1.0, color: _textDim),
); bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 12, height: 1.45, color: _textHi),
bodySmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 11, color: _text),
),
dividerTheme: const DividerThemeData(color: _border, thickness: 1, space: 1),
iconTheme: const IconThemeData(color: _text, size: 14),
);
} }
+76 -55
View File
@@ -7,71 +7,92 @@ import 'package:flutter/material.dart';
import 'clide_theme.dart' show ClideTokens; import 'clide_theme.dart' show ClideTokens;
class TerminalTheme { class TerminalTheme {
static const _bg = Color(0xFF0A0A0A); static const _bg = Color(0xFF0A0A0A);
static const _bgSunken = Color(0xFF000000); static const _bgSunken = Color(0xFF000000);
static const _surface = Color(0xFF111111); static const _surface = Color(0xFF111111);
static const _surfaceHi = Color(0xFF181818); static const _surfaceHi = Color(0xFF181818);
static const _border = Color(0xFF242424); static const _border = Color(0xFF242424);
static const _borderHi = Color(0xFF2E2E2E); static const _borderHi = Color(0xFF2E2E2E);
static const _textHi = Color(0xFFE6E6E6); static const _textHi = Color(0xFFE6E6E6);
static const _text = Color(0xFFBDBDBD); static const _text = Color(0xFFBDBDBD);
static const _textDim = Color(0xFF7A7A7A); static const _textDim = Color(0xFF7A7A7A);
static const _textMute = Color(0xFF4A4A4A); static const _textMute = Color(0xFF4A4A4A);
static const _accent = Color(0xFFE0B050); // amber static const _accent = Color(0xFFE0B050); // amber
static const _accentPress = Color(0xFFC29438); static const _accentPress = Color(0xFFC29438);
static const _accentSoft = Color(0x22E0B050); static const _accentSoft = Color(0x22E0B050);
static const _ok = Color(0xFF8FDC9B); static const _ok = Color(0xFF8FDC9B);
static const _warn = Color(0xFFE0B050); static const _warn = Color(0xFFE0B050);
static const _err = Color(0xFFE05050); static const _err = Color(0xFFE05050);
static const _info = Color(0xFFA3C4FF); static const _info = Color(0xFFA3C4FF);
// Classic 16-color palette feel // Classic 16-color palette feel
static const _synKeyword = Color(0xFFE05050); static const _synKeyword = Color(0xFFE05050);
static const _synType = Color(0xFFE0B050); static const _synType = Color(0xFFE0B050);
static const _synString = Color(0xFF8FDC9B); static const _synString = Color(0xFF8FDC9B);
static const _synNumber = Color(0xFFC792EA); static const _synNumber = Color(0xFFC792EA);
static const _synComment = Color(0xFF4A4A4A); static const _synComment = Color(0xFF4A4A4A);
static const _synMethod = Color(0xFFA3C4FF); static const _synMethod = Color(0xFFA3C4FF);
static const _synPunct = Color(0xFF7A7A7A); static const _synPunct = Color(0xFF7A7A7A);
static const tokens = ClideTokens( static const tokens = ClideTokens(
bg: _bg, bgSunken: _bgSunken, surface: _surface, surfaceHi: _surfaceHi, bg: _bg,
border: _border, borderHi: _borderHi, bgSunken: _bgSunken,
textHi: _textHi, text: _text, textDim: _textDim, textMute: _textMute, surface: _surface,
accent: _accent, accentPress: _accentPress, accentSoft: _accentSoft, surfaceHi: _surfaceHi,
ok: _ok, warn: _warn, err: _err, info: _info, border: _border,
synKeyword: _synKeyword, synType: _synType, synString: _synString, borderHi: _borderHi,
synNumber: _synNumber, synComment: _synComment, synMethod: _synMethod, textHi: _textHi,
text: _text,
textDim: _textDim,
textMute: _textMute,
accent: _accent,
accentPress: _accentPress,
accentSoft: _accentSoft,
ok: _ok,
warn: _warn,
err: _err,
info: _info,
synKeyword: _synKeyword,
synType: _synType,
synString: _synString,
synNumber: _synNumber,
synComment: _synComment,
synMethod: _synMethod,
synPunct: _synPunct, synPunct: _synPunct,
); );
static ThemeData get data => ThemeData( static ThemeData get data => ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.dark, brightness: Brightness.dark,
scaffoldBackgroundColor: _bg, scaffoldBackgroundColor: _bg,
canvasColor: _bg, canvasColor: _bg,
colorScheme: const ColorScheme.dark( colorScheme: const ColorScheme.dark(
primary: _accent, onPrimary: Color(0xFF000000), primary: _accent,
secondary: _info, onSecondary: Color(0xFF000000), onPrimary: Color(0xFF000000),
surface: _surface, onSurface: _textHi, secondary: _info,
surfaceContainerHighest: _surfaceHi, onSecondary: Color(0xFF000000),
outline: _border, outlineVariant: _borderHi, surface: _surface,
error: _err, onError: Color(0xFF000000), onSurface: _textHi,
), surfaceContainerHighest: _surfaceHi,
textTheme: const TextTheme( outline: _border,
displayLarge: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 48, color: _textHi), outlineVariant: _borderHi,
displayMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 34, color: _textHi), error: _err,
headlineSmall: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 20, color: _textHi), onError: Color(0xFF000000),
titleMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 14, color: _text), ),
// Terminal mockups go full-mono even for display. textTheme: const TextTheme(
labelSmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w500, fontSize: 10, letterSpacing: 1.0, color: _textMute), displayLarge: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 48, color: _textHi),
bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 12, height: 1.45, color: _textHi), displayMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w300, fontSize: 34, color: _textHi),
bodySmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 11, color: _text), headlineSmall: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 20, color: _textHi),
), titleMedium: TextStyle(fontFamily: 'Josefin Sans', fontWeight: FontWeight.w400, fontSize: 14, color: _text),
dividerTheme: const DividerThemeData(color: _border, thickness: 1, space: 1), // Terminal mockups go full-mono even for display.
iconTheme: const IconThemeData(color: _textDim, size: 14), labelSmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w500, fontSize: 10, letterSpacing: 1.0, color: _textMute),
); bodyMedium: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 12, height: 1.45, color: _textHi),
bodySmall: TextStyle(fontFamily: 'JetBrains Mono', fontWeight: FontWeight.w400, fontSize: 11, color: _text),
),
dividerTheme: const DividerThemeData(color: _border, thickness: 1, space: 1),
iconTheme: const IconThemeData(color: _textDim, size: 14),
);
} }
+96 -96
View File
@@ -72,33 +72,33 @@ class ClideThemes {
dark: true, dark: true,
subtitle: 'cool near-black + periwinkle · default', subtitle: 'cool near-black + periwinkle · default',
palette: ClidePalette( palette: ClidePalette(
bg: Color(0xFF20202C), bg: Color(0xFF20202C),
bgSunken: Color(0xFF1A1A24), bgSunken: Color(0xFF1A1A24),
surface: Color(0xFF242838), surface: Color(0xFF242838),
surfaceHi: Color(0xFF2C3046), surfaceHi: Color(0xFF2C3046),
border: Color(0xFF343850), border: Color(0xFF343850),
borderHi: Color(0xFF3C445C), borderHi: Color(0xFF3C445C),
textHi: Color(0xFFE6E8F2), textHi: Color(0xFFE6E8F2),
text: Color(0xFFB1BBE3), text: Color(0xFFB1BBE3),
textDim: Color(0xFF78809C), textDim: Color(0xFF78809C),
textMute: Color(0xFF545C84), textMute: Color(0xFF545C84),
accent: Color(0xFF78A0F8), accent: Color(0xFF78A0F8),
accentPress: Color(0xFF6C90DC), accentPress: Color(0xFF6C90DC),
accentSoft: Color(0x2178A0F8), accentSoft: Color(0x2178A0F8),
onAccent: Color(0xFF0D1020), onAccent: Color(0xFF0D1020),
ok: Color(0xFF7DD3A8), ok: Color(0xFF7DD3A8),
warn: Color(0xFFE6C370), warn: Color(0xFFE6C370),
err: Color(0xFFE87D7D), err: Color(0xFFE87D7D),
info: Color(0xFF78A0F8), info: Color(0xFF78A0F8),
), ),
syntax: ClideSyntax( syntax: ClideSyntax(
keyword: Color(0xFFC792EA), keyword: Color(0xFFC792EA),
type: Color(0xFF78A0F8), type: Color(0xFF78A0F8),
string: Color(0xFFA8D99B), string: Color(0xFFA8D99B),
number: Color(0xFFE6C370), number: Color(0xFFE6C370),
comment: Color(0xFF545C84), comment: Color(0xFF545C84),
method: Color(0xFF82B1FF), method: Color(0xFF82B1FF),
punct: Color(0xFF78809C), punct: Color(0xFF78809C),
), ),
); );
@@ -108,33 +108,33 @@ class ClideThemes {
dark: true, dark: true,
subtitle: 'VS Code-adjacent muted dark', subtitle: 'VS Code-adjacent muted dark',
palette: ClidePalette( palette: ClidePalette(
bg: Color(0xFF1E1E1E), bg: Color(0xFF1E1E1E),
bgSunken: Color(0xFF181818), bgSunken: Color(0xFF181818),
surface: Color(0xFF252526), surface: Color(0xFF252526),
surfaceHi: Color(0xFF2D2D2E), surfaceHi: Color(0xFF2D2D2E),
border: Color(0xFF333333), border: Color(0xFF333333),
borderHi: Color(0xFF3F3F3F), borderHi: Color(0xFF3F3F3F),
textHi: Color(0xFFD4D4D4), textHi: Color(0xFFD4D4D4),
text: Color(0xFFBBBBBB), text: Color(0xFFBBBBBB),
textDim: Color(0xFF858585), textDim: Color(0xFF858585),
textMute: Color(0xFF6A6A6A), textMute: Color(0xFF6A6A6A),
accent: Color(0xFF569CD6), accent: Color(0xFF569CD6),
accentPress: Color(0xFF4785BD), accentPress: Color(0xFF4785BD),
accentSoft: Color(0x21569CD6), accentSoft: Color(0x21569CD6),
onAccent: Color(0xFF0B1220), onAccent: Color(0xFF0B1220),
ok: Color(0xFF89D185), ok: Color(0xFF89D185),
warn: Color(0xFFD7BA7D), warn: Color(0xFFD7BA7D),
err: Color(0xFFF48771), err: Color(0xFFF48771),
info: Color(0xFF569CD6), info: Color(0xFF569CD6),
), ),
syntax: ClideSyntax( syntax: ClideSyntax(
keyword: Color(0xFFC586C0), keyword: Color(0xFFC586C0),
type: Color(0xFF4EC9B0), type: Color(0xFF4EC9B0),
string: Color(0xFFCE9178), string: Color(0xFFCE9178),
number: Color(0xFFB5CEA8), number: Color(0xFFB5CEA8),
comment: Color(0xFF6A9955), comment: Color(0xFF6A9955),
method: Color(0xFFDCDCAA), method: Color(0xFFDCDCAA),
punct: Color(0xFF858585), punct: Color(0xFF858585),
), ),
); );
@@ -144,33 +144,33 @@ class ClideThemes {
dark: false, dark: false,
subtitle: 'drafting sheet · red-pencil accent · light', subtitle: 'drafting sheet · red-pencil accent · light',
palette: ClidePalette( palette: ClidePalette(
bg: Color(0xFFF4F1EA), bg: Color(0xFFF4F1EA),
bgSunken: Color(0xFFECE7DB), bgSunken: Color(0xFFECE7DB),
surface: Color(0xFFFBF8F1), surface: Color(0xFFFBF8F1),
surfaceHi: Color(0xFFECE7DB), surfaceHi: Color(0xFFECE7DB),
border: Color(0xFF1A1A1A), border: Color(0xFF1A1A1A),
borderHi: Color(0xFF4A4A4A), borderHi: Color(0xFF4A4A4A),
textHi: Color(0xFF1A1A1A), textHi: Color(0xFF1A1A1A),
text: Color(0xFF4A4A4A), text: Color(0xFF4A4A4A),
textDim: Color(0xFF8A8A82), textDim: Color(0xFF8A8A82),
textMute: Color(0xFFA8A89E), textMute: Color(0xFFA8A89E),
accent: Color(0xFFC14B2A), accent: Color(0xFFC14B2A),
accentPress: Color(0xFFA03D20), accentPress: Color(0xFFA03D20),
accentSoft: Color(0x21C14B2A), accentSoft: Color(0x21C14B2A),
onAccent: Color(0xFFFBF8F1), onAccent: Color(0xFFFBF8F1),
ok: Color(0xFF2D8A52), ok: Color(0xFF2D8A52),
warn: Color(0xFFB88A2A), warn: Color(0xFFB88A2A),
err: Color(0xFFB03A2A), err: Color(0xFFB03A2A),
info: Color(0xFF2A6FC1), info: Color(0xFF2A6FC1),
), ),
syntax: ClideSyntax( syntax: ClideSyntax(
keyword: Color(0xFF7B3F8C), keyword: Color(0xFF7B3F8C),
type: Color(0xFF2A6FC1), type: Color(0xFF2A6FC1),
string: Color(0xFF2D8A52), string: Color(0xFF2D8A52),
number: Color(0xFFB88A2A), number: Color(0xFFB88A2A),
comment: Color(0xFF8A8A82), comment: Color(0xFF8A8A82),
method: Color(0xFF1E5D9E), method: Color(0xFF1E5D9E),
punct: Color(0xFF4A4A4A), punct: Color(0xFF4A4A4A),
), ),
); );
@@ -180,33 +180,33 @@ class ClideThemes {
dark: true, dark: true,
subtitle: 'near-black + amber · tmux feel', subtitle: 'near-black + amber · tmux feel',
palette: ClidePalette( palette: ClidePalette(
bg: Color(0xFF0A0A0A), bg: Color(0xFF0A0A0A),
bgSunken: Color(0xFF000000), bgSunken: Color(0xFF000000),
surface: Color(0xFF111111), surface: Color(0xFF111111),
surfaceHi: Color(0xFF181818), surfaceHi: Color(0xFF181818),
border: Color(0xFF242424), border: Color(0xFF242424),
borderHi: Color(0xFF2E2E2E), borderHi: Color(0xFF2E2E2E),
textHi: Color(0xFFE6E6E6), textHi: Color(0xFFE6E6E6),
text: Color(0xFFBDBDBD), text: Color(0xFFBDBDBD),
textDim: Color(0xFF7A7A7A), textDim: Color(0xFF7A7A7A),
textMute: Color(0xFF4A4A4A), textMute: Color(0xFF4A4A4A),
accent: Color(0xFFE0B050), accent: Color(0xFFE0B050),
accentPress: Color(0xFFC29438), accentPress: Color(0xFFC29438),
accentSoft: Color(0x21E0B050), accentSoft: Color(0x21E0B050),
onAccent: Color(0xFF000000), onAccent: Color(0xFF000000),
ok: Color(0xFF8FDC9B), ok: Color(0xFF8FDC9B),
warn: Color(0xFFE0B050), warn: Color(0xFFE0B050),
err: Color(0xFFE05050), err: Color(0xFFE05050),
info: Color(0xFFA3C4FF), info: Color(0xFFA3C4FF),
), ),
syntax: ClideSyntax( syntax: ClideSyntax(
keyword: Color(0xFFE05050), keyword: Color(0xFFE05050),
type: Color(0xFFE0B050), type: Color(0xFFE0B050),
string: Color(0xFF8FDC9B), string: Color(0xFF8FDC9B),
number: Color(0xFFC792EA), number: Color(0xFFC792EA),
comment: Color(0xFF4A4A4A), comment: Color(0xFF4A4A4A),
method: Color(0xFFA3C4FF), method: Color(0xFFA3C4FF),
punct: Color(0xFF7A7A7A), punct: Color(0xFF7A7A7A),
), ),
); );
+2 -5
View File
@@ -21,9 +21,7 @@ import '../test/helpers/fake_ipc.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets( testWidgets('clide app boots with classic 3-column layout + welcome + statusbar', (tester) async {
'clide app boots with classic 3-column layout + welcome + statusbar',
(tester) async {
final themes = [ final themes = [
await const ThemeLoader().fromAsset( await const ThemeLoader().fromAsset(
rootBundle, rootBundle,
@@ -40,8 +38,7 @@ void main() {
'builtin.theme-picker', 'builtin.theme-picker',
'builtin.default-layout', 'builtin.default-layout',
], ],
daemonClientFactory: (log, events) => daemonClientFactory: (log, events) => FakeDaemonClient(log: log, events: events),
FakeDaemonClient(log: log, events: events),
autoStartDaemonClient: false, autoStartDaemonClient: false,
); );
services.extensions services.extensions
@@ -14,8 +14,7 @@ import '../test/helpers/fake_ipc.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('disable + re-enable an extension mounts/unmounts its UI', testWidgets('disable + re-enable an extension mounts/unmounts its UI', (tester) async {
(tester) async {
final themes = [ final themes = [
await const ThemeLoader().fromAsset( await const ThemeLoader().fromAsset(
rootBundle, rootBundle,
@@ -31,8 +30,7 @@ void main() {
'builtin.ipc-status', 'builtin.ipc-status',
'builtin.default-layout', 'builtin.default-layout',
], ],
daemonClientFactory: (log, events) => daemonClientFactory: (log, events) => FakeDaemonClient(log: log, events: events),
FakeDaemonClient(log: log, events: events),
autoStartDaemonClient: false, autoStartDaemonClient: false,
); );
services.extensions services.extensions
+2 -4
View File
@@ -14,8 +14,7 @@ import '../test/helpers/fake_ipc.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('theme.pick command opens modal; selecting dismisses it', testWidgets('theme.pick command opens modal; selecting dismisses it', (tester) async {
(tester) async {
final themes = [ final themes = [
await const ThemeLoader().fromAsset( await const ThemeLoader().fromAsset(
rootBundle, rootBundle,
@@ -31,8 +30,7 @@ void main() {
'builtin.theme-picker', 'builtin.theme-picker',
'builtin.default-layout', 'builtin.default-layout',
], ],
daemonClientFactory: (log, events) => daemonClientFactory: (log, events) => FakeDaemonClient(log: log, events: events),
FakeDaemonClient(log: log, events: events),
autoStartDaemonClient: false, autoStartDaemonClient: false,
); );
services.extensions services.extensions
@@ -109,7 +109,13 @@ class _TabRow extends StatelessWidget {
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: tokens.dividerColor))), decoration: BoxDecoration(border: Border(bottom: BorderSide(color: tokens.dividerColor))),
child: Row( child: Row(
children: [ children: [
for (var i = 0; i < sessions.length; i++) _Tab(session: sessions[i], active: i == activeIndex, tokens: tokens, onTap: () => onSelect(i), onClose: sessions[i].isPrimary ? null : () => onClose(i)), for (var i = 0; i < sessions.length; i++)
_Tab(
session: sessions[i],
active: i == activeIndex,
tokens: tokens,
onTap: () => onSelect(i),
onClose: sessions[i].isPrimary ? null : () => onClose(i)),
const SizedBox(width: 4), const SizedBox(width: 4),
_AddButton(tokens: tokens, onTap: onAdd), _AddButton(tokens: tokens, onTap: onAdd),
const Spacer(), const Spacer(),
@@ -30,6 +30,5 @@ class DecisionTypeColors {
rejected: Color(0xFFC03030), rejected: Color(0xFFC03030),
); );
static DecisionTypeColors forTheme({required bool dark}) => static DecisionTypeColors forTheme({required bool dark}) => dark ? DecisionTypeColors.dark : DecisionTypeColors.light;
dark ? DecisionTypeColors.dark : DecisionTypeColors.light;
} }
+61 -28
View File
@@ -32,7 +32,10 @@ class _DecisionsViewState extends State<DecisionsView> {
if (_focusSub == null) { if (_focusSub == null) {
final kernel = ClideKernel.of(context); final kernel = ClideKernel.of(context);
_focusSub = kernel.messages.subscribe(publisher: 'builtin.decisions', channel: 'focus').listen(_onFocus); _focusSub = kernel.messages.subscribe(publisher: 'builtin.decisions', channel: 'focus').listen(_onFocus);
_fileSub = kernel.events.on<DaemonEvent>().where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && _isDecisionPath(e.data['path'] as String? ?? '')).listen((_) => _refresh()); _fileSub = kernel.events
.on<DaemonEvent>()
.where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && _isDecisionPath(e.data['path'] as String? ?? ''))
.listen((_) => _refresh());
_schedulerSub = kernel.events.on<SchedulerTick>().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh()); _schedulerSub = kernel.events.on<SchedulerTick>().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh());
} }
if (!_loading || _decisions.isNotEmpty) return; if (!_loading || _decisions.isNotEmpty) return;
@@ -43,7 +46,10 @@ class _DecisionsViewState extends State<DecisionsView> {
Future<void> _refresh() async { Future<void> _refresh() async {
if (!mounted) return; if (!mounted) return;
if (_refreshing) { _pendingRefresh = true; return; } if (_refreshing) {
_pendingRefresh = true;
return;
}
_refreshing = true; _refreshing = true;
_pendingRefresh = false; _pendingRefresh = false;
await _load(); await _load();
@@ -117,11 +123,20 @@ class _DecisionsViewState extends State<DecisionsView> {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
if (_loading) return const Center(child: ClideText('Loading decisions...', muted: true)); if (_loading) return const Center(child: ClideText('Loading decisions...', muted: true));
if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)); if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
if (_decisions.isEmpty) return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true)); if (_decisions.isEmpty)
return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true));
final lf = _filter.toLowerCase(); final lf = _filter.toLowerCase();
final hasFilter = lf.isNotEmpty; final hasFilter = lf.isNotEmpty;
final filtered = hasFilter ? _decisions.where((d) => d.id.toLowerCase().contains(lf) || d.title.toLowerCase().contains(lf) || (d.domain ?? '').toLowerCase().contains(lf) || (d.type ?? '').contains(lf)).toList() : _decisions; final filtered = hasFilter
? _decisions
.where((d) =>
d.id.toLowerCase().contains(lf) ||
d.title.toLowerCase().contains(lf) ||
(d.domain ?? '').toLowerCase().contains(lf) ||
(d.type ?? '').contains(lf))
.toList()
: _decisions;
final confirmed = filtered.where((d) => d.type == 'confirmed').toList(); final confirmed = filtered.where((d) => d.type == 'confirmed').toList();
final questions = filtered.where((d) => d.type == 'question').toList(); final questions = filtered.where((d) => d.type == 'question').toList();
@@ -140,7 +155,8 @@ class _DecisionsViewState extends State<DecisionsView> {
child: ClideTappable( child: ClideTappable(
onTap: _refreshing ? null : _refresh, onTap: _refreshing ? null : _refresh,
tooltip: 'Refresh decisions', tooltip: 'Refresh decisions',
builder: (ctx, hovered, _) => ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted), builder: (ctx, hovered, _) =>
ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
), ),
), ),
], ],
@@ -151,27 +167,45 @@ class _DecisionsViewState extends State<DecisionsView> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (confirmed.isNotEmpty) ClideAccordion( if (confirmed.isNotEmpty)
label: 'CONFIRMED', count: confirmed.length, ClideAccordion(
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.confirmed, shape: BoxShape.circle)), label: 'CONFIRMED',
expanded: hasFilter || _isSectionExpanded('confirmed'), count: confirmed.length,
onToggle: () => _toggleSection('confirmed'), leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.confirmed, shape: BoxShape.circle)),
children: [for (final d in confirmed) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)], expanded: hasFilter || _isSectionExpanded('confirmed'),
), onToggle: () => _toggleSection('confirmed'),
if (questions.isNotEmpty) ClideAccordion( children: [
label: 'QUESTIONS', count: questions.length, for (final d in confirmed)
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.question, shape: BoxShape.circle)), _DecisionCard(
expanded: hasFilter || _isSectionExpanded('question'), entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)
onToggle: () => _toggleSection('question'), ],
children: [for (final d in questions) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)], ),
), if (questions.isNotEmpty)
if (rejected.isNotEmpty) ClideAccordion( ClideAccordion(
label: 'REJECTED', count: rejected.length, label: 'QUESTIONS',
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.rejected, shape: BoxShape.circle)), count: questions.length,
expanded: hasFilter || _isSectionExpanded('rejected'), leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.question, shape: BoxShape.circle)),
onToggle: () => _toggleSection('rejected'), expanded: hasFilter || _isSectionExpanded('question'),
children: [for (final d in rejected) _DecisionCard(entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)], onToggle: () => _toggleSection('question'),
), children: [
for (final d in questions)
_DecisionCard(
entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)
],
),
if (rejected.isNotEmpty)
ClideAccordion(
label: 'REJECTED',
count: rejected.length,
leading: Container(width: 8, height: 8, decoration: BoxDecoration(color: typeColors.rejected, shape: BoxShape.circle)),
expanded: hasFilter || _isSectionExpanded('rejected'),
onToggle: () => _toggleSection('rejected'),
children: [
for (final d in rejected)
_DecisionCard(
entry: d, tokens: tokens, typeColors: typeColors, focused: d.id == _focusedId, focusKey: d.id == _focusedId ? _focusedKey : null)
],
),
], ],
), ),
), ),
@@ -233,8 +267,7 @@ class _DecisionCard extends StatelessWidget {
const SizedBox(width: 6), const SizedBox(width: 6),
ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily), ClideText(entry.id, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
const Spacer(), const Spacer(),
if (entry.domain != null) if (entry.domain != null) ClideText(entry.domain!, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
ClideText(entry.domain!, fontSize: clideFontBadge, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
+6 -17
View File
@@ -68,9 +68,7 @@ class _DiffViewState extends State<DiffView> {
Padding( Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: ClideText( child: ClideText(
c.showStaged c.showStaged ? 'No staged changes.' : 'No unstaged changes.',
? 'No staged changes.'
: 'No unstaged changes.',
muted: true, muted: true,
), ),
), ),
@@ -81,8 +79,7 @@ class _DiffViewState extends State<DiffView> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
for (final diff in c.diffs) for (final diff in c.diffs) _FileDiff(diff: diff, controller: c),
_FileDiff(diff: diff, controller: c),
], ],
), ),
), ),
@@ -118,9 +115,7 @@ class _DiffToolbar extends StatelessWidget {
child: ClideText( child: ClideText(
'Unstaged', 'Unstaged',
fontSize: clideFontCaption, fontSize: clideFontCaption,
color: controller.showStaged color: controller.showStaged ? tokens.globalTextMuted : tokens.globalForeground,
? tokens.globalTextMuted
: tokens.globalForeground,
), ),
), ),
), ),
@@ -134,9 +129,7 @@ class _DiffToolbar extends StatelessWidget {
child: ClideText( child: ClideText(
'Staged', 'Staged',
fontSize: clideFontCaption, fontSize: clideFontCaption,
color: controller.showStaged color: controller.showStaged ? tokens.globalForeground : tokens.globalTextMuted,
? tokens.globalForeground
: tokens.globalTextMuted,
), ),
), ),
), ),
@@ -188,12 +181,8 @@ class _FileDiff extends StatelessWidget {
color: tokens.panelHeaderForeground, color: tokens.panelHeaderForeground,
), ),
), ),
if (additions > 0) if (additions > 0) ClideText('+$additions ', fontSize: clideFontCaption, color: tokens.statusSuccess),
ClideText('+$additions ', fontSize: clideFontCaption, if (removals > 0) ClideText('-$removals', fontSize: clideFontCaption, color: tokens.statusError),
color: tokens.statusSuccess),
if (removals > 0)
ClideText('-$removals', fontSize: clideFontCaption,
color: tokens.statusError),
], ],
), ),
), ),
@@ -17,8 +17,7 @@ import 'package:clide/kernel/kernel.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class EditorController extends ChangeNotifier { class EditorController extends ChangeNotifier {
EditorController({required this.ipc, required DaemonBus events}) EditorController({required this.ipc, required DaemonBus events}) : _events = events {
: _events = events {
_eventSub = events.on<DaemonEvent>().listen(_onEvent); _eventSub = events.on<DaemonEvent>().listen(_onEvent);
} }
@@ -77,9 +76,7 @@ class EditorController extends ChangeNotifier {
_activePath = r.data['path']! as String; _activePath = r.data['path']! as String;
_content = (r.data['content'] as String?) ?? ''; _content = (r.data['content'] as String?) ?? '';
final sel = r.data['selection']; final sel = r.data['selection'];
_selection = sel is Map _selection = sel is Map ? Selection.fromJson(sel.cast<String, Object?>()) : const Selection.collapsed(0);
? Selection.fromJson(sel.cast<String, Object?>())
: const Selection.collapsed(0);
_dirty = (r.data['dirty'] as bool?) ?? false; _dirty = (r.data['dirty'] as bool?) ?? false;
_error = null; _error = null;
notifyListeners(); notifyListeners();
+5 -13
View File
@@ -45,8 +45,7 @@ class _EditorViewState extends State<EditorView> {
super.didChangeDependencies(); super.didChangeDependencies();
if (_controller != null) return; if (_controller != null) return;
final kernel = ClideKernel.of(context); final kernel = ClideKernel.of(context);
_controller = EditorController(ipc: kernel.ipc, events: kernel.events) _controller = EditorController(ipc: kernel.ipc, events: kernel.events)..addListener(_onControllerChanged);
..addListener(_onControllerChanged);
unawaited(_controller!.hydrate()); unawaited(_controller!.hydrate());
} }
@@ -80,29 +79,22 @@ class _EditorViewState extends State<EditorView> {
final c = _controller; final c = _controller;
if (c == null || c.activeId == null) return; if (c == null || c.activeId == null) return;
final value = _text.value; final value = _text.value;
if (value.text == c.content && if (value.text == c.content && value.selection.baseOffset == c.selection.start && value.selection.extentOffset == c.selection.end) {
value.selection.baseOffset == c.selection.start &&
value.selection.extentOffset == c.selection.end) {
return; return;
} }
_lastRemoteContent = value.text; _lastRemoteContent = value.text;
c.pushLocalEdit( c.pushLocalEdit(
newContent: value.text, newContent: value.text,
newSelection: Selection( newSelection: Selection(
start: value.selection.start < 0 start: value.selection.start < 0 ? value.text.length : value.selection.start,
? value.text.length end: value.selection.end < 0 ? value.text.length : value.selection.end,
: value.selection.start,
end: value.selection.end < 0
? value.text.length
: value.selection.end,
), ),
); );
} }
KeyEventResult _onKey(FocusNode node, KeyEvent event) { KeyEventResult _onKey(FocusNode node, KeyEvent event) {
if (event is! KeyDownEvent) return KeyEventResult.ignored; if (event is! KeyDownEvent) return KeyEventResult.ignored;
final isCmd = HardwareKeyboard.instance.isMetaPressed || final isCmd = HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed;
HardwareKeyboard.instance.isControlPressed;
if (isCmd && event.logicalKey == LogicalKeyboardKey.keyS) { if (isCmd && event.logicalKey == LogicalKeyboardKey.keyS) {
unawaited(_controller?.save()); unawaited(_controller?.save());
return KeyEventResult.handled; return KeyEventResult.handled;
@@ -8,8 +8,7 @@ import 'package:clide/kernel/src/theme/tokens.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class SyntaxTextController extends TextEditingController { class SyntaxTextController extends TextEditingController {
SyntaxTextController({required TreeSitterService syntax}) SyntaxTextController({required TreeSitterService syntax}) : _syntax = syntax;
: _syntax = syntax;
final TreeSitterService _syntax; final TreeSitterService _syntax;
@@ -74,8 +73,7 @@ class SyntaxTextController extends TextEditingController {
// Convert byte offsets to character offsets. // Convert byte offsets to character offsets.
// Build a byte-to-char map only up to the max byte we need. // Build a byte-to-char map only up to the max byte we need.
final spans = _spans.where((s) => s.end <= sourceBytes.length).toList() final spans = _spans.where((s) => s.end <= sourceBytes.length).toList()..sort((a, b) => a.start != b.start ? a.start - b.start : a.end - b.end);
..sort((a, b) => a.start != b.start ? a.start - b.start : a.end - b.end);
if (spans.isEmpty) { if (spans.isEmpty) {
return TextSpan(text: text, style: style); return TextSpan(text: text, style: style);
@@ -121,9 +119,7 @@ class SyntaxTextController extends TextEditingController {
continue; continue;
} }
final spanCharStart = byteToChar[span.start]; final spanCharStart = byteToChar[span.start];
final spanCharEnd = span.end <= maxByte final spanCharEnd = span.end <= maxByte ? byteToChar[span.end] : source.length;
? byteToChar[span.end]
: source.length;
if (spanCharStart < charPos) continue; if (spanCharStart < charPos) continue;
+1 -3
View File
@@ -136,8 +136,7 @@ class _Children extends StatelessWidget {
controller: controller, controller: controller,
depth: depth, depth: depth,
), ),
if (controller.isExpanded(e.path)) if (controller.isExpanded(e.path)) _Children(path: e.path, controller: controller, depth: depth + 1),
_Children(path: e.path, controller: controller, depth: depth + 1),
], ],
) )
else else
@@ -294,4 +293,3 @@ class _FilteredFileRow extends StatelessWidget {
); );
} }
} }
+84 -86
View File
@@ -76,94 +76,93 @@ class _GitPanelViewState extends State<GitPanelView> {
child: Column( child: Column(
children: [ children: [
ClideFilterBox(hint: 'Filter changes…', onChanged: (v) => setState(() => _filter = v)), ClideFilterBox(hint: 'Filter changes…', onChanged: (v) => setState(() => _filter = v)),
Expanded(child: SingleChildScrollView( Expanded(
padding: const EdgeInsets.symmetric(vertical: 4), child: SingleChildScrollView(
child: Column( padding: const EdgeInsets.symmetric(vertical: 4),
crossAxisAlignment: CrossAxisAlignment.stretch, child: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ mainAxisSize: MainAxisSize.min,
_BranchHeader(controller: c), children: [
if (c.error != null) _BranchHeader(controller: c),
Padding( if (c.error != null)
padding: const EdgeInsets.symmetric( Padding(
horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: ClideText( child: ClideText(
c.error!, c.error!,
color: tokens.statusError, color: tokens.statusError,
fontSize: clideFontCaption, fontSize: clideFontCaption,
maxLines: 3, maxLines: 3,
), ),
), ),
if (c.loading && c.isClean) if (c.loading && c.isClean)
const Padding( const Padding(
padding: EdgeInsets.all(12), padding: EdgeInsets.all(12),
child: ClideText('Loading…', muted: true), child: ClideText('Loading…', muted: true),
), ),
if (!c.loading && c.isClean && c.error == null) if (!c.loading && c.isClean && c.error == null)
const Padding( const Padding(
padding: EdgeInsets.all(12), padding: EdgeInsets.all(12),
child: ClideText('Nothing to commit, working tree clean.', child: ClideText('Nothing to commit, working tree clean.', muted: true),
muted: true), ),
), if (c.conflicted.isNotEmpty)
if (c.conflicted.isNotEmpty) _FileGroup(
_FileGroup( label: 'Merge conflicts',
label: 'Merge conflicts', entries: _applyFilter(c.conflicted),
entries: _applyFilter(c.conflicted), actions: const [],
actions: const [], ),
), if (c.staged.isNotEmpty) ...[
if (c.staged.isNotEmpty) ...[ _FileGroup(
_FileGroup( label: 'Staged',
label: 'Staged', entries: _applyFilter(c.staged),
entries: _applyFilter(c.staged), actions: [
actions: [ _GroupAction(
_GroupAction( label: 'Unstage all',
label: 'Unstage all', onTap: () => unawaited(c.unstage(const [])),
onTap: () => unawaited(c.unstage(const [])), ),
],
onUnstage: (path) => unawaited(c.unstage([path])),
),
_CommitInput(
commitMsg: _commitMsg,
commitFocus: _commitFocus,
controller: c,
), ),
], ],
onUnstage: (path) => unawaited(c.unstage([path])), if (c.unstaged.isNotEmpty)
), _FileGroup(
_CommitInput( label: 'Changes',
commitMsg: _commitMsg, entries: _applyFilter(c.unstaged),
commitFocus: _commitFocus, actions: [
controller: c, _GroupAction(
), label: 'Stage all',
], onTap: () => unawaited(c.stageAll()),
if (c.unstaged.isNotEmpty) ),
_FileGroup( ],
label: 'Changes', onStage: (path) => unawaited(c.stage([path])),
entries: _applyFilter(c.unstaged), onDiscard: (path) => _confirmDiscard(context, c, path),
actions: [
_GroupAction(
label: 'Stage all',
onTap: () => unawaited(c.stageAll()),
), ),
], if (c.untracked.isNotEmpty)
onStage: (path) => unawaited(c.stage([path])), _FileGroup(
onDiscard: (path) => _confirmDiscard(context, c, path), label: 'Untracked',
), entries: _applyFilter(c.untracked),
if (c.untracked.isNotEmpty) actions: [
_FileGroup( _GroupAction(
label: 'Untracked', label: 'Stage all',
entries: _applyFilter(c.untracked), onTap: () {
actions: [ final paths = [
_GroupAction( for (final e in c.untracked) e['path'] as String,
label: 'Stage all', ];
onTap: () { unawaited(c.stage(paths));
final paths = [ },
for (final e in c.untracked) e['path'] as String, ),
]; ],
unawaited(c.stage(paths)); onStage: (path) => unawaited(c.stage([path])),
},
), ),
], ],
onStage: (path) => unawaited(c.stage([path])), ),
), )),
], ],
), ),
)),
],
),
); );
}, },
); );
@@ -369,8 +368,7 @@ class _GitFileRow extends StatelessWidget {
}, },
builder: (context, hovered, _) => Container( builder: (context, hovered, _) => Container(
color: hovered ? tokens.sidebarItemHover : null, color: hovered ? tokens.sidebarItemHover : null,
padding: const EdgeInsets.only( padding: const EdgeInsets.only(left: 20, right: 8, top: 2, bottom: 2),
left: 20, right: 8, top: 2, bottom: 2),
child: Row( child: Row(
children: [ children: [
ClideText( ClideText(
+4 -9
View File
@@ -140,8 +140,7 @@ class _BranchPickerState extends State<_BranchPicker> {
_loading = false; _loading = false;
if (r.ok) { if (r.ok) {
_branches = [ _branches = [
for (final b in (r.data['branches'] as List? ?? const [])) for (final b in (r.data['branches'] as List? ?? const [])) (b as Map).cast<String, Object?>(),
(b as Map).cast<String, Object?>(),
]; ];
} else { } else {
_error = r.error?.message ?? 'failed to load branches'; _error = r.error?.message ?? 'failed to load branches';
@@ -197,10 +196,8 @@ class _BranchPickerState extends State<_BranchPicker> {
], ],
), ),
), ),
if (_loading) if (_loading) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)), if (_error != null) Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)),
if (_error != null)
Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true)),
if (!_loading && _error == null && _branches.isEmpty) if (!_loading && _error == null && _branches.isEmpty)
const Padding(padding: EdgeInsets.all(12), child: ClideText('No branches found.', muted: true)), const Padding(padding: EdgeInsets.all(12), child: ClideText('No branches found.', muted: true)),
if (_branches.isNotEmpty) if (_branches.isNotEmpty)
@@ -266,9 +263,7 @@ class _BranchRow extends StatelessWidget {
name, name,
fontFamily: clideMonoFamily, fontFamily: clideMonoFamily,
fontSize: clideFontMono, fontSize: clideFontMono,
color: current color: current ? tokens.globalForeground : tokens.listItemForeground,
? tokens.globalForeground
: tokens.listItemForeground,
), ),
), ),
], ],
+7 -15
View File
@@ -61,8 +61,7 @@ class _BacklinksViewState extends State<BacklinksView> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
horizontal: 12, vertical: 4),
child: ClideText( child: ClideText(
c.activePath!.split('/').last, c.activePath!.split('/').last,
color: tokens.globalForeground, color: tokens.globalForeground,
@@ -70,8 +69,7 @@ class _BacklinksViewState extends State<BacklinksView> {
), ),
if (c.error != null) if (c.error != null)
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
horizontal: 12, vertical: 4),
child: ClideText( child: ClideText(
c.error!, c.error!,
color: tokens.statusError, color: tokens.statusError,
@@ -120,8 +118,7 @@ class _LinkGroup extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: padding: const EdgeInsets.only(left: 12, right: 8, top: 8, bottom: 2),
const EdgeInsets.only(left: 12, right: 8, top: 8, bottom: 2),
child: ClideText( child: ClideText(
'$label (${links.length})', '$label (${links.length})',
fontSize: clideFontCaption, fontSize: clideFontCaption,
@@ -133,8 +130,7 @@ class _LinkGroup extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 2), padding: EdgeInsets.symmetric(horizontal: 20, vertical: 2),
child: ClideText('None', fontSize: clideFontCaption, muted: true), child: ClideText('None', fontSize: clideFontCaption, muted: true),
), ),
for (final link in links) for (final link in links) _LinkRow(link: link, pathKey: pathKey),
_LinkRow(link: link, pathKey: pathKey),
], ],
); );
} }
@@ -159,21 +155,17 @@ class _LinkRow extends StatelessWidget {
onTap: () { onTap: () {
if (!target.startsWith('http')) { if (!target.startsWith('http')) {
final kernel = ClideKernel.of(context); final kernel = ClideKernel.of(context);
unawaited( unawaited(kernel.ipc.request('editor.open', args: {'path': target}));
kernel.ipc.request('editor.open', args: {'path': target}));
} }
}, },
builder: (context, hovered, _) => Container( builder: (context, hovered, _) => Container(
color: hovered ? tokens.sidebarItemHover : null, color: hovered ? tokens.sidebarItemHover : null,
padding: padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2),
const EdgeInsets.symmetric(horizontal: 20, vertical: 2),
child: ClideText( child: ClideText(
display, display,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: target.startsWith('http') color: target.startsWith('http') ? tokens.statusInfo : tokens.sidebarForeground,
? tokens.statusInfo
: tokens.sidebarForeground,
), ),
), ),
), ),
+6 -7
View File
@@ -43,7 +43,10 @@ class _PqlPanelViewState extends State<PqlPanelView> {
if (ctx != null) Scrollable.ensureVisible(ctx, duration: const Duration(milliseconds: 200), alignment: 0.3); if (ctx != null) Scrollable.ensureVisible(ctx, duration: const Duration(milliseconds: 200), alignment: 0.3);
}); });
}); });
_fileSub = kernel.events.on<DaemonEvent>().where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && (e.data['path'] as String? ?? '').endsWith('.md')).listen((_) { _fileSub = kernel.events
.on<DaemonEvent>()
.where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && (e.data['path'] as String? ?? '').endsWith('.md'))
.listen((_) {
if (_controller?.view == PqlView.markdown) { if (_controller?.view == PqlView.markdown) {
unawaited(_controller!.loadMarkdownFiles()); unawaited(_controller!.loadMarkdownFiles());
} }
@@ -83,8 +86,7 @@ class _PqlPanelViewState extends State<PqlPanelView> {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: ClideText(c.error!, color: tokens.statusError, fontSize: clideFontCaption, maxLines: 3), child: ClideText(c.error!, color: tokens.statusError, fontSize: clideFontCaption, maxLines: 3),
), ),
if (c.loading && c.results.isEmpty) if (c.loading && c.results.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
if (!c.loading && c.results.isEmpty && c.error == null && c.view == PqlView.markdown) if (!c.loading && c.results.isEmpty && c.error == null && c.view == PqlView.markdown)
const Padding(padding: EdgeInsets.all(12), child: ClideText('No markdown files found.', muted: true)), const Padding(padding: EdgeInsets.all(12), child: ClideText('No markdown files found.', muted: true)),
Expanded( Expanded(
@@ -318,10 +320,7 @@ class _QueryResultRow extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
final name = entry['name'] as String? ?? entry['path'] as String? ?? ''; final name = entry['name'] as String? ?? entry['path'] as String? ?? '';
final values = entry.entries final values = entry.entries.where((e) => e.key != 'name' && e.key != 'path').map((e) => '${e.key}: ${e.value}').join(' · ');
.where((e) => e.key != 'name' && e.key != 'path')
.map((e) => '${e.key}: ${e.value}')
.join(' · ');
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
child: Column( child: Column(
@@ -55,8 +55,7 @@ class ProblemsController extends ChangeNotifier {
} }
final skill = (doctor.data['skill'] as Map?)?.cast<String, Object?>(); final skill = (doctor.data['skill'] as Map?)?.cast<String, Object?>();
if (skill != null) { if (skill != null) {
final project = final project = (skill['project'] as Map?)?.cast<String, Object?>();
(skill['project'] as Map?)?.cast<String, Object?>();
if (project != null) { if (project != null) {
final state = project['state'] as String?; final state = project['state'] as String?;
if (state == 'stale') { if (state == 'stale') {
+6 -6
View File
@@ -49,7 +49,8 @@ class _ProblemsViewState extends State<ProblemsView> {
explicitChildNodes: true, explicitChildNodes: true,
child: () { child: () {
final lf = _filter.toLowerCase(); final lf = _filter.toLowerCase();
final filtered = lf.isEmpty ? c.problems : c.problems.where((p) => p.message.toLowerCase().contains(lf) || p.source.toLowerCase().contains(lf)).toList(); final filtered =
lf.isEmpty ? c.problems : c.problems.where((p) => p.message.toLowerCase().contains(lf) || p.source.toLowerCase().contains(lf)).toList();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@@ -64,16 +65,15 @@ class _ProblemsViewState extends State<ProblemsView> {
label: 'refresh problems', label: 'refresh problems',
child: GestureDetector( child: GestureDetector(
onTap: () => unawaited(c.refresh()), onTap: () => unawaited(c.refresh()),
child: MouseRegion(cursor: SystemMouseCursors.click, child: ClideText('Refresh', fontSize: clideFontCaption, color: tokens.sidebarForeground)), child: MouseRegion(
cursor: SystemMouseCursors.click, child: ClideText('Refresh', fontSize: clideFontCaption, color: tokens.sidebarForeground)),
), ),
), ),
], ],
), ),
), ),
if (c.loading && c.problems.isEmpty) if (c.loading && c.problems.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('Scanning…', muted: true)),
const Padding(padding: EdgeInsets.all(12), child: ClideText('Scanning…', muted: true)), if (!c.loading && filtered.isEmpty) const Padding(padding: EdgeInsets.all(12), child: ClideText('No problems found.', muted: true)),
if (!c.loading && filtered.isEmpty)
const Padding(padding: EdgeInsets.all(12), child: ClideText('No problems found.', muted: true)),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),
+1 -3
View File
@@ -139,9 +139,7 @@ class _TerminalPaneState extends State<TerminalPane> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final subtitle = _error != null final subtitle = _error != null ? _error! : (_paneId == null ? 'spawning shell…' : 'pid $_pid · ${_paneId!}');
? _error!
: (_paneId == null ? 'spawning shell…' : 'pid $_pid · ${_paneId!}');
return ClidePaneChrome( return ClidePaneChrome(
title: 'terminal', title: 'terminal',
+8 -21
View File
@@ -31,8 +31,7 @@ class _ThemePickerViewState extends State<ThemePickerView> {
return Semantics( return Semantics(
container: true, container: true,
label: i.string('modal.title', label: i.string('modal.title', namespace: ThemePickerView.ns, placeholder: 'Select theme'),
namespace: ThemePickerView.ns, placeholder: 'Select theme'),
explicitChildNodes: true, explicitChildNodes: true,
child: ClideSurface( child: ClideSurface(
width: 420, width: 420,
@@ -45,8 +44,7 @@ class _ThemePickerViewState extends State<ThemePickerView> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
ClideText( ClideText(
i.string('modal.title', i.string('modal.title', namespace: ThemePickerView.ns, placeholder: 'Select theme'),
namespace: ThemePickerView.ns, placeholder: 'Select theme'),
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@@ -65,9 +63,7 @@ class _ThemePickerViewState extends State<ThemePickerView> {
displayName: t.displayName, displayName: t.displayName,
selected: t.name == currentName, selected: t.name == currentName,
hovered: _hovered == t.name, hovered: _hovered == t.name,
hint: i.string('row.select.hint', hint: i.string('row.select.hint', namespace: ThemePickerView.ns, placeholder: 'Activate this theme'),
namespace: ThemePickerView.ns,
placeholder: 'Activate this theme'),
onEnter: () => setState(() => _hovered = t.name), onEnter: () => setState(() => _hovered = t.name),
onExit: () => setState(() => _hovered = null), onExit: () => setState(() => _hovered = null),
onTap: () { onTap: () {
@@ -84,12 +80,9 @@ class _ThemePickerViewState extends State<ThemePickerView> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
ClideButton( ClideButton(
label: i.string('modal.cancel', label: i.string('modal.cancel', namespace: ThemePickerView.ns, placeholder: 'Cancel'),
namespace: ThemePickerView.ns, placeholder: 'Cancel'), semanticHint:
semanticHint: i.string('modal.cancel.hint', i.string('modal.cancel.hint', namespace: ThemePickerView.ns, placeholder: 'Close the theme picker without changing the current theme'),
namespace: ThemePickerView.ns,
placeholder:
'Close the theme picker without changing the current theme'),
onPressed: () => widget.onDismiss(), onPressed: () => widget.onDismiss(),
), ),
], ],
@@ -125,14 +118,8 @@ class _ThemeRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
final bg = selected final bg = selected ? tokens.listItemSelectedBackground : (hovered ? tokens.listItemHoverBackground : tokens.listItemBackground);
? tokens.listItemSelectedBackground final fg = selected ? tokens.listItemSelectedForeground : tokens.listItemForeground;
: (hovered
? tokens.listItemHoverBackground
: tokens.listItemBackground);
final fg = selected
? tokens.listItemSelectedForeground
: tokens.listItemForeground;
return Semantics( return Semantics(
button: true, button: true,
selected: selected, selected: selected,
+1 -2
View File
@@ -40,6 +40,5 @@ class TicketTypeColors {
bug: Color(0xFFC03030), bug: Color(0xFFC03030),
); );
static TicketTypeColors forTheme({required bool dark}) => static TicketTypeColors forTheme({required bool dark}) => dark ? TicketTypeColors.dark : TicketTypeColors.light;
dark ? TicketTypeColors.dark : TicketTypeColors.light;
} }
@@ -122,8 +122,7 @@ class _TicketHeader extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
ClideText(detail.id, fontSize: clideFontSmall, color: typeColor, fontFamily: clideMonoFamily), ClideText(detail.id, fontSize: clideFontSmall, color: typeColor, fontFamily: clideMonoFamily),
const Spacer(), const Spacer(),
if (detail.priority != null) if (detail.priority != null) ClideText(detail.priority!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
ClideText(detail.priority!, fontSize: clideFontSmall, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -153,13 +152,18 @@ class _StatusControls extends StatelessWidget {
for (final s in _statuses) ...[ for (final s in _statuses) ...[
Expanded( Expanded(
child: ClideTappable( child: ClideTappable(
onTap: detail.status == s ? null : () async { onTap: detail.status == s
final resp = await controller.ipc.request('pql.tickets.status', args: {'ids': [detail.id], 'status': s}); ? null
if (resp.ok) { : () async {
controller.messages.publish('builtin.tickets', 'changed', {'id': detail.id}); final resp = await controller.ipc.request('pql.tickets.status', args: {
await controller.load(detail.id); 'ids': [detail.id],
} 'status': s
}, });
if (resp.ok) {
controller.messages.publish('builtin.tickets', 'changed', {'id': detail.id});
await controller.load(detail.id);
}
},
builder: (ctx, hovered, _) { builder: (ctx, hovered, _) {
final active = detail.status == s; final active = detail.status == s;
final color = active ? tokens.statusInfo : (hovered ? tokens.globalForeground : tokens.globalTextMuted); final color = active ? tokens.statusInfo : (hovered ? tokens.globalForeground : tokens.globalTextMuted);
+11 -3
View File
@@ -88,7 +88,10 @@ class _TicketsViewState extends State<TicketsView> {
Future<void> _refresh() async { Future<void> _refresh() async {
if (!mounted) return; if (!mounted) return;
if (_refreshing) { _pendingRefresh = true; return; } if (_refreshing) {
_pendingRefresh = true;
return;
}
_refreshing = true; _refreshing = true;
_pendingRefresh = false; _pendingRefresh = false;
await _load(); await _load();
@@ -130,7 +133,11 @@ class _TicketsViewState extends State<TicketsView> {
final lf = _filter.toLowerCase(); final lf = _filter.toLowerCase();
final hasFilter = lf.isNotEmpty; final hasFilter = lf.isNotEmpty;
final filtered = hasFilter ? _tickets.where((t) => t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf)).toList() : _tickets; final filtered = hasFilter
? _tickets
.where((t) => t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf))
.toList()
: _tickets;
const sections = [ const sections = [
('in_progress', 'IN PROGRESS'), ('in_progress', 'IN PROGRESS'),
@@ -159,7 +166,8 @@ class _TicketsViewState extends State<TicketsView> {
child: ClideTappable( child: ClideTappable(
onTap: _refreshing ? null : _refresh, onTap: _refreshing ? null : _refresh,
tooltip: 'Refresh tickets', tooltip: 'Refresh tickets',
builder: (ctx, hovered, _) => ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted), builder: (ctx, hovered, _) =>
ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
), ),
), ),
], ],
+12 -9
View File
@@ -115,9 +115,9 @@ class _StartColumn extends StatelessWidget {
kernel.panels.activateTab(Slots.workspace, 'claude.primary'); kernel.panels.activateTab(Slots.workspace, 'claude.primary');
} else { } else {
kernel.dialog.show((ctx, dismiss) => _NotARepoDialog( kernel.dialog.show((ctx, dismiss) => _NotARepoDialog(
path: picked, path: picked,
onDismiss: () => dismiss(), onDismiss: () => dismiss(),
)); ));
} }
} }
return; return;
@@ -163,8 +163,7 @@ class _ActionRow extends StatelessWidget {
ClideIcon(icon, size: 18, color: tokens.globalTextMuted), ClideIcon(icon, size: 18, color: tokens.globalTextMuted),
const SizedBox(width: 14), const SizedBox(width: 14),
Expanded(child: ClideText(label, fontSize: 15, color: tokens.globalForeground)), Expanded(child: ClideText(label, fontSize: 15, color: tokens.globalForeground)),
if (shortcut != null) if (shortcut != null) ClideText(shortcut!, fontSize: 13, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
ClideText(shortcut!, fontSize: 13, color: tokens.globalTextMuted, fontFamily: clideMonoFamily),
], ],
), ),
), ),
@@ -191,8 +190,7 @@ class _RecentColumn extends StatelessWidget {
if (recents.isEmpty) if (recents.isEmpty)
const ClideText('No recent projects.', muted: true, fontSize: 14) const ClideText('No recent projects.', muted: true, fontSize: 14)
else else
for (final r in recents) for (final r in recents) _RecentRow(project: r, tokens: tokens, onTap: () => _openRecent(r.path)),
_RecentRow(project: r, tokens: tokens, onTap: () => _openRecent(r.path)),
], ],
); );
}, },
@@ -232,7 +230,9 @@ class _RecentRow extends StatelessWidget {
const SizedBox(height: 3), const SizedBox(height: 3),
Row( Row(
children: [ children: [
Flexible(child: ClideText(project.relativePath, muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)), Flexible(
child: ClideText(project.relativePath,
muted: true, fontSize: 13, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)),
if (project.branch != null) ...[ if (project.branch != null) ...[
ClideText(' · ', muted: true, fontSize: 13), ClideText(' · ', muted: true, fontSize: 13),
ClideIcon(PhosphorIcons.gitBranch, size: 11, color: tokens.globalTextMuted), ClideIcon(PhosphorIcons.gitBranch, size: 11, color: tokens.globalTextMuted),
@@ -336,7 +336,10 @@ class _OpenProjectDialogState extends State<_OpenProjectDialog> {
Future<void> _submit() async { Future<void> _submit() async {
final path = _controller.text.trim(); final path = _controller.text.trim();
if (path.isEmpty) return; if (path.isEmpty) return;
setState(() { _loading = true; _error = null; }); setState(() {
_loading = true;
_error = null;
});
try { try {
await widget.onOpen(path); await widget.onOpen(path);
} catch (_) { } catch (_) {
+1 -2
View File
@@ -23,8 +23,7 @@ export 'src/files/listing.dart' show FileEntry, listDir;
export 'src/git/diff.dart' show GitDiff, GitHunk, DiffLine, DiffLineKind; export 'src/git/diff.dart' show GitDiff, GitHunk, DiffLine, DiffLineKind;
export 'src/git/client.dart' show GitClient; export 'src/git/client.dart' show GitClient;
export 'src/git/operations.dart' show GitLogEntry, GitException; export 'src/git/operations.dart' show GitLogEntry, GitException;
export 'src/git/status.dart' export 'src/git/status.dart' show GitStatus, GitFileStatus, GitFileState, GitConflictType;
show GitStatus, GitFileStatus, GitFileState, GitConflictType;
export 'src/pql/client.dart' show PqlClient, PqlException; export 'src/pql/client.dart' show PqlClient, PqlException;
export 'src/ipc/envelope.dart'; export 'src/ipc/envelope.dart';
export 'src/ipc/paths.dart'; export 'src/ipc/paths.dart';
+1 -2
View File
@@ -91,8 +91,7 @@ extension ClideExtensionContextMessages on ClideExtensionContext {
extension ClideExtensionContextI18n on ClideExtensionContext { extension ClideExtensionContextI18n on ClideExtensionContext {
/// `ctx.t('welcome.title', placeholder: 'clide')` → /// `ctx.t('welcome.title', placeholder: 'clide')` →
/// `i18n.string('welcome.title', namespace: id, placeholder: 'clide')`. /// `i18n.string('welcome.title', namespace: id, placeholder: 'clide')`.
String t(String key, {String? placeholder}) => String t(String key, {String? placeholder}) => i18n.string(key, namespace: id, placeholder: placeholder);
i18n.string(key, namespace: id, placeholder: placeholder);
/// [t] with interpolation replacers. /// [t] with interpolation replacers.
String tr( String tr(
+1 -2
View File
@@ -55,6 +55,5 @@ class ExtensionManifest {
); );
} }
static Future<ExtensionManifest> fromFile(File f) async => static Future<ExtensionManifest> fromFile(File f) async => ExtensionManifest.fromYamlString(await f.readAsString());
ExtensionManifest.fromYamlString(await f.readAsString());
} }
+2 -2
View File
@@ -30,6 +30,7 @@ import 'package:clide/src/pql/client.dart';
class BackendBootMessage { class BackendBootMessage {
const BackendBootMessage({required this.frontendPort, this.hintRoot}); const BackendBootMessage({required this.frontendPort, this.hintRoot});
final SendPort frontendPort; final SendPort frontendPort;
/// Optional path hint for initial toolchain resolution (e.g. CLIDE_PROJECT). /// Optional path hint for initial toolchain resolution (e.g. CLIDE_PROJECT).
/// Used to find project-local binaries like dugite before a project opens. /// Used to find project-local binaries like dugite before a project opens.
final String? hintRoot; final String? hintRoot;
@@ -61,8 +62,7 @@ void backendEntry(BackendBootMessage boot) {
final path = message['path'] as String; final path = message['path'] as String;
final id = message['id'] as String; final id = message['id'] as String;
try { try {
final r = await Process.run(toolchain.git, ['rev-parse', '--show-toplevel'], final r = await Process.run(toolchain.git, ['rev-parse', '--show-toplevel'], workingDirectory: path, environment: toolchain.gitEnv);
workingDirectory: path, environment: toolchain.gitEnv);
if (r.exitCode == 0) { if (r.exitCode == 0) {
final root = (r.stdout as String).trim(); final root = (r.stdout as String).trim();
frontendPort.send({'type': 'project.validated', 'id': id, 'root': root}); frontendPort.send({'type': 'project.validated', 'id': id, 'root': root});
+2 -4
View File
@@ -22,8 +22,7 @@ class ClideClipboard {
bucket.insert(0, value); bucket.insert(0, value);
if (bucket.length > historyLimit) bucket.removeLast(); if (bucket.length > historyLimit) bucket.removeLast();
if (toPlain != null) { if (toPlain != null) {
await flutter_services.Clipboard.setData( await flutter_services.Clipboard.setData(flutter_services.ClipboardData(text: toPlain(value)));
flutter_services.ClipboardData(text: toPlain(value)));
} }
} }
@@ -45,8 +44,7 @@ class ClideClipboard {
} }
Future<void> writePlain(String text) async { Future<void> writePlain(String text) async {
await flutter_services.Clipboard.setData( await flutter_services.Clipboard.setData(flutter_services.ClipboardData(text: text));
flutter_services.ClipboardData(text: text));
final bucket = _history.putIfAbsent(String, () => <Object>[]); final bucket = _history.putIfAbsent(String, () => <Object>[]);
bucket.insert(0, text); bucket.insert(0, text);
if (bucket.length > historyLimit) bucket.removeLast(); if (bucket.length > historyLimit) bucket.removeLast();
+1 -4
View File
@@ -36,10 +36,7 @@ class Keybinding {
} }
@override @override
bool operator ==(Object other) => bool operator ==(Object other) => other is Keybinding && other.key == key && listEquals(other.modifiers, modifiers);
other is Keybinding &&
other.key == key &&
listEquals(other.modifiers, modifiers);
@override @override
int get hashCode => Object.hash(key, Object.hashAll(modifiers)); int get hashCode => Object.hash(key, Object.hashAll(modifiers));
+2 -4
View File
@@ -5,13 +5,11 @@ import 'package:clide/kernel/src/events/types.dart';
class DaemonBus { class DaemonBus {
DaemonBus(); DaemonBus();
final StreamController<ClideEventEnvelope> _controller = final StreamController<ClideEventEnvelope> _controller = StreamController<ClideEventEnvelope>.broadcast();
StreamController<ClideEventEnvelope>.broadcast();
Stream<ClideEventEnvelope> get stream => _controller.stream; Stream<ClideEventEnvelope> get stream => _controller.stream;
Stream<T> on<T extends ClideEvent>() => Stream<T> on<T extends ClideEvent>() => _controller.stream.where((e) => e.event is T).map((e) => e.event as T);
_controller.stream.where((e) => e.event is T).map((e) => e.event as T);
void emit(ClideEvent event) { void emit(ClideEvent event) {
if (_controller.isClosed) return; if (_controller.isClosed) return;
+3 -6
View File
@@ -124,8 +124,7 @@ class ExtensionManager extends ChangeNotifier {
} }
for (final dep in ext.dependsOn) { for (final dep in ext.dependsOn) {
if (!_activated.contains(dep)) { if (!_activated.contains(dep)) {
log.warn( log.warn('extensions', 'skipping ${ext.id}: dependency not activated: $dep');
'extensions', 'skipping ${ext.id}: dependency not activated: $dep');
return; return;
} }
} }
@@ -140,8 +139,7 @@ class ExtensionManager extends ChangeNotifier {
notifyListeners(); notifyListeners();
log.info('extensions', 'activated $id'); log.info('extensions', 'activated $id');
} catch (e, st) { } catch (e, st) {
log.error('extensions', 'activate failed for $id', log.error('extensions', 'activate failed for $id', error: e, stackTrace: st);
error: e, stackTrace: st);
} }
} }
@@ -159,8 +157,7 @@ class ExtensionManager extends ChangeNotifier {
notifyListeners(); notifyListeners();
log.info('extensions', 'deactivated $id'); log.info('extensions', 'deactivated $id');
} catch (e, st) { } catch (e, st) {
log.error('extensions', 'deactivate failed for $id', log.error('extensions', 'deactivate failed for $id', error: e, stackTrace: st);
error: e, stackTrace: st);
} }
} }
+4 -6
View File
@@ -154,8 +154,8 @@ class KernelServices {
onProjectOpen: onProjectOpen, onProjectOpen: onProjectOpen,
onValidateProject: onValidateProject, onValidateProject: onValidateProject,
); );
final ipc = isolateClient final ipc = isolateClient ??
?? (daemonClientFactory != null (daemonClientFactory != null
? daemonClientFactory(log, events) ? daemonClientFactory(log, events)
: DaemonClient( : DaemonClient(
socketPath: socketPath ?? defaultSocketPath(), socketPath: socketPath ?? defaultSocketPath(),
@@ -257,13 +257,11 @@ class ClideKernel extends InheritedWidget {
static KernelServices of(BuildContext context) { static KernelServices of(BuildContext context) {
final w = context.dependOnInheritedWidgetOfExactType<ClideKernel>(); final w = context.dependOnInheritedWidgetOfExactType<ClideKernel>();
if (w == null) { if (w == null) {
throw FlutterError( throw FlutterError('ClideKernel.of() called with a context that is not a descendant of a ClideKernel.');
'ClideKernel.of() called with a context that is not a descendant of a ClideKernel.');
} }
return w.services; return w.services;
} }
@override @override
bool updateShouldNotify(ClideKernel oldWidget) => bool updateShouldNotify(ClideKernel oldWidget) => services != oldWidget.services;
services != oldWidget.services;
} }
+1 -2
View File
@@ -88,6 +88,5 @@ class InMemoryCatalogLoader implements CatalogLoader {
return const {}; return const {};
} }
static bool _eq(Locale a, Locale b) => static bool _eq(Locale a, Locale b) => a.languageCode == b.languageCode && a.countryCode == b.countryCode;
a.languageCode == b.languageCode && a.countryCode == b.countryCode;
} }
+1 -2
View File
@@ -55,8 +55,7 @@ class I18n extends ChangeNotifier {
Locale locale, Locale locale,
Map<String, Object?> catalog, Map<String, Object?> catalog,
) { ) {
_cache.putIfAbsent( _cache.putIfAbsent(namespace, () => <Locale, Map<String, Object?>>{})[locale] = catalog;
namespace, () => <Locale, Map<String, Object?>>{})[locale] = catalog;
notifyListeners(); notifyListeners();
} }
+2 -7
View File
@@ -79,11 +79,7 @@ class DaemonClient extends ChangeNotifier {
_backoff = const Duration(milliseconds: 200); _backoff = const Duration(milliseconds: 200);
_setConnected(true); _setConnected(true);
_log.info('ipc', 'connected to $socketPath'); _log.info('ipc', 'connected to $socketPath');
socket socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()).listen(
.cast<List<int>>()
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
_handleLine, _handleLine,
onDone: _handleDisconnect, onDone: _handleDisconnect,
onError: (Object e) { onError: (Object e) {
@@ -93,8 +89,7 @@ class DaemonClient extends ChangeNotifier {
cancelOnError: true, cancelOnError: true,
); );
} catch (e) { } catch (e) {
_log.debug( _log.debug('ipc', 'connect failed ($e); retry in ${_backoff.inMilliseconds}ms');
'ipc', 'connect failed ($e); retry in ${_backoff.inMilliseconds}ms');
_scheduleReconnect(); _scheduleReconnect();
} }
} }
+10 -20
View File
@@ -23,8 +23,7 @@ class LogRecord {
@override @override
String toString() { String toString() {
final lv = level.name.toUpperCase().padRight(5); final lv = level.name.toUpperCase().padRight(5);
final buf = final buf = StringBuffer('${timestamp.toIso8601String()} $lv [$source] $message');
StringBuffer('${timestamp.toIso8601String()} $lv [$source] $message');
if (error != null) buf.write(' | error=$error'); if (error != null) buf.write(' | error=$error');
return buf.toString(); return buf.toString();
} }
@@ -33,33 +32,24 @@ class LogRecord {
typedef LogSink = void Function(LogRecord); typedef LogSink = void Function(LogRecord);
class Logger { class Logger {
Logger({this.minLevel = LogLevel.info, List<LogSink>? sinks}) Logger({this.minLevel = LogLevel.info, List<LogSink>? sinks}) : _sinks = List<LogSink>.from(sinks ?? <LogSink>[stderrSink]);
: _sinks = List<LogSink>.from(sinks ?? <LogSink>[stderrSink]);
LogLevel minLevel; LogLevel minLevel;
final List<LogSink> _sinks; final List<LogSink> _sinks;
final StreamController<LogRecord> _stream = final StreamController<LogRecord> _stream = StreamController<LogRecord>.broadcast();
StreamController<LogRecord>.broadcast();
Stream<LogRecord> get records => _stream.stream; Stream<LogRecord> get records => _stream.stream;
void addSink(LogSink sink) => _sinks.add(sink); void addSink(LogSink sink) => _sinks.add(sink);
void trace(String source, String message) => void trace(String source, String message) => _emit(LogLevel.trace, source, message);
_emit(LogLevel.trace, source, message); void debug(String source, String message) => _emit(LogLevel.debug, source, message);
void debug(String source, String message) => void info(String source, String message) => _emit(LogLevel.info, source, message);
_emit(LogLevel.debug, source, message); void warn(String source, String message, {Object? error}) => _emit(LogLevel.warn, source, message, error: error);
void info(String source, String message) => void error(String source, String message, {Object? error, StackTrace? stackTrace}) =>
_emit(LogLevel.info, source, message); _emit(LogLevel.error, source, message, error: error, stackTrace: stackTrace);
void warn(String source, String message, {Object? error}) =>
_emit(LogLevel.warn, source, message, error: error);
void error(String source, String message,
{Object? error, StackTrace? stackTrace}) =>
_emit(LogLevel.error, source, message,
error: error, stackTrace: stackTrace);
void _emit(LogLevel level, String source, String message, void _emit(LogLevel level, String source, String message, {Object? error, StackTrace? stackTrace}) {
{Object? error, StackTrace? stackTrace}) {
if (level.index < minLevel.index) return; if (level.index < minLevel.index) return;
final rec = LogRecord( final rec = LogRecord(
level: level, level: level,
+4 -10
View File
@@ -29,16 +29,10 @@ class Notifications extends ChangeNotifier {
List<ClideNotification> get active => List.unmodifiable(_active); List<ClideNotification> get active => List.unmodifiable(_active);
void info(String message, {String? title, Duration? duration}) => void info(String message, {String? title, Duration? duration}) => _push(NotificationLevel.info, message, title: title, duration: duration);
_push(NotificationLevel.info, message, title: title, duration: duration); void warn(String message, {String? title, Duration? duration}) => _push(NotificationLevel.warning, message, title: title, duration: duration);
void warn(String message, {String? title, Duration? duration}) => void error(String message, {String? title, Duration? duration}) => _push(NotificationLevel.error, message, title: title, duration: duration);
_push(NotificationLevel.warning, message, void success(String message, {String? title, Duration? duration}) => _push(NotificationLevel.success, message, title: title, duration: duration);
title: title, duration: duration);
void error(String message, {String? title, Duration? duration}) =>
_push(NotificationLevel.error, message, title: title, duration: duration);
void success(String message, {String? title, Duration? duration}) =>
_push(NotificationLevel.success, message,
title: title, duration: duration);
void dismiss(String id) { void dismiss(String id) {
_timers.remove(id)?.cancel(); _timers.remove(id)?.cancel();
+2 -6
View File
@@ -37,9 +37,7 @@ class _DragResizeHandleState extends State<DragResizeHandle> {
final lineColor = _hovered ? tokens.panelActiveBorder : tokens.dividerColor; final lineColor = _hovered ? tokens.panelActiveBorder : tokens.dividerColor;
return MouseRegion( return MouseRegion(
cursor: widget.axis == Axis.horizontal cursor: widget.axis == Axis.horizontal ? SystemMouseCursors.resizeColumn : SystemMouseCursors.resizeRow,
? SystemMouseCursors.resizeColumn
: SystemMouseCursors.resizeRow,
onEnter: (_) => setState(() => _hovered = true), onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false), onExit: (_) => setState(() => _hovered = false),
child: Listener( child: Listener(
@@ -72,9 +70,7 @@ class _DragResizeHandleState extends State<DragResizeHandle> {
final start = _dragStartSize; final start = _dragStartSize;
final startPt = _dragStartPointer; final startPt = _dragStartPointer;
if (start == null || startPt == null) return; if (start == null || startPt == null) return;
final rawDelta = widget.axis == Axis.horizontal final rawDelta = widget.axis == Axis.horizontal ? e.position.dx - startPt.dx : e.position.dy - startPt.dy;
? e.position.dx - startPt.dx
: e.position.dy - startPt.dy;
final delta = widget.slot == Slots.contextPanel ? -rawDelta : rawDelta; final delta = widget.slot == Slots.contextPanel ? -rawDelta : rawDelta;
widget.arrangement.setSize(widget.slot, start + delta); widget.arrangement.setSize(widget.slot, start + delta);
} }
+3 -7
View File
@@ -53,9 +53,7 @@ class SettingsStore extends ChangeNotifier {
return _projectValues[key]; return _projectValues[key];
case SettingsScope.ext: case SettingsScope.ext:
// project overrides app for the same ext.* key // project overrides app for the same ext.* key
return _projectValues.containsKey(key) return _projectValues.containsKey(key) ? _projectValues[key] : _appValues[key];
? _projectValues[key]
: _appValues[key];
} }
} }
@@ -66,8 +64,7 @@ class SettingsStore extends ChangeNotifier {
await _writeFile(_appFile, _appValues); await _writeFile(_appFile, _appValues);
case SettingsScope.project: case SettingsScope.project:
if (projectDir == null) { if (projectDir == null) {
throw StateError( throw StateError('Cannot set project-scoped key with no project open: $key');
'Cannot set project-scoped key with no project open: $key');
} }
_projectValues[key] = value; _projectValues[key] = value;
await _writeFile(_projectFile, _projectValues); await _writeFile(_projectFile, _projectValues);
@@ -109,8 +106,7 @@ class SettingsStore extends ChangeNotifier {
if (key.startsWith('app.')) return SettingsScope.app; if (key.startsWith('app.')) return SettingsScope.app;
if (key.startsWith('project.')) return SettingsScope.project; if (key.startsWith('project.')) return SettingsScope.project;
if (key.startsWith('ext.')) return SettingsScope.ext; if (key.startsWith('ext.')) return SettingsScope.ext;
throw ArgumentError( throw ArgumentError('Settings key must start with app.|project.|ext.: "$key"');
'Settings key must start with app.|project.|ext.: "$key"');
} }
} }
+43 -88
View File
@@ -8,10 +8,15 @@ import 'package:ffi/ffi.dart';
// -- Opaque handles ---------------------------------------------------------- // -- Opaque handles ----------------------------------------------------------
final class TSParser extends Opaque {} final class TSParser extends Opaque {}
final class TSTree extends Opaque {} final class TSTree extends Opaque {}
final class TSQuery extends Opaque {} final class TSQuery extends Opaque {}
final class TSQueryCursor extends Opaque {} final class TSQueryCursor extends Opaque {}
final class TSWasmStore extends Opaque {} final class TSWasmStore extends Opaque {}
final class TSWasmEngine extends Opaque {} final class TSWasmEngine extends Opaque {}
// -- Structs ----------------------------------------------------------------- // -- Structs -----------------------------------------------------------------
@@ -51,10 +56,8 @@ final class TSWasmError extends Struct {
typedef _TsParserNew = Pointer<TSParser> Function(); typedef _TsParserNew = Pointer<TSParser> Function();
typedef _TsParserDelete = Void Function(Pointer<TSParser>); typedef _TsParserDelete = Void Function(Pointer<TSParser>);
typedef _TsParserSetLanguage = Bool Function(Pointer<TSParser>, Pointer<Void>); typedef _TsParserSetLanguage = Bool Function(Pointer<TSParser>, Pointer<Void>);
typedef _TsParserSetWasmStore = Void Function( typedef _TsParserSetWasmStore = Void Function(Pointer<TSParser>, Pointer<TSWasmStore>);
Pointer<TSParser>, Pointer<TSWasmStore>); typedef _TsParserParseString = Pointer<TSTree> Function(Pointer<TSParser>, Pointer<TSTree>, Pointer<Utf8>, Uint32);
typedef _TsParserParseString = Pointer<TSTree> Function(
Pointer<TSParser>, Pointer<TSTree>, Pointer<Utf8>, Uint32);
// Tree // Tree
typedef _TsTreeDelete = Void Function(Pointer<TSTree>); typedef _TsTreeDelete = Void Function(Pointer<TSTree>);
@@ -65,28 +68,21 @@ typedef _TsNodeStartByte = Uint32 Function(TSNode);
typedef _TsNodeEndByte = Uint32 Function(TSNode); typedef _TsNodeEndByte = Uint32 Function(TSNode);
// Query // Query
typedef _TsQueryNew = Pointer<TSQuery> Function( typedef _TsQueryNew = Pointer<TSQuery> Function(Pointer<Void>, Pointer<Utf8>, Uint32, Pointer<Uint32>, Pointer<Int32>);
Pointer<Void>, Pointer<Utf8>, Uint32, Pointer<Uint32>, Pointer<Int32>);
typedef _TsQueryDelete = Void Function(Pointer<TSQuery>); typedef _TsQueryDelete = Void Function(Pointer<TSQuery>);
typedef _TsQueryCaptureCount = Uint32 Function(Pointer<TSQuery>); typedef _TsQueryCaptureCount = Uint32 Function(Pointer<TSQuery>);
typedef _TsQueryCaptureNameForId = Pointer<Utf8> Function( typedef _TsQueryCaptureNameForId = Pointer<Utf8> Function(Pointer<TSQuery>, Uint32, Pointer<Uint32>);
Pointer<TSQuery>, Uint32, Pointer<Uint32>);
// Query cursor // Query cursor
typedef _TsQueryCursorNew = Pointer<TSQueryCursor> Function(); typedef _TsQueryCursorNew = Pointer<TSQueryCursor> Function();
typedef _TsQueryCursorDelete = Void Function(Pointer<TSQueryCursor>); typedef _TsQueryCursorDelete = Void Function(Pointer<TSQueryCursor>);
typedef _TsQueryCursorExec = Void Function( typedef _TsQueryCursorExec = Void Function(Pointer<TSQueryCursor>, Pointer<TSQuery>, TSNode);
Pointer<TSQueryCursor>, Pointer<TSQuery>, TSNode); typedef _TsQueryCursorNextMatch = Bool Function(Pointer<TSQueryCursor>, Pointer<TSQueryMatch>);
typedef _TsQueryCursorNextMatch = Bool Function(
Pointer<TSQueryCursor>, Pointer<TSQueryMatch>);
// WASM store // WASM store
typedef _TsWasmStoreNew = Pointer<TSWasmStore> Function( typedef _TsWasmStoreNew = Pointer<TSWasmStore> Function(Pointer<TSWasmEngine>, Pointer<TSWasmError>);
Pointer<TSWasmEngine>, Pointer<TSWasmError>);
typedef _TsWasmStoreDelete = Void Function(Pointer<TSWasmStore>); typedef _TsWasmStoreDelete = Void Function(Pointer<TSWasmStore>);
typedef _TsWasmStoreLoadLanguage = Pointer<Void> Function( typedef _TsWasmStoreLoadLanguage = Pointer<Void> Function(Pointer<TSWasmStore>, Pointer<Utf8>, Pointer<Uint8>, Uint32, Pointer<TSWasmError>);
Pointer<TSWasmStore>, Pointer<Utf8>, Pointer<Uint8>, Uint32,
Pointer<TSWasmError>);
// WASM engine (from wasmtime C API, re-exported by tree-sitter) // WASM engine (from wasmtime C API, re-exported by tree-sitter)
typedef _WasmEngineNew = Pointer<TSWasmEngine> Function(); typedef _WasmEngineNew = Pointer<TSWasmEngine> Function();
@@ -97,10 +93,8 @@ typedef _WasmEngineDelete = Void Function(Pointer<TSWasmEngine>);
typedef DTsParserNew = Pointer<TSParser> Function(); typedef DTsParserNew = Pointer<TSParser> Function();
typedef DTsParserDelete = void Function(Pointer<TSParser>); typedef DTsParserDelete = void Function(Pointer<TSParser>);
typedef DTsParserSetLanguage = bool Function(Pointer<TSParser>, Pointer<Void>); typedef DTsParserSetLanguage = bool Function(Pointer<TSParser>, Pointer<Void>);
typedef DTsParserSetWasmStore = void Function( typedef DTsParserSetWasmStore = void Function(Pointer<TSParser>, Pointer<TSWasmStore>);
Pointer<TSParser>, Pointer<TSWasmStore>); typedef DTsParserParseString = Pointer<TSTree> Function(Pointer<TSParser>, Pointer<TSTree>, Pointer<Utf8>, int);
typedef DTsParserParseString = Pointer<TSTree> Function(
Pointer<TSParser>, Pointer<TSTree>, Pointer<Utf8>, int);
typedef DTsTreeDelete = void Function(Pointer<TSTree>); typedef DTsTreeDelete = void Function(Pointer<TSTree>);
typedef DTsTreeRootNode = TSNode Function(Pointer<TSTree>); typedef DTsTreeRootNode = TSNode Function(Pointer<TSTree>);
@@ -108,26 +102,19 @@ typedef DTsTreeRootNode = TSNode Function(Pointer<TSTree>);
typedef DTsNodeStartByte = int Function(TSNode); typedef DTsNodeStartByte = int Function(TSNode);
typedef DTsNodeEndByte = int Function(TSNode); typedef DTsNodeEndByte = int Function(TSNode);
typedef DTsQueryNew = Pointer<TSQuery> Function( typedef DTsQueryNew = Pointer<TSQuery> Function(Pointer<Void>, Pointer<Utf8>, int, Pointer<Uint32>, Pointer<Int32>);
Pointer<Void>, Pointer<Utf8>, int, Pointer<Uint32>, Pointer<Int32>);
typedef DTsQueryDelete = void Function(Pointer<TSQuery>); typedef DTsQueryDelete = void Function(Pointer<TSQuery>);
typedef DTsQueryCaptureCount = int Function(Pointer<TSQuery>); typedef DTsQueryCaptureCount = int Function(Pointer<TSQuery>);
typedef DTsQueryCaptureNameForId = Pointer<Utf8> Function( typedef DTsQueryCaptureNameForId = Pointer<Utf8> Function(Pointer<TSQuery>, int, Pointer<Uint32>);
Pointer<TSQuery>, int, Pointer<Uint32>);
typedef DTsQueryCursorNew = Pointer<TSQueryCursor> Function(); typedef DTsQueryCursorNew = Pointer<TSQueryCursor> Function();
typedef DTsQueryCursorDelete = void Function(Pointer<TSQueryCursor>); typedef DTsQueryCursorDelete = void Function(Pointer<TSQueryCursor>);
typedef DTsQueryCursorExec = void Function( typedef DTsQueryCursorExec = void Function(Pointer<TSQueryCursor>, Pointer<TSQuery>, TSNode);
Pointer<TSQueryCursor>, Pointer<TSQuery>, TSNode); typedef DTsQueryCursorNextMatch = bool Function(Pointer<TSQueryCursor>, Pointer<TSQueryMatch>);
typedef DTsQueryCursorNextMatch = bool Function(
Pointer<TSQueryCursor>, Pointer<TSQueryMatch>);
typedef DTsWasmStoreNew = Pointer<TSWasmStore> Function( typedef DTsWasmStoreNew = Pointer<TSWasmStore> Function(Pointer<TSWasmEngine>, Pointer<TSWasmError>);
Pointer<TSWasmEngine>, Pointer<TSWasmError>);
typedef DTsWasmStoreDelete = void Function(Pointer<TSWasmStore>); typedef DTsWasmStoreDelete = void Function(Pointer<TSWasmStore>);
typedef DTsWasmStoreLoadLanguage = Pointer<Void> Function( typedef DTsWasmStoreLoadLanguage = Pointer<Void> Function(Pointer<TSWasmStore>, Pointer<Utf8>, Pointer<Uint8>, int, Pointer<TSWasmError>);
Pointer<TSWasmStore>, Pointer<Utf8>, Pointer<Uint8>, int,
Pointer<TSWasmError>);
typedef DWasmEngineNew = Pointer<TSWasmEngine> Function(); typedef DWasmEngineNew = Pointer<TSWasmEngine> Function();
typedef DWasmEngineDelete = void Function(Pointer<TSWasmEngine>); typedef DWasmEngineDelete = void Function(Pointer<TSWasmEngine>);
@@ -136,60 +123,28 @@ typedef DWasmEngineDelete = void Function(Pointer<TSWasmEngine>);
class TreeSitterLib { class TreeSitterLib {
TreeSitterLib._(DynamicLibrary lib) TreeSitterLib._(DynamicLibrary lib)
: parserNew = lib.lookupFunction<_TsParserNew, DTsParserNew>( : parserNew = lib.lookupFunction<_TsParserNew, DTsParserNew>('ts_parser_new'),
'ts_parser_new'), parserDelete = lib.lookupFunction<_TsParserDelete, DTsParserDelete>('ts_parser_delete'),
parserDelete = lib.lookupFunction<_TsParserDelete, DTsParserDelete>( parserSetLanguage = lib.lookupFunction<_TsParserSetLanguage, DTsParserSetLanguage>('ts_parser_set_language'),
'ts_parser_delete'), parserSetWasmStore = lib.lookupFunction<_TsParserSetWasmStore, DTsParserSetWasmStore>('ts_parser_set_wasm_store'),
parserSetLanguage = parserParseString = lib.lookupFunction<_TsParserParseString, DTsParserParseString>('ts_parser_parse_string'),
lib.lookupFunction<_TsParserSetLanguage, DTsParserSetLanguage>( treeDelete = lib.lookupFunction<_TsTreeDelete, DTsTreeDelete>('ts_tree_delete'),
'ts_parser_set_language'), treeRootNode = lib.lookupFunction<_TsTreeRootNode, DTsTreeRootNode>('ts_tree_root_node'),
parserSetWasmStore = nodeStartByte = lib.lookupFunction<_TsNodeStartByte, DTsNodeStartByte>('ts_node_start_byte'),
lib.lookupFunction<_TsParserSetWasmStore, DTsParserSetWasmStore>( nodeEndByte = lib.lookupFunction<_TsNodeEndByte, DTsNodeEndByte>('ts_node_end_byte'),
'ts_parser_set_wasm_store'), queryNew = lib.lookupFunction<_TsQueryNew, DTsQueryNew>('ts_query_new'),
parserParseString = queryDelete = lib.lookupFunction<_TsQueryDelete, DTsQueryDelete>('ts_query_delete'),
lib.lookupFunction<_TsParserParseString, DTsParserParseString>( queryCaptureCount = lib.lookupFunction<_TsQueryCaptureCount, DTsQueryCaptureCount>('ts_query_capture_count'),
'ts_parser_parse_string'), queryCaptureNameForId = lib.lookupFunction<_TsQueryCaptureNameForId, DTsQueryCaptureNameForId>('ts_query_capture_name_for_id'),
treeDelete = lib.lookupFunction<_TsTreeDelete, DTsTreeDelete>( queryCursorNew = lib.lookupFunction<_TsQueryCursorNew, DTsQueryCursorNew>('ts_query_cursor_new'),
'ts_tree_delete'), queryCursorDelete = lib.lookupFunction<_TsQueryCursorDelete, DTsQueryCursorDelete>('ts_query_cursor_delete'),
treeRootNode = lib.lookupFunction<_TsTreeRootNode, DTsTreeRootNode>( queryCursorExec = lib.lookupFunction<_TsQueryCursorExec, DTsQueryCursorExec>('ts_query_cursor_exec'),
'ts_tree_root_node'), queryCursorNextMatch = lib.lookupFunction<_TsQueryCursorNextMatch, DTsQueryCursorNextMatch>('ts_query_cursor_next_match'),
nodeStartByte = lib.lookupFunction<_TsNodeStartByte, DTsNodeStartByte>( wasmStoreNew = lib.lookupFunction<_TsWasmStoreNew, DTsWasmStoreNew>('ts_wasm_store_new'),
'ts_node_start_byte'), wasmStoreDelete = lib.lookupFunction<_TsWasmStoreDelete, DTsWasmStoreDelete>('ts_wasm_store_delete'),
nodeEndByte = lib.lookupFunction<_TsNodeEndByte, DTsNodeEndByte>( wasmStoreLoadLanguage = lib.lookupFunction<_TsWasmStoreLoadLanguage, DTsWasmStoreLoadLanguage>('ts_wasm_store_load_language'),
'ts_node_end_byte'), wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>('wasm_engine_new'),
queryNew = wasmEngineDelete = lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>('wasm_engine_delete');
lib.lookupFunction<_TsQueryNew, DTsQueryNew>('ts_query_new'),
queryDelete = lib.lookupFunction<_TsQueryDelete, DTsQueryDelete>(
'ts_query_delete'),
queryCaptureCount =
lib.lookupFunction<_TsQueryCaptureCount, DTsQueryCaptureCount>(
'ts_query_capture_count'),
queryCaptureNameForId = lib.lookupFunction<_TsQueryCaptureNameForId,
DTsQueryCaptureNameForId>('ts_query_capture_name_for_id'),
queryCursorNew =
lib.lookupFunction<_TsQueryCursorNew, DTsQueryCursorNew>(
'ts_query_cursor_new'),
queryCursorDelete =
lib.lookupFunction<_TsQueryCursorDelete, DTsQueryCursorDelete>(
'ts_query_cursor_delete'),
queryCursorExec =
lib.lookupFunction<_TsQueryCursorExec, DTsQueryCursorExec>(
'ts_query_cursor_exec'),
queryCursorNextMatch =
lib.lookupFunction<_TsQueryCursorNextMatch, DTsQueryCursorNextMatch>(
'ts_query_cursor_next_match'),
wasmStoreNew = lib.lookupFunction<_TsWasmStoreNew, DTsWasmStoreNew>(
'ts_wasm_store_new'),
wasmStoreDelete =
lib.lookupFunction<_TsWasmStoreDelete, DTsWasmStoreDelete>(
'ts_wasm_store_delete'),
wasmStoreLoadLanguage = lib.lookupFunction<_TsWasmStoreLoadLanguage,
DTsWasmStoreLoadLanguage>('ts_wasm_store_load_language'),
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>(
'wasm_engine_new'),
wasmEngineDelete =
lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>(
'wasm_engine_delete');
final DTsParserNew parserNew; final DTsParserNew parserNew;
final DTsParserDelete parserDelete; final DTsParserDelete parserDelete;
+20 -18
View File
@@ -96,8 +96,7 @@ class TreeSitterService {
try { try {
// Load grammar WASM bytes. // Load grammar WASM bytes.
final wasmData = final wasmData = await rootBundle.load('assets/grammars/$language.wasm');
await rootBundle.load('assets/grammars/$language.wasm');
final wasmBytes = wasmData.buffer.asUint8List(); final wasmBytes = wasmData.buffer.asUint8List();
// Load into WASM store. // Load into WASM store.
@@ -107,7 +106,11 @@ class TreeSitterService {
final error = calloc<TSWasmError>(); final error = calloc<TSWasmError>();
final lang = lib.wasmStoreLoadLanguage( final lang = lib.wasmStoreLoadLanguage(
_store!, nameNative.cast(), wasmNative, wasmBytes.length, error, _store!,
nameNative.cast(),
wasmNative,
wasmBytes.length,
error,
); );
calloc.free(wasmNative); calloc.free(wasmNative);
@@ -125,8 +128,7 @@ class TreeSitterService {
// Load highlight query. // Load highlight query.
String? querySource; String? querySource;
try { try {
querySource = querySource = await rootBundle.loadString('assets/queries/$language.scm');
await rootBundle.loadString('assets/queries/$language.scm');
} catch (_) {} } catch (_) {}
Pointer<TSQuery> query = nullptr; Pointer<TSQuery> query = nullptr;
@@ -139,7 +141,11 @@ class TreeSitterService {
final errorType = calloc<Int32>(); final errorType = calloc<Int32>();
query = lib.queryNew( query = lib.queryNew(
lang, queryNative.cast(), queryLen, errorOffset, errorType, lang,
queryNative.cast(),
queryLen,
errorOffset,
errorType,
); );
calloc.free(queryNative); calloc.free(queryNative);
@@ -205,7 +211,10 @@ class TreeSitterService {
final sourceNative = source.toNativeUtf8(); final sourceNative = source.toNativeUtf8();
final sourceLen = utf8.encode(source).length; final sourceLen = utf8.encode(source).length;
final tree = lib.parserParseString( final tree = lib.parserParseString(
parser, nullptr, sourceNative.cast(), sourceLen, parser,
nullptr,
sourceNative.cast(),
sourceLen,
); );
if (tree == nullptr) { if (tree == nullptr) {
@@ -266,21 +275,14 @@ class TreeSitterService {
static Color colorForRole(String role, SurfaceTokens tokens) { static Color colorForRole(String role, SurfaceTokens tokens) {
return switch (role) { return switch (role) {
'keyword' || 'repeat' || 'conditional' || 'include' || 'keyword' || 'repeat' || 'conditional' || 'include' || 'exception' || 'operator' => tokens.syntaxKeyword,
'exception' || 'operator' =>
tokens.syntaxKeyword,
'type' || 'type.builtin' || 'constructor' => tokens.syntaxType, 'type' || 'type.builtin' || 'constructor' => tokens.syntaxType,
'string' || 'string.special' => tokens.syntaxString, 'string' || 'string.special' => tokens.syntaxString,
'number' || 'float' || 'boolean' => tokens.syntaxNumber, 'number' || 'float' || 'boolean' => tokens.syntaxNumber,
'comment' => tokens.syntaxComment, 'comment' => tokens.syntaxComment,
'function' || 'function.builtin' || 'function.method' || 'function' || 'function.builtin' || 'function.method' || 'method' => tokens.syntaxMethod,
'method' => 'punctuation.bracket' || 'punctuation.delimiter' || 'punctuation.special' => tokens.syntaxPunct,
tokens.syntaxMethod, 'variable' || 'variable.builtin' || 'variable.parameter' => tokens.globalForeground,
'punctuation.bracket' || 'punctuation.delimiter' ||
'punctuation.special' =>
tokens.syntaxPunct,
'variable' || 'variable.builtin' || 'variable.parameter' =>
tokens.globalForeground,
'property' || 'field' => tokens.syntaxMethod, 'property' || 'field' => tokens.syntaxMethod,
'constant' || 'constant.builtin' => tokens.syntaxNumber, 'constant' || 'constant.builtin' => tokens.syntaxNumber,
'tag' || 'attribute' => tokens.syntaxKeyword, 'tag' || 'attribute' => tokens.syntaxKeyword,
+1 -2
View File
@@ -145,7 +145,6 @@ Color _composite(Color src, Color dst) {
} }
double _relativeLuminance(Color c) { double _relativeLuminance(Color c) {
double chan(double v) => double chan(double v) => v <= 0.03928 ? v / 12.92 : math.pow((v + 0.055) / 1.055, 2.4).toDouble();
v <= 0.03928 ? v / 12.92 : math.pow((v + 0.055) / 1.055, 2.4).toDouble();
return 0.2126 * chan(c.r) + 0.7152 * chan(c.g) + 0.0722 * chan(c.b); return 0.2126 * chan(c.r) + 0.7152 * chan(c.g) + 0.0722 * chan(c.b);
} }
+3 -7
View File
@@ -25,9 +25,7 @@ class ThemeController extends ChangeNotifier {
String? initialName, String? initialName,
}) : _resolver = resolver, }) : _resolver = resolver,
_defs = Map.fromEntries(bundled.map((d) => MapEntry(d.name, d))) { _defs = Map.fromEntries(bundled.map((d) => MapEntry(d.name, d))) {
final first = initialName != null && _defs.containsKey(initialName) final first = initialName != null && _defs.containsKey(initialName) ? initialName : bundled.first.name;
? initialName
: bundled.first.name;
_currentName = first; _currentName = first;
_current = _build(first); _current = _build(first);
} }
@@ -89,8 +87,7 @@ class ClideTheme extends InheritedNotifier<ThemeController> {
static ClideThemeData of(BuildContext context) { static ClideThemeData of(BuildContext context) {
final w = context.dependOnInheritedWidgetOfExactType<ClideTheme>(); final w = context.dependOnInheritedWidgetOfExactType<ClideTheme>();
if (w == null) { if (w == null) {
throw FlutterError( throw FlutterError('ClideTheme.of() called with a context that is not a descendant of a ClideTheme.');
'ClideTheme.of() called with a context that is not a descendant of a ClideTheme.');
} }
return w.notifier!.current; return w.notifier!.current;
} }
@@ -98,8 +95,7 @@ class ClideTheme extends InheritedNotifier<ThemeController> {
static ThemeController controllerOf(BuildContext context) { static ThemeController controllerOf(BuildContext context) {
final w = context.dependOnInheritedWidgetOfExactType<ClideTheme>(); final w = context.dependOnInheritedWidgetOfExactType<ClideTheme>();
if (w == null) { if (w == null) {
throw FlutterError( throw FlutterError('ClideTheme.controllerOf() called with a context that is not a descendant of a ClideTheme.');
'ClideTheme.controllerOf() called with a context that is not a descendant of a ClideTheme.');
} }
return w.notifier!; return w.notifier!;
} }
+3 -6
View File
@@ -80,15 +80,13 @@ class ThemeLoader {
displayName: displayName, displayName: displayName,
dark: dark, dark: dark,
palette: palette, palette: palette,
semanticOverride: semanticOverride: semantic is Map ? _parseSemantic(semantic, palette) : null,
semantic is Map ? _parseSemantic(semantic, palette) : null,
surfaceOverride: mergedSurface.isNotEmpty ? mergedSurface : null, surfaceOverride: mergedSurface.isNotEmpty ? mergedSurface : null,
extensionOverride: extension is Map ? _parseRefMap(extension) : null, extensionOverride: extension is Map ? _parseRefMap(extension) : null,
); );
} }
Future<ThemeDefinition> fromAsset( Future<ThemeDefinition> fromAsset(AssetBundle bundle, String assetPath) async {
AssetBundle bundle, String assetPath) async {
final txt = await bundle.loadString(assetPath); final txt = await bundle.loadString(assetPath);
final fallback = assetPath.split('/').last.replaceAll('.yaml', ''); final fallback = assetPath.split('/').last.replaceAll('.yaml', '');
return fromYamlString(txt, fallbackName: fallback); return fromYamlString(txt, fallbackName: fallback);
@@ -115,8 +113,7 @@ SemanticRoles _parseSemantic(Map src, Palette palette) {
final roles = <String, Color>{}; final roles = <String, Color>{};
src.forEach((k, v) { src.forEach((k, v) {
if (v is! String) return; if (v is! String) return;
final resolved = final resolved = v.startsWith('#') ? Palette.parseHex(v) : palette.lookup(v);
v.startsWith('#') ? Palette.parseHex(v) : palette.lookup(v);
if (resolved != null) roles['$k'] = resolved; if (resolved != null) roles['$k'] = resolved;
}); });
return SemanticRoles(roles); return SemanticRoles(roles);
+5 -11
View File
@@ -65,10 +65,8 @@ class ThemeResolver {
sidebarSectionHeader: surface[TokenKeys.sidebarSectionHeader]!, sidebarSectionHeader: surface[TokenKeys.sidebarSectionHeader]!,
statusBarBackground: surface[TokenKeys.statusBarBackground]!, statusBarBackground: surface[TokenKeys.statusBarBackground]!,
statusBarForeground: surface[TokenKeys.statusBarForeground]!, statusBarForeground: surface[TokenKeys.statusBarForeground]!,
statusBarItemActiveBackground: statusBarItemActiveBackground: surface[TokenKeys.statusBarItemActiveBackground]!,
surface[TokenKeys.statusBarItemActiveBackground]!, statusBarItemHoverBackground: surface[TokenKeys.statusBarItemHoverBackground]!,
statusBarItemHoverBackground:
surface[TokenKeys.statusBarItemHoverBackground]!,
tabBarBackground: surface[TokenKeys.tabBarBackground]!, tabBarBackground: surface[TokenKeys.tabBarBackground]!,
tabActive: surface[TokenKeys.tabActive]!, tabActive: surface[TokenKeys.tabActive]!,
tabInactive: surface[TokenKeys.tabInactive]!, tabInactive: surface[TokenKeys.tabInactive]!,
@@ -84,10 +82,8 @@ class ThemeResolver {
listItemBackground: surface[TokenKeys.listItemBackground]!, listItemBackground: surface[TokenKeys.listItemBackground]!,
listItemForeground: surface[TokenKeys.listItemForeground]!, listItemForeground: surface[TokenKeys.listItemForeground]!,
listItemHoverBackground: surface[TokenKeys.listItemHoverBackground]!, listItemHoverBackground: surface[TokenKeys.listItemHoverBackground]!,
listItemSelectedBackground: listItemSelectedBackground: surface[TokenKeys.listItemSelectedBackground]!,
surface[TokenKeys.listItemSelectedBackground]!, listItemSelectedForeground: surface[TokenKeys.listItemSelectedForeground]!,
listItemSelectedForeground:
surface[TokenKeys.listItemSelectedForeground]!,
scrollbarSlider: surface[TokenKeys.scrollbarSlider]!, scrollbarSlider: surface[TokenKeys.scrollbarSlider]!,
scrollbarSliderHover: surface[TokenKeys.scrollbarSliderHover]!, scrollbarSliderHover: surface[TokenKeys.scrollbarSliderHover]!,
scrollbarTrack: surface[TokenKeys.scrollbarTrack]!, scrollbarTrack: surface[TokenKeys.scrollbarTrack]!,
@@ -135,9 +131,7 @@ class ThemeResolver {
// theme never has a null surface color. Themes that omit these // theme never has a null surface color. Themes that omit these
// will land readable if uninspired. // will land readable if uninspired.
roles.putIfAbsent(role, () { roles.putIfAbsent(role, () {
return palette.lookup('foreground') ?? return palette.lookup('foreground') ?? palette.lookup('background') ?? const Color(0xFFFFFFFF);
palette.lookup('background') ??
const Color(0xFFFFFFFF);
}); });
} }
return SemanticRoles(roles); return SemanticRoles(roles);
+17 -18
View File
@@ -67,6 +67,7 @@ class Toolchain extends ChangeNotifier {
if (!c.isCompleted) c.complete(); if (!c.isCompleted) c.complete();
} }
} }
addListener(listener); addListener(listener);
return c.future; return c.future;
} }
@@ -104,17 +105,16 @@ class Toolchain extends ChangeNotifier {
final pql = _findOnPath('pql'); final pql = _findOnPath('pql');
final tmux = _findOnPath('tmux'); final tmux = _findOnPath('tmux');
final shell = _findOnPath( final shell = _findOnPath(Platform.environment['SHELL']?.split('/').last ?? 'bash');
Platform.environment['SHELL']?.split('/').last ?? 'bash');
final ptyc = _firstExisting([ final ptyc = _firstExisting([
'$workspaceRoot/ptyc/bin/ptyc', '$workspaceRoot/ptyc/bin/ptyc',
'$workspaceRoot/native/linux-x64/ptyc', '$workspaceRoot/native/linux-x64/ptyc',
'$workspaceRoot/native/macos-arm64/ptyc', '$workspaceRoot/native/macos-arm64/ptyc',
'$workspaceRoot/native/macos-x64/ptyc', '$workspaceRoot/native/macos-x64/ptyc',
if (Platform.environment['HOME'] case final home?) if (Platform.environment['HOME'] case final home?) '$home/.local/bin/ptyc',
'$home/.local/bin/ptyc', ]) ??
]) ?? _findOnPath('ptyc'); _findOnPath('ptyc');
return ResolvedPaths( return ResolvedPaths(
git: git, git: git,
@@ -184,15 +184,14 @@ ResolvedPaths resolveToolchainPaths(String workspaceRoot) {
pql: _findOnPathStandalone('pql'), pql: _findOnPathStandalone('pql'),
tmux: _findOnPathStandalone('tmux'), tmux: _findOnPathStandalone('tmux'),
ptyc: _firstExistingStandalone([ ptyc: _firstExistingStandalone([
'$workspaceRoot/ptyc/bin/ptyc', '$workspaceRoot/ptyc/bin/ptyc',
'$workspaceRoot/native/linux-x64/ptyc', '$workspaceRoot/native/linux-x64/ptyc',
'$workspaceRoot/native/macos-arm64/ptyc', '$workspaceRoot/native/macos-arm64/ptyc',
'$workspaceRoot/native/macos-x64/ptyc', '$workspaceRoot/native/macos-x64/ptyc',
if (Platform.environment['HOME'] case final home?) if (Platform.environment['HOME'] case final home?) '$home/.local/bin/ptyc',
'$home/.local/bin/ptyc', ]) ??
]) ?? _findOnPathStandalone('ptyc'), _findOnPathStandalone('ptyc'),
shell: _findOnPathStandalone( shell: _findOnPathStandalone(Platform.environment['SHELL']?.split('/').last ?? 'bash'),
Platform.environment['SHELL']?.split('/').last ?? 'bash'),
gitEnv: gitEnv, gitEnv: gitEnv,
); );
} }
+1 -2
View File
@@ -20,8 +20,7 @@ class TrayRegistry extends ChangeNotifier {
} }
Iterable<TrayItemContribution> get items { Iterable<TrayItemContribution> get items {
final sorted = _items.values.toList() final sorted = _items.values.toList()..sort((a, b) => a.priority.compareTo(b.priority));
..sort((a, b) => a.priority.compareTo(b.priority));
return sorted; return sorted;
} }
} }
+1 -2
View File
@@ -6,8 +6,7 @@ class LuaHost {
/// Boot the vendored liblua. Throws until Tier 6. /// Boot the vendored liblua. Throws until Tier 6.
static Future<LuaHost> start() async { static Future<LuaHost> start() async {
throw UnsupportedError( throw UnsupportedError('Lua runtime lands at Tier 6 (supporter tool sibling of ptyc).');
'Lua runtime lands at Tier 6 (supporter tool sibling of ptyc).');
} }
Future<void> dispose() async {} Future<void> dispose() async {}
+4 -5
View File
@@ -95,7 +95,9 @@ Future<IpcResponse> _activate(IpcRequest req, EditorRegistry r) async {
Future<IpcResponse> _list(IpcRequest req, EditorRegistry r) async { Future<IpcResponse> _list(IpcRequest req, EditorRegistry r) async {
return IpcResponse.ok( return IpcResponse.ok(
id: req.id, id: req.id,
data: {'buffers': [for (final b in r.buffers) b.toJson()]}, data: {
'buffers': [for (final b in r.buffers) b.toJson()]
},
); );
} }
@@ -139,9 +141,7 @@ Future<IpcResponse> _setContent(IpcRequest req, EditorRegistry r) async {
if (id == null) return _notFound(req.id, 'no active buffer'); if (id == null) return _notFound(req.id, 'no active buffer');
if (r.get(id) == null) return _notFound(req.id, 'no such buffer: $id'); if (r.get(id) == null) return _notFound(req.id, 'no such buffer: $id');
final content = EditorRegistry.contentFromArgs(req.args); final content = EditorRegistry.contentFromArgs(req.args);
final sel = req.args['selection'] == null final sel = req.args['selection'] == null ? null : EditorRegistry.selectionFromArgs(req.args['selection']);
? null
: EditorRegistry.selectionFromArgs(req.args['selection']);
r.setContent(id, content, selection: sel); r.setContent(id, content, selection: sel);
return IpcResponse.ok(id: req.id, data: {'id': id, 'length': content.length}); return IpcResponse.ok(id: req.id, data: {'id': id, 'length': content.length});
} }
@@ -161,4 +161,3 @@ Future<IpcResponse> _close(IpcRequest req, EditorRegistry r) async {
r.close(id); r.close(id);
return IpcResponse.ok(id: req.id, data: {'id': id}); return IpcResponse.ok(id: req.id, data: {'id': id});
} }
+9 -7
View File
@@ -56,13 +56,15 @@ class FilesService {
} }
void registerFilesCommands(DaemonDispatcher d, FilesService files) { void registerFilesCommands(DaemonDispatcher d, FilesService files) {
d.register('files.root', (req) async => IpcResponse.ok( d.register(
id: req.id, 'files.root',
data: { (req) async => IpcResponse.ok(
'path': files.root.absolute.path, id: req.id,
'ignorePatterns': files.ignore.length, data: {
}, 'path': files.root.absolute.path,
)); 'ignorePatterns': files.ignore.length,
},
));
d.register('files.read', (req) async { d.register('files.read', (req) async {
final path = req.args['path'] as String?; final path = req.args['path'] as String?;
+3 -1
View File
@@ -227,7 +227,9 @@ void registerGitCommands(
try { try {
final b = await git.branches(); final b = await git.branches();
return IpcResponse.ok(id: req.id, data: { return IpcResponse.ok(id: req.id, data: {
'branches': [for (final e in b) {'name': e.name, 'current': e.current}], 'branches': [
for (final e in b) {'name': e.name, 'current': e.current}
],
}); });
} on GitException catch (e) { } on GitException catch (e) {
return _gitError(req.id, e); return _gitError(req.id, e);
+5 -5
View File
@@ -27,8 +27,7 @@ void registerPaneCommands(DaemonDispatcher d, PaneRegistry registry) {
d.register('pane.tail', (req) => _tail(req, registry)); d.register('pane.tail', (req) => _tail(req, registry));
} }
IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse _userErr(String id, String message, {String? hint}) => IpcResponse.err(
IpcResponse.err(
id: id, id: id,
error: IpcError( error: IpcError(
code: IpcExitCode.userError, code: IpcExitCode.userError,
@@ -70,8 +69,7 @@ Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
Map<String, String>? env; Map<String, String>? env;
if (envArg is Map) { if (envArg is Map) {
env = { env = {
for (final e in envArg.entries) for (final e in envArg.entries) '${e.key}': '${e.value}',
'${e.key}': '${e.value}',
}; };
} }
@@ -101,7 +99,9 @@ Future<IpcResponse> _spawn(IpcRequest req, PaneRegistry registry) async {
Future<IpcResponse> _list(IpcRequest req, PaneRegistry registry) async { Future<IpcResponse> _list(IpcRequest req, PaneRegistry registry) async {
return IpcResponse.ok( return IpcResponse.ok(
id: req.id, id: req.id,
data: {'panes': [for (final p in registry.panes) p.toJson()]}, data: {
'panes': [for (final p in registry.panes) p.toJson()]
},
); );
} }
+5 -1
View File
@@ -211,7 +211,11 @@ void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
d.register('pql.tickets.status', (req) async { d.register('pql.tickets.status', (req) async {
final rawIds = req.args['ids']; final rawIds = req.args['ids'];
final ids = rawIds is List ? rawIds.cast<String>() : rawIds is String ? [rawIds] : <String>[]; final ids = rawIds is List
? rawIds.cast<String>()
: rawIds is String
? [rawIds]
: <String>[];
final status = req.args['status'] as String?; final status = req.args['status'] as String?;
if (ids.isEmpty || status == null || status.isEmpty) { if (ids.isEmpty || status == null || status.isEmpty) {
return _userError(req.id, 'pql.tickets.status requires ids and status'); return _userError(req.id, 'pql.tickets.status requires ids and status');
+1 -2
View File
@@ -27,8 +27,7 @@ class Selection {
); );
@override @override
bool operator ==(Object other) => bool operator ==(Object other) => other is Selection && other.start == start && other.end == end;
other is Selection && other.start == start && other.end == end;
@override @override
int get hashCode => Object.hash(start, end); int get hashCode => Object.hash(start, end);
+1 -2
View File
@@ -31,8 +31,7 @@ class EditorRegistry {
Iterable<EditorBuffer> get buffers => _buffers.values; Iterable<EditorBuffer> get buffers => _buffers.values;
EditorBuffer? get(String id) => _buffers[id]; EditorBuffer? get(String id) => _buffers[id];
EditorBuffer? get active => EditorBuffer? get active => _activeId == null ? null : _buffers[_activeId!];
_activeId == null ? null : _buffers[_activeId!];
/// Open a file. If [path] is already open, returns the existing /// Open a file. If [path] is already open, returns the existing
/// buffer (no re-read from disk — the in-memory content is the /// buffer (no re-read from disk — the in-memory content is the
+2 -6
View File
@@ -47,16 +47,12 @@ Future<List<FileEntry>> listDir({
required String dir, required String dir,
required IgnoreSet ignore, required IgnoreSet ignore,
}) async { }) async {
final resolved = dir.isEmpty final resolved = dir.isEmpty ? root : Directory('${root.absolute.path}${Platform.pathSeparator}${dir.replaceAll('/', Platform.pathSeparator)}');
? root
: Directory('${root.absolute.path}${Platform.pathSeparator}${dir.replaceAll('/', Platform.pathSeparator)}');
if (!await resolved.exists()) return const []; if (!await resolved.exists()) return const [];
final entries = <FileEntry>[]; final entries = <FileEntry>[];
await for (final e in resolved.list(followLinks: false)) { await for (final e in resolved.list(followLinks: false)) {
final name = e.uri.pathSegments.isNotEmpty final name = e.uri.pathSegments.isNotEmpty ? e.uri.pathSegments.where((s) => s.isNotEmpty).last : '';
? e.uri.pathSegments.where((s) => s.isNotEmpty).last
: '';
final rel = dir.isEmpty ? name : '$dir/$name'; final rel = dir.isEmpty ? name : '$dir/$name';
final stat = await e.stat(); final stat = await e.stat();
final isDir = stat.type == FileSystemEntityType.directory; final isDir = stat.type == FileSystemEntityType.directory;
+3 -3
View File
@@ -71,9 +71,9 @@ class FileWatcher {
Future<void> start() async { Future<void> start() async {
if (_sub != null) return; if (_sub != null) return;
_sub = root.watch(recursive: true).listen( _sub = root.watch(recursive: true).listen(
_onEvent, _onEvent,
onError: (Object e, StackTrace _) => _controller.addError(e), onError: (Object e, StackTrace _) => _controller.addError(e),
); );
} }
Future<void> stop() async { Future<void> stop() async {
+2 -6
View File
@@ -209,9 +209,7 @@ class GitClient {
Future<ProcessResult> _run(List<String> args) async { Future<ProcessResult> _run(List<String> args) async {
try { try {
return await Process.run(toolchain.git, args, return await Process.run(toolchain.git, args, workingDirectory: workDir.path, environment: toolchain.gitEnv);
workingDirectory: workDir.path,
environment: toolchain.gitEnv);
} on ProcessException catch (e) { } on ProcessException catch (e) {
throw GitException('git ${args.first}: ${e.message}', stderr: e.toString()); throw GitException('git ${args.first}: ${e.message}', stderr: e.toString());
} }
@@ -223,9 +221,7 @@ class GitClient {
if (reverse) args.add('--reverse'); if (reverse) args.add('--reverse');
args.addAll(['--unidiff-zero', '-']); args.addAll(['--unidiff-zero', '-']);
final proc = await Process.start(toolchain.git, args, final proc = await Process.start(toolchain.git, args, workingDirectory: workDir.path, environment: toolchain.gitEnv);
workingDirectory: workDir.path,
environment: toolchain.gitEnv);
proc.stdin.write(patch); proc.stdin.write(patch);
await proc.stdin.close(); await proc.stdin.close();
final exitCode = await proc.exitCode; final exitCode = await proc.exitCode;
+3 -5
View File
@@ -16,6 +16,7 @@ String get gitBin {
_gitBin ??= _resolveGit(); _gitBin ??= _resolveGit();
return _gitBin!; return _gitBin!;
} }
String? _gitBin; String? _gitBin;
String _resolveGit() { String _resolveGit() {
@@ -215,8 +216,7 @@ Future<String> gitPush(
} }
/// List local branches. Returns (name, isCurrent) pairs. /// List local branches. Returns (name, isCurrent) pairs.
Future<List<({String name, bool current})>> gitBranches( Future<List<({String name, bool current})>> gitBranches(Directory workDir) async {
Directory workDir) async {
final r = await Process.run( final r = await Process.run(
gitBin, gitBin,
['branch', '--format=%(refname:short)|%(HEAD)'], ['branch', '--format=%(refname:short)|%(HEAD)'],
@@ -302,9 +302,7 @@ Future<void> _applyPatch(
await proc.stdin.close(); await proc.stdin.close();
final exitCode = await proc.exitCode; final exitCode = await proc.exitCode;
if (exitCode != 0) { if (exitCode != 0) {
final stderr = await proc.stderr final stderr = await proc.stderr.transform(const SystemEncoding().decoder).join();
.transform(const SystemEncoding().decoder)
.join();
throw GitException( throw GitException(
'git apply failed', 'git apply failed',
stderr: stderr, stderr: stderr,
+6 -17
View File
@@ -44,15 +44,8 @@ class GitFileStatus {
final String? origPath; final String? origPath;
final GitConflictType? conflictType; final GitConflictType? conflictType;
bool get isStaged => bool get isStaged => indexState != null && indexState != GitFileState.untracked && indexState != GitFileState.ignored && !isConflicted;
indexState != null && bool get isUnstaged => workTreeState != null && workTreeState != GitFileState.untracked && !isConflicted;
indexState != GitFileState.untracked &&
indexState != GitFileState.ignored &&
!isConflicted;
bool get isUnstaged =>
workTreeState != null &&
workTreeState != GitFileState.untracked &&
!isConflicted;
bool get isUntracked => workTreeState == GitFileState.untracked; bool get isUntracked => workTreeState == GitFileState.untracked;
bool get isConflicted => conflictType != null; bool get isConflicted => conflictType != null;
@@ -84,14 +77,10 @@ class GitStatus {
final int behind; final int behind;
final List<GitFileStatus> entries; final List<GitFileStatus> entries;
List<GitFileStatus> get staged => List<GitFileStatus> get staged => entries.where((e) => e.isStaged).toList();
entries.where((e) => e.isStaged).toList(); List<GitFileStatus> get unstaged => entries.where((e) => e.isUnstaged).toList();
List<GitFileStatus> get unstaged => List<GitFileStatus> get untracked => entries.where((e) => e.isUntracked).toList();
entries.where((e) => e.isUnstaged).toList(); List<GitFileStatus> get conflicted => entries.where((e) => e.isConflicted).toList();
List<GitFileStatus> get untracked =>
entries.where((e) => e.isUntracked).toList();
List<GitFileStatus> get conflicted =>
entries.where((e) => e.isConflicted).toList();
bool get isClean => entries.isEmpty; bool get isClean => entries.isEmpty;
bool get hasConflicts => entries.any((e) => e.isConflicted); bool get hasConflicts => entries.any((e) => e.isConflicted);
+1 -3
View File
@@ -90,9 +90,7 @@ class IpcResponse extends IpcMessage {
id: j['id']! as String, id: j['id']! as String,
ok: ok, ok: ok,
data: (j['data'] as Map?)?.cast<String, Object?>() ?? const {}, data: (j['data'] as Map?)?.cast<String, Object?>() ?? const {},
error: ok error: ok ? null : IpcError.fromJson((j['error'] as Map).cast<String, Object?>()),
? null
: IpcError.fromJson((j['error'] as Map).cast<String, Object?>()),
); );
} }
} }
+1 -5
View File
@@ -69,11 +69,7 @@ class DaemonServer {
void _handleClient(Socket client) { void _handleClient(Socket client) {
_clients.add(client); _clients.add(client);
client client.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()).listen(
.cast<List<int>>()
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
(line) => _handleLine(client, line), (line) => _handleLine(client, line),
onDone: () => _clients.remove(client), onDone: () => _clients.remove(client),
onError: (Object e) { onError: (Object e) {
+2 -4
View File
@@ -22,10 +22,8 @@ class RecordingEventSink implements DaemonEventSink {
void emit(IpcEvent event) => events.add(event); void emit(IpcEvent event) => events.add(event);
/// Convenience: filter to a single subsystem (`pane`, `git`, …). /// Convenience: filter to a single subsystem (`pane`, `git`, …).
Iterable<IpcEvent> ofSubsystem(String subsystem) => Iterable<IpcEvent> ofSubsystem(String subsystem) => events.where((e) => e.subsystem == subsystem);
events.where((e) => e.subsystem == subsystem);
/// Convenience: filter to a specific `type` (`pane.spawned`, …). /// Convenience: filter to a specific `type` (`pane.spawned`, …).
Iterable<IpcEvent> ofKind(String kind) => Iterable<IpcEvent> ofKind(String kind) => events.where((e) => e.kind == kind);
events.where((e) => e.kind == kind);
} }
+2 -1
View File
@@ -39,7 +39,8 @@ class ClideAccordion extends StatelessWidget {
ClideIcon(expanded ? PhosphorIcons.caretDown : PhosphorIcons.caretRight, size: 10, color: tokens.globalTextMuted), ClideIcon(expanded ? PhosphorIcons.caretDown : PhosphorIcons.caretRight, size: 10, color: tokens.globalTextMuted),
const SizedBox(width: 6), const SizedBox(width: 6),
if (leading != null) ...[leading!, const SizedBox(width: 6)], if (leading != null) ...[leading!, const SizedBox(width: 6)],
ClideText('$label · $count', fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader, fontFamily: clideMonoFamily), ClideText('$label · $count',
fontSize: clideFontSmall, color: hovered ? tokens.globalForeground : tokens.sidebarSectionHeader, fontFamily: clideMonoFamily),
], ],
), ),
), ),
+1 -2
View File
@@ -52,6 +52,5 @@ class _IconPainterAdapter extends CustomPainter {
} }
@override @override
bool shouldRepaint(covariant _IconPainterAdapter old) => bool shouldRepaint(covariant _IconPainterAdapter old) => old.painter != painter || old.color != color;
old.painter != painter || old.color != color;
} }
+12 -3
View File
@@ -130,7 +130,10 @@ class ClideMarkdown extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [for (final c in el.children ?? const []) if (c is md.Element) _buildListItem(c, tokens, onRecordTap, ordered: false)], children: [
for (final c in el.children ?? const [])
if (c is md.Element) _buildListItem(c, tokens, onRecordTap, ordered: false)
],
), ),
); );
case 'ol': case 'ol':
@@ -266,12 +269,18 @@ class ClideMarkdown extends StatelessWidget {
case 'strong': case 'strong':
return TextSpan( return TextSpan(
style: const TextStyle(fontWeight: FontWeight.w700), style: const TextStyle(fontWeight: FontWeight.w700),
children: [for (final c in el.children ?? const []) if (c is md.Text) TextSpan(text: _unescapeHtml(c.text)) else if (c is md.Element) _inlineElementSpan(c, tokens, onRecordTap)], children: [
for (final c in el.children ?? const [])
if (c is md.Text) TextSpan(text: _unescapeHtml(c.text)) else if (c is md.Element) _inlineElementSpan(c, tokens, onRecordTap)
],
); );
case 'em': case 'em':
return TextSpan( return TextSpan(
style: const TextStyle(fontStyle: FontStyle.italic), style: const TextStyle(fontStyle: FontStyle.italic),
children: [for (final c in el.children ?? const []) if (c is md.Text) TextSpan(text: _unescapeHtml(c.text)) else if (c is md.Element) _inlineElementSpan(c, tokens, onRecordTap)], children: [
for (final c in el.children ?? const [])
if (c is md.Text) TextSpan(text: _unescapeHtml(c.text)) else if (c is md.Element) _inlineElementSpan(c, tokens, onRecordTap)
],
); );
case 'code': case 'code':
return TextSpan( return TextSpan(
+6 -5
View File
@@ -151,12 +151,13 @@ class _Header extends StatelessWidget {
], ],
), ),
), ),
if (trailing != null) ...trailing!.map( if (trailing != null)
(w) => Padding( ...trailing!.map(
padding: const EdgeInsets.only(left: 6), (w) => Padding(
child: w, padding: const EdgeInsets.only(left: 6),
child: w,
),
), ),
),
if (onClose != null) if (onClose != null)
Padding( Padding(
padding: const EdgeInsets.only(left: 6), padding: const EdgeInsets.only(left: 6),
+1 -4
View File
@@ -47,8 +47,5 @@ class ScrollbarTheme extends InheritedWidget {
final Color track; final Color track;
@override @override
bool updateShouldNotify(ScrollbarTheme old) => bool updateShouldNotify(ScrollbarTheme old) => slider != old.slider || sliderHover != old.sliderHover || track != old.track;
slider != old.slider ||
sliderHover != old.sliderHover ||
track != old.track;
} }
+1 -2
View File
@@ -37,8 +37,7 @@ class ClideText extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
final resolved = final resolved = color ?? (muted ? tokens.globalTextMuted : tokens.globalForeground);
color ?? (muted ? tokens.globalTextMuted : tokens.globalForeground);
return Text( return Text(
data, data,
maxLines: maxLines, maxLines: maxLines,
+1 -2
View File
@@ -18,8 +18,7 @@ class PlugIcon extends ClideIconPainter {
..moveTo(0.30, 0.30) ..moveTo(0.30, 0.30)
..lineTo(0.60, 0.30) ..lineTo(0.60, 0.30)
..lineTo(0.60, 0.55) ..lineTo(0.60, 0.55)
..arcToPoint(const Offset(0.30, 0.55), ..arcToPoint(const Offset(0.30, 0.55), radius: const Radius.circular(0.15), clockwise: false)
radius: const Radius.circular(0.15), clockwise: false)
..close(); ..close();
canvas.drawPath(body, p); canvas.drawPath(body, p);
// cord // cord
+2 -4
View File
@@ -33,14 +33,12 @@ void main() {
expect(node.hasFocus, isTrue); expect(node.hasFocus, isTrue);
}); });
testWidgets('interactive widgets expose tap actions to a11y', testWidgets('interactive widgets expose tap actions to a11y', (tester) async {
(tester) async {
await tester.pumpWidget( await tester.pumpWidget(
harness(f, ClideButton(label: 'Save', onPressed: () {})), harness(f, ClideButton(label: 'Save', onPressed: () {})),
); );
final handle = tester.ensureSemantics(); final handle = tester.ensureSemantics();
final data = final data = tester.getSemantics(find.byType(ClideButton)).getSemanticsData();
tester.getSemantics(find.byType(ClideButton)).getSemanticsData();
expect(data.hasAction(SemanticsAction.tap), isTrue); expect(data.hasAction(SemanticsAction.tap), isTrue);
handle.dispose(); handle.dispose();
}); });
+3 -7
View File
@@ -34,19 +34,15 @@ void main() {
test('tab contributions carry title + i18n key + namespace', () { test('tab contributions carry title + i18n key + namespace', () {
final tabs = ext.contributions.whereType<TabContribution>().toList(); final tabs = ext.contributions.whereType<TabContribution>().toList();
for (final t in tabs) { for (final t in tabs) {
expect(t.title, isNotEmpty, expect(t.title, isNotEmpty, reason: '${ext.id} tab ${t.id} missing English title');
reason: '${ext.id} tab ${t.id} missing English title');
if (t.titleKey != null) { if (t.titleKey != null) {
expect(t.i18nNamespace, isNotNull, expect(t.i18nNamespace, isNotNull, reason: '${ext.id} tab ${t.id} has titleKey but no namespace');
reason:
'${ext.id} tab ${t.id} has titleKey but no namespace');
} }
} }
}); });
test('command contributions carry stable ids', () { test('command contributions carry stable ids', () {
final cmds = final cmds = ext.contributions.whereType<CommandContribution>().toList();
ext.contributions.whereType<CommandContribution>().toList();
for (final c in cmds) { for (final c in cmds) {
expect(c.command, isNotEmpty); expect(c.command, isNotEmpty);
expect(c.id, isNotEmpty); expect(c.id, isNotEmpty);
+1 -4
View File
@@ -20,10 +20,7 @@ void main() {
test('contributes a statusbar item', () async { test('contributes a statusbar item', () async {
f.services.extensions.register(IpcStatusExtension()); f.services.extensions.register(IpcStatusExtension());
await f.services.extensions.activateAll(); await f.services.extensions.activateAll();
final items = f.services.panels final items = f.services.panels.contributionsFor(Slots.statusbar).whereType<StatusItemContribution>().toList();
.contributionsFor(Slots.statusbar)
.whereType<StatusItemContribution>()
.toList();
expect(items, hasLength(1)); expect(items, hasLength(1));
expect(items.first.priority, 100); expect(items.first.priority, 100);
}); });
+1 -2
View File
@@ -80,8 +80,7 @@ void main() {
expect(find.text('Cancel'), findsOneWidget); expect(find.text('Cancel'), findsOneWidget);
}); });
testWidgets('tapping a row calls controller.select + onDismiss', testWidgets('tapping a row calls controller.select + onDismiss', (tester) async {
(tester) async {
String? dismissed; String? dismissed;
await tester.pumpWidget( await tester.pumpWidget(
harness( harness(
+1 -3
View File
@@ -20,9 +20,7 @@ void main() {
'builtin.welcome': { 'builtin.welcome': {
const Locale('en', 'US'): const { const Locale('en', 'US'): const {
'title': {'translation': 'clide'}, 'title': {'translation': 'clide'},
'subtitle': { 'subtitle': {'translation': 'Flutter desktop IDE for Claude Code'},
'translation': 'Flutter desktop IDE for Claude Code'
},
'open-project': {'translation': 'Open project'}, 'open-project': {'translation': 'Open project'},
'open-project.hint': {'translation': 'Pick a git repository'}, 'open-project.hint': {'translation': 'Pick a git repository'},
'tab.title': {'translation': 'Welcome'}, 'tab.title': {'translation': 'Welcome'},
+4 -12
View File
@@ -49,10 +49,7 @@ void main() {
); );
// Wait for the "listening" line on stderr so we know it's ready. // Wait for the "listening" line on stderr so we know it's ready.
final ready = Completer<void>(); final ready = Completer<void>();
daemon.stderr daemon.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((line) {
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
if (!ready.isCompleted && line.contains('listening')) { if (!ready.isCompleted && line.contains('listening')) {
ready.complete(); ready.complete();
} }
@@ -62,8 +59,7 @@ void main() {
tearDown(() async { tearDown(() async {
daemon.kill(ProcessSignal.sigterm); daemon.kill(ProcessSignal.sigterm);
await daemon.exitCode.timeout(const Duration(seconds: 3), await daemon.exitCode.timeout(const Duration(seconds: 3), onTimeout: () {
onTimeout: () {
daemon.kill(ProcessSignal.sigkill); daemon.kill(ProcessSignal.sigkill);
return -1; return -1;
}); });
@@ -129,10 +125,7 @@ void main() {
); );
final received = <Map<String, Object?>>[]; final received = <Map<String, Object?>>[];
final sub = tail.stdout final sub = tail.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((line) {
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
if (line.isEmpty) return; if (line.isEmpty) return;
received.add(jsonDecode(line) as Map<String, Object?>); received.add(jsonDecode(line) as Map<String, Object?>);
}); });
@@ -149,8 +142,7 @@ void main() {
} }
tail.kill(ProcessSignal.sigint); tail.kill(ProcessSignal.sigint);
await tail.exitCode.timeout(const Duration(seconds: 2), await tail.exitCode.timeout(const Duration(seconds: 2), onTimeout: () {
onTimeout: () {
tail.kill(ProcessSignal.sigkill); tail.kill(ProcessSignal.sigkill);
return -1; return -1;
}); });
+22 -9
View File
@@ -44,8 +44,7 @@ void main() {
if (sandbox.existsSync()) sandbox.deleteSync(recursive: true); if (sandbox.existsSync()) sandbox.deleteSync(recursive: true);
}); });
Future<IpcResponse> call(String cmd, Future<IpcResponse> call(String cmd, [Map<String, Object?> args = const {}]) {
[Map<String, Object?> args = const {}]) {
return dispatcher.dispatch(IpcRequest(id: '1', cmd: cmd, args: args)); return dispatcher.dispatch(IpcRequest(id: '1', cmd: cmd, args: args));
} }
@@ -66,7 +65,9 @@ void main() {
test('git.stage + git.status shows staged file', () async { test('git.stage + git.status shows staged file', () async {
await File('${sandbox.path}/new.txt').writeAsString('x'); await File('${sandbox.path}/new.txt').writeAsString('x');
final stage = await call('git.stage', {'paths': ['new.txt']}); final stage = await call('git.stage', {
'paths': ['new.txt']
});
expect(stage.ok, isTrue); expect(stage.ok, isTrue);
final r = await call('git.status'); final r = await call('git.status');
@@ -82,8 +83,12 @@ void main() {
test('git.unstage removes from staging', () async { test('git.unstage removes from staging', () async {
await File('${sandbox.path}/new.txt').writeAsString('x'); await File('${sandbox.path}/new.txt').writeAsString('x');
await call('git.stage', {'paths': ['new.txt']}); await call('git.stage', {
final unstage = await call('git.unstage', {'paths': ['new.txt']}); 'paths': ['new.txt']
});
final unstage = await call('git.unstage', {
'paths': ['new.txt']
});
expect(unstage.ok, isTrue); expect(unstage.ok, isTrue);
final r = await call('git.status'); final r = await call('git.status');
@@ -93,7 +98,9 @@ void main() {
test('git.commit creates a commit', () async { test('git.commit creates a commit', () async {
await File('${sandbox.path}/c.txt').writeAsString('x'); await File('${sandbox.path}/c.txt').writeAsString('x');
await call('git.stage', {'paths': ['c.txt']}); await call('git.stage', {
'paths': ['c.txt']
});
final r = await call('git.commit', {'message': 'test commit'}); final r = await call('git.commit', {'message': 'test commit'});
expect(r.ok, isTrue); expect(r.ok, isTrue);
expect(r.data['hash'], hasLength(40)); expect(r.data['hash'], hasLength(40));
@@ -115,7 +122,9 @@ void main() {
test('git.diff --staged returns staged diffs', () async { test('git.diff --staged returns staged diffs', () async {
await File('${sandbox.path}/file.txt').writeAsString('modified\n'); await File('${sandbox.path}/file.txt').writeAsString('modified\n');
await call('git.stage', {'paths': ['file.txt']}); await call('git.stage', {
'paths': ['file.txt']
});
final r = await call('git.diff', {'staged': true}); final r = await call('git.diff', {'staged': true});
expect(r.ok, isTrue); expect(r.ok, isTrue);
final diffs = r.data['diffs'] as List; final diffs = r.data['diffs'] as List;
@@ -131,7 +140,9 @@ void main() {
test('git.discard restores a file', () async { test('git.discard restores a file', () async {
await File('${sandbox.path}/file.txt').writeAsString('changed'); await File('${sandbox.path}/file.txt').writeAsString('changed');
final r = await call('git.discard', {'paths': ['file.txt']}); final r = await call('git.discard', {
'paths': ['file.txt']
});
expect(r.ok, isTrue); expect(r.ok, isTrue);
final content = await File('${sandbox.path}/file.txt').readAsString(); final content = await File('${sandbox.path}/file.txt').readAsString();
expect(content, 'hello\n'); expect(content, 'hello\n');
@@ -145,7 +156,9 @@ void main() {
test('mutations emit git.changed events', () async { test('mutations emit git.changed events', () async {
await File('${sandbox.path}/e.txt').writeAsString('x'); await File('${sandbox.path}/e.txt').writeAsString('x');
await call('git.stage', {'paths': ['e.txt']}); await call('git.stage', {
'paths': ['e.txt']
});
expect( expect(
sink.events, sink.events,
contains(predicate<IpcEvent>((e) => e.kind == 'git.changed')), contains(predicate<IpcEvent>((e) => e.kind == 'git.changed')),
+3 -13
View File
@@ -71,19 +71,14 @@ void main() {
} }
final lines = <String>[]; final lines = <String>[];
final done = Completer<void>(); final done = Completer<void>();
final sub = socket final sub = socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()).listen((line) {
.cast<List<int>>()
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
lines.add(line); lines.add(line);
if (lines.length == 5) done.complete(); if (lines.length == 5) done.complete();
}); });
await done.future.timeout(const Duration(seconds: 2)); await done.future.timeout(const Duration(seconds: 2));
await sub.cancel(); await sub.cancel();
await socket.close(); await socket.close();
final ids = final ids = lines.map((l) => (IpcMessage.decode(l) as IpcResponse).id).toSet();
lines.map((l) => (IpcMessage.decode(l) as IpcResponse).id).toSet();
expect(ids, {'0', '1', '2', '3', '4'}); expect(ids, {'0', '1', '2', '3', '4'});
}); });
@@ -112,12 +107,7 @@ Future<String> _send(String socketPath, String line) async {
0, 0,
); );
socket.writeln(line); socket.writeln(line);
final resp = await socket final resp = await socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()).first.timeout(const Duration(seconds: 2));
.cast<List<int>>()
.transform(utf8.decoder)
.transform(const LineSplitter())
.first
.timeout(const Duration(seconds: 2));
await socket.close(); await socket.close();
return resp; return resp;
} }
+4 -14
View File
@@ -15,8 +15,7 @@ void main() {
group('bin/clide --daemon (subprocess)', () { group('bin/clide --daemon (subprocess)', () {
setUpAll(() { setUpAll(() {
if (!binary.existsSync()) { if (!binary.existsSync()) {
markTestSkipped( markTestSkipped('bin/clide not built; run `make build` first to enable this suite');
'bin/clide not built; run `make build` first to enable this suite');
} }
}); });
@@ -41,10 +40,7 @@ void main() {
// Wait for "listening on ..." on stderr before connecting. // Wait for "listening on ..." on stderr before connecting.
final ready = Completer<void>(); final ready = Completer<void>();
final stderrLines = <String>[]; final stderrLines = <String>[];
final sub = process.stderr final sub = process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((line) {
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
stderrLines.add(line); stderrLines.add(line);
if (line.contains('listening')) ready.complete(); if (line.contains('listening')) ready.complete();
}); });
@@ -58,12 +54,7 @@ void main() {
0, 0,
).timeout(const Duration(seconds: 3)); ).timeout(const Duration(seconds: 3));
sock.writeln(IpcRequest(id: '1', cmd: 'ping').encode()); sock.writeln(IpcRequest(id: '1', cmd: 'ping').encode());
final line = await sock final line = await sock.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()).first.timeout(const Duration(seconds: 3));
.cast<List<int>>()
.transform(utf8.decoder)
.transform(const LineSplitter())
.first
.timeout(const Duration(seconds: 3));
await sock.close(); await sock.close();
final resp = IpcMessage.decode(line) as IpcResponse; final resp = IpcMessage.decode(line) as IpcResponse;
expect(resp.ok, true); expect(resp.ok, true);
@@ -71,8 +62,7 @@ void main() {
// Clean shutdown // Clean shutdown
process.kill(ProcessSignal.sigterm); process.kill(ProcessSignal.sigterm);
final exitCode = final exitCode = await process.exitCode.timeout(const Duration(seconds: 3));
await process.exitCode.timeout(const Duration(seconds: 3));
expect(exitCode, 0); expect(exitCode, 0);
// Socket file should be unlinked // Socket file should be unlinked
+1 -2
View File
@@ -76,8 +76,7 @@ void main() {
expect(sink.ofKind('editor.saved'), hasLength(1)); expect(sink.ofKind('editor.saved'), hasLength(1));
}); });
test('close picks a new active buffer when the active one closes', test('close picks a new active buffer when the active one closes', () async {
() async {
final a = await reg.open('README.md'); final a = await reg.open('README.md');
await File('${sandbox.path}/b.txt').writeAsString('two'); await File('${sandbox.path}/b.txt').writeAsString('two');
final b = await reg.open('b.txt'); final b = await reg.open('b.txt');
+2 -4
View File
@@ -30,11 +30,9 @@ void main() {
test('discovers extensions from their own subdirs', () async { test('discovers extensions from their own subdirs', () async {
final a = Directory('${root.path}/ext.a')..createSync(); final a = Directory('${root.path}/ext.a')..createSync();
await File('${a.path}/manifest.yaml') await File('${a.path}/manifest.yaml').writeAsString('id: ext.a\ntitle: A\nversion: 1.0.0\n');
.writeAsString('id: ext.a\ntitle: A\nversion: 1.0.0\n');
final b = Directory('${root.path}/ext.b')..createSync(); final b = Directory('${root.path}/ext.b')..createSync();
await File('${b.path}/manifest.yaml') await File('${b.path}/manifest.yaml').writeAsString('id: ext.b\ntitle: B\nversion: 1.2.0\n');
.writeAsString('id: ext.b\ntitle: B\nversion: 1.2.0\n');
final out = await const ExtensionScanner().discover(root: root); final out = await const ExtensionScanner().discover(root: root);
expect(out.map((m) => m.id).toSet(), {'ext.a', 'ext.b'}); expect(out.map((m) => m.id).toSet(), {'ext.a', 'ext.b'});
}); });
+2 -4
View File
@@ -185,8 +185,7 @@ index abc..def 100644
}); });
test('returns unstaged diff after modification', () async { test('returns unstaged diff after modification', () async {
await File('${sandbox.path}/file.txt') await File('${sandbox.path}/file.txt').writeAsString('line1\nmodified\n');
.writeAsString('line1\nmodified\n');
final diffs = await gitDiff(sandbox); final diffs = await gitDiff(sandbox);
expect(diffs, hasLength(1)); expect(diffs, hasLength(1));
expect(diffs.first.path, 'file.txt'); expect(diffs.first.path, 'file.txt');
@@ -194,8 +193,7 @@ index abc..def 100644
}); });
test('returns staged diff with staged: true', () async { test('returns staged diff with staged: true', () async {
await File('${sandbox.path}/file.txt') await File('${sandbox.path}/file.txt').writeAsString('line1\nmodified\n');
.writeAsString('line1\nmodified\n');
await Process.run( await Process.run(
'git', 'git',
['add', 'file.txt'], ['add', 'file.txt'],
+2 -4
View File
@@ -4,12 +4,10 @@ import 'package:clide/kernel/kernel.dart';
/// A DaemonClient that doesn't actually open a socket. Use in tests /// A DaemonClient that doesn't actually open a socket. Use in tests
/// that need a connected-state observable but not a real daemon. /// that need a connected-state observable but not a real daemon.
class FakeDaemonClient extends DaemonClient { class FakeDaemonClient extends DaemonClient {
FakeDaemonClient({required super.log, required super.events}) FakeDaemonClient({required super.log, required super.events}) : super(socketPath: '/dev/null/fake-clide.sock');
: super(socketPath: '/dev/null/fake-clide.sock');
bool _fakeConnected = false; bool _fakeConnected = false;
final Map<String, Future<IpcResponse> Function(Map<String, Object?>)> _stubs = final Map<String, Future<IpcResponse> Function(Map<String, Object?>)> _stubs = {};
{};
@override @override
bool get isConnected => _fakeConnected; bool get isConnected => _fakeConnected;
+1 -2
View File
@@ -9,8 +9,7 @@ import 'fake_ipc.dart';
/// No real daemon, no real filesystem outside a temp dir, no asset /// No real daemon, no real filesystem outside a temp dir, no asset
/// bundle — i18n catalogs are passed as literals. /// bundle — i18n catalogs are passed as literals.
class KernelFixture { class KernelFixture {
KernelFixture._( KernelFixture._({required this.services, required this.ipc, required this.tempDir});
{required this.services, required this.ipc, required this.tempDir});
final KernelServices services; final KernelServices services;
final FakeDaemonClient ipc; final FakeDaemonClient ipc;
+1 -2
View File
@@ -117,8 +117,7 @@ void main() {
}); });
test('throws on malformed JSON', () { test('throws on malformed JSON', () {
expect(() => IpcMessage.decode('{this is not json'), expect(() => IpcMessage.decode('{this is not json'), throwsA(isA<FormatException>()));
throwsA(isA<FormatException>()));
}); });
}); });
} }
+1 -2
View File
@@ -3,8 +3,7 @@ import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart'; import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
CommandContribution _cmd(String name, Future<IpcResponse> Function() run) => CommandContribution _cmd(String name, Future<IpcResponse> Function() run) => CommandContribution(
CommandContribution(
id: name, id: name,
command: name, command: name,
title: 'cmd $name', title: 'cmd $name',
+2 -6
View File
@@ -172,12 +172,8 @@ void main() {
test('emits ExtensionActivated / ExtensionDeactivated events', () async { test('emits ExtensionActivated / ExtensionDeactivated events', () async {
final activated = <String>[]; final activated = <String>[];
final deactivated = <String>[]; final deactivated = <String>[];
final s1 = f.services.events final s1 = f.services.events.on<ExtensionActivated>().listen((e) => activated.add(e.id));
.on<ExtensionActivated>() final s2 = f.services.events.on<ExtensionDeactivated>().listen((e) => deactivated.add(e.id));
.listen((e) => activated.add(e.id));
final s2 = f.services.events
.on<ExtensionDeactivated>()
.listen((e) => deactivated.add(e.id));
f.services.extensions.register(_Ext(id: 'e')); f.services.extensions.register(_Ext(id: 'e'));
await f.services.extensions.activateAll(); await f.services.extensions.activateAll();
await Future<void>.delayed(Duration.zero); await Future<void>.delayed(Duration.zero);
+2 -4
View File
@@ -85,8 +85,7 @@ void main() {
); );
}); });
test('falls through to default-locale when current locale is empty', test('falls through to default-locale when current locale is empty', () async {
() async {
final i = build(catalogs: { final i = build(catalogs: {
'builtin.x': { 'builtin.x': {
const Locale('en', 'US'): { const Locale('en', 'US'): {
@@ -143,8 +142,7 @@ void main() {
expect(i.string('k', namespace: 'b', placeholder: '-'), 'B'); expect(i.string('k', namespace: 'b', placeholder: '-'), 'B');
}); });
test('setLocale refreshes cached namespaces and notifies listeners', test('setLocale refreshes cached namespaces and notifies listeners', () async {
() async {
final i = build(catalogs: { final i = build(catalogs: {
'x': { 'x': {
const Locale('en', 'US'): { const Locale('en', 'US'): {
+1 -4
View File
@@ -32,10 +32,7 @@ void main() {
test('honors initialName when present', () { test('honors initialName when present', () {
final c = ThemeController( final c = ThemeController(
bundled: [ bundled: [_def('a', const Color(0xFF000000)), _def('b', const Color(0xFF999999))],
_def('a', const Color(0xFF000000)),
_def('b', const Color(0xFF999999))
],
initialName: 'b', initialName: 'b',
); );
expect(c.currentName, 'b'); expect(c.currentName, 'b');
+1 -2
View File
@@ -35,8 +35,7 @@ semantic:
mainchrome: red mainchrome: red
focus: "#123456" focus: "#123456"
'''); ''');
expect( expect(def.semanticOverride!.lookup('mainchrome'), const Color(0xFFFF0000));
def.semanticOverride!.lookup('mainchrome'), const Color(0xFFFF0000));
expect(def.semanticOverride!.lookup('focus'), const Color(0xFF123456)); expect(def.semanticOverride!.lookup('focus'), const Color(0xFF123456));
}); });
+1 -2
View File
@@ -126,8 +126,7 @@ void main() {
'ext.sqlite.table.background': '#ABCDEF', 'ext.sqlite.table.background': '#ABCDEF',
}, },
); );
expect(tokens.extensionTokens['ext.sqlite.table.background'], expect(tokens.extensionTokens['ext.sqlite.table.background'], const Color(0xFFABCDEF));
const Color(0xFFABCDEF));
}); });
}); });
+4 -8
View File
@@ -24,8 +24,7 @@ void main() {
expect(find.text('Save'), findsOneWidget); expect(find.text('Save'), findsOneWidget);
}); });
testWidgets('emits a Semantics node with button: true + label', testWidgets('emits a Semantics node with button: true + label', (tester) async {
(tester) async {
await tester.pumpWidget( await tester.pumpWidget(
harness(f, ClideButton(label: 'Commit', onPressed: () {})), harness(f, ClideButton(label: 'Commit', onPressed: () {})),
); );
@@ -38,8 +37,7 @@ void main() {
); );
}); });
testWidgets('semanticLabel overrides the visible label for a11y', testWidgets('semanticLabel overrides the visible label for a11y', (tester) async {
(tester) async {
await tester.pumpWidget( await tester.pumpWidget(
harness( harness(
f, f,
@@ -54,8 +52,7 @@ void main() {
expect(semantics.label, 'Save document'); expect(semantics.label, 'Save document');
}); });
testWidgets('semanticHint propagates to the Semantics node', testWidgets('semanticHint propagates to the Semantics node', (tester) async {
(tester) async {
await tester.pumpWidget( await tester.pumpWidget(
harness( harness(
f, f,
@@ -75,8 +72,7 @@ void main() {
harness(f, const ClideButton(label: 'Nope', onPressed: null)), harness(f, const ClideButton(label: 'Nope', onPressed: null)),
); );
final semantics = tester.getSemantics(find.byType(ClideButton)); final semantics = tester.getSemantics(find.byType(ClideButton));
expect( expect(semantics.getSemanticsData().hasAction(SemanticsAction.tap), isFalse);
semantics.getSemanticsData().hasAction(SemanticsAction.tap), isFalse);
}); });
testWidgets('tap invokes onPressed', (tester) async { testWidgets('tap invokes onPressed', (tester) async {
+2 -4
View File
@@ -11,16 +11,14 @@ void main() {
setUp(() async => f = await KernelFixture.create()); setUp(() async => f = await KernelFixture.create());
tearDown(() async => f.dispose()); tearDown(() async => f.dispose());
testWidgets('renders a 1px horizontal container by default', testWidgets('renders a 1px horizontal container by default', (tester) async {
(tester) async {
await tester.pumpWidget(harness(f, const ClideDivider())); await tester.pumpWidget(harness(f, const ClideDivider()));
final c = tester.widget<Container>(find.byType(Container)); final c = tester.widget<Container>(find.byType(Container));
expect(c.constraints?.maxHeight, 1.0); expect(c.constraints?.maxHeight, 1.0);
expect(c.color, f.services.theme.current.surface.dividerColor); expect(c.color, f.services.theme.current.surface.dividerColor);
}); });
testWidgets('vertical axis yields a width-constrained container', testWidgets('vertical axis yields a width-constrained container', (tester) async {
(tester) async {
await tester.pumpWidget( await tester.pumpWidget(
harness(f, const ClideDivider(axis: Axis.vertical, thickness: 2)), harness(f, const ClideDivider(axis: Axis.vertical, thickness: 2)),
); );
+1 -2
View File
@@ -11,8 +11,7 @@ void main() {
setUp(() async => f = await KernelFixture.create()); setUp(() async => f = await KernelFixture.create());
tearDown(() async => f.dispose()); tearDown(() async => f.dispose());
testWidgets('sizes a SizedBox + CustomPaint to the given size', testWidgets('sizes a SizedBox + CustomPaint to the given size', (tester) async {
(tester) async {
await tester.pumpWidget( await tester.pumpWidget(
harness(f, const ClideIcon(FolderIcon(), size: 24)), harness(f, const ClideIcon(FolderIcon(), size: 24)),
); );

Some files were not shown because too many files have changed in this diff Show More