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

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