diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc new file mode 100644 index 0000000..e5ef89b --- /dev/null +++ b/.opencode/opencode.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "dart": { + "type": "local", + "command": ["dart", "mcp-server"] + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f1d4a62 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,115 @@ +# FLUTTER AGENT PANEL - PROJECT KNOWLEDGE BASE + +**Generated:** 2026-01-08 +**Commit:** b8ef0c5 +**Branch:** main + +## OVERVIEW +Cross-platform desktop terminal aggregator with AI agent integration. Built with Flutter + shadcn_ui, using BLoC/HydratedBloc for state persistence. + +## STRUCTURE +``` +flutter_agent_panel/ +├── lib/ +│ ├── main.dart # Entry: error handling, HydratedStorage init +│ ├── app.dart # Root widget: MultiBlocProvider, theming +│ ├── core/ # Cross-cutting: services, router, l10n, extensions +│ ├── features/ # Feature modules (BLoC pattern) +│ │ ├── home/ # App shell layout +│ │ ├── info/ # About/Help dialogs +│ │ ├── settings/ # App configuration (879-line dialog) +│ │ ├── terminal/ # PTY management, themes +│ │ └── workspace/ # Multi-workspace organization +│ └── shared/ # Utils, constants, common widgets +├── packages/ +│ ├── flutter_pty/ # FFI PTY bindings (ConPTY/POSIX) +│ └── xterm/ # Terminal emulator core (60fps render) +└── assets/ # Images, agent logos +``` + +## WHERE TO LOOK + +| Task | Location | Notes | +|------|----------|-------| +| Add new feature | `lib/features/{name}/` | Create bloc/, models/, views/, widgets/ | +| Modify theming | `lib/app.dart` | ShadThemeData, colorScheme switching | +| Add localization | `lib/core/l10n/` | ARB files, regenerate with `flutter gen-l10n` | +| Configure routing | `lib/core/router/app_router.dart` | AutoRoute, regenerate with `lean_builder` | +| Add service | `lib/core/services/` | Singleton pattern, init in main.dart | +| Terminal rendering | `packages/xterm/lib/src/ui/` | Custom RenderBox, pixel-aligned painting | +| PTY native code | `packages/flutter_pty/src/` | C files per platform | + +## CONVENTIONS + +### State Management +- **HydratedBloc** for persistent state (workspace, settings) +- **Regular Bloc** for ephemeral state (terminal instances) +- Event/State in `part` files: `{name}_event.dart`, `{name}_state.dart` +- Always use `.copyWith()` for immutable updates + +### UI Patterns +- **shadcn_ui** components: ShadDialog, ShadSelect, ShadInput, ShadTooltip +- **Context extensions**: `context.theme`, `context.t` (localization) +- **LucideIcons** throughout +- **Gap** widgets for spacing (not SizedBox) + +### Code Style +- Single quotes for strings +- Trailing commas required +- Relative imports within lib/ +- `@pragma('vm:prefer-inline')` on hot paths (xterm painter) + +## ANTI-PATTERNS (THIS PROJECT) + +| Forbidden | Reason | +|-----------|--------| +| Direct list mutation | Use `.copyWith()` or create new list | +| Heavy logic in `build()` | Move to Bloc or extract methods | +| Inline sorting | Delegate to Bloc events | +| Hardcoded strings | Use `context.t` localized strings | +| Manual mock edits | `*.mocks.dart` are generated | +| `flutter_pty_bindings_generated.dart` edits | Auto-generated by ffigen | +| Sub-pixel offsets in painter | Use `roundToDouble()` for crispness | +| Blocking PTY calls on main isolate | Use dedicated isolates | + +## COMMANDS +```bash +# Development +flutter pub get +dart run lean_builder build # Generate routes +flutter gen-l10n # Generate localizations +flutter run -d windows # Run on Windows + +# Packages (run from package dir) +flutter pub run ffigen --config ffigen.yaml # Regenerate PTY bindings + +# Build +flutter build windows +flutter build macos +flutter build linux +``` + +## NOTES + +### Desktop-Only +No Android/iOS directories - configured for Windows, macOS, Linux only. + +### Monorepo Structure +Uses Flutter workspace with local packages: +- `packages/xterm` - forked/customized terminal emulator +- `packages/flutter_pty` - native PTY bindings + +### Large File Hotspots +- `settings_dialog.dart` (879 lines) - Tab-based settings UI +- `workspace_drawer.dart` (541 lines) - Drag-drop with pin constraints +- `terminal_bloc.dart` (434 lines) - Cross-platform shell handling + +### Platform-Specific Shell Logic +Terminal creation handles: PowerShell, pwsh, cmd, WSL, bash, zsh. See `TerminalServiceImpl` and `TerminalBloc._createTerminalNode()`. + +### Inter-Feature Dependencies +``` +workspace → terminal (TerminalConfig model) +settings → terminal (font/theme models) +terminal → flutter_pty, xterm packages +``` diff --git a/lib/core/AGENTS.md b/lib/core/AGENTS.md new file mode 100644 index 0000000..1234c1f --- /dev/null +++ b/lib/core/AGENTS.md @@ -0,0 +1,73 @@ +# lib/core - AGENT GUIDE + +## OVERVIEW +Cross-cutting concerns: services, routing, localization, extensions. + +## STRUCTURE +``` +core/ +├── services/ +│ ├── terminal_service.dart # PTY startup abstraction +│ ├── app_logger.dart # Singleton Logger wrapper +│ ├── crash_log_service.dart # Error persistence to disk +│ ├── user_config_service.dart # Config directory paths +│ ├── app_version_service.dart # Version check + updates +│ └── app_bloc_observer.dart # BLoC event/transition logging +├── router/ +│ ├── app_router.dart # AutoRoute configuration +│ └── app_router.gr.dart # GENERATED - do not edit +├── l10n/ +│ ├── app_localizations.dart # GENERATED base class +│ ├── app_localizations_en.dart # GENERATED +│ └── app_localizations_zh.dart # GENERATED +├── extensions/ +│ └── context_extension.dart # context.theme, context.t +├── constants/ +│ └── assets.dart # Asset path constants +└── types/ + └── typedefs.dart # Callback type definitions +``` + +## WHERE TO LOOK + +| Task | File | Notes | +|------|------|-------| +| Add new service | `services/` | Singleton pattern, init in main.dart | +| Add route | `router/app_router.dart` | Run `dart run lean_builder build` | +| Add translation | `assets/l10n/*.arb` | Run `flutter gen-l10n` | +| Add extension | `extensions/context_extension.dart` | On BuildContext | + +## CONVENTIONS + +### Services +```dart +// Singleton pattern used throughout +class MyService { + MyService._(); + static final MyService instance = MyService._(); + + Future init() async { ... } +} +``` + +### Context Extensions +```dart +// Access anywhere in widget tree +context.theme // ShadThemeData +context.t // AppLocalizations +context.colorScheme +context.textTheme +``` + +### Generated Files (DO NOT EDIT) +- `app_router.gr.dart` - regenerate with `lean_builder` +- `app_localizations*.dart` - regenerate with `flutter gen-l10n` + +## ANTI-PATTERNS + +| Forbidden | Reason | +|-----------|--------| +| Edit `*.gr.dart` | Auto-generated by lean_builder | +| Edit `app_localizations*.dart` | Auto-generated by flutter gen-l10n | +| Service without singleton | Inconsistent state across app | +| Direct Logger() usage | Use AppLogger.instance.logger | diff --git a/lib/features/settings/AGENTS.md b/lib/features/settings/AGENTS.md new file mode 100644 index 0000000..640c027 --- /dev/null +++ b/lib/features/settings/AGENTS.md @@ -0,0 +1,66 @@ +# lib/features/settings - AGENT GUIDE + +## OVERVIEW +App configuration management: themes, fonts, shells, agents, localization. + +## STRUCTURE +``` +settings/ +├── bloc/ +│ ├── settings_bloc.dart # HydratedBloc (persistent) +│ ├── settings_event.dart # 15+ event types +│ └── settings_state.dart # AppSettings wrapper +├── models/ +│ ├── app_settings.dart # Main config model (JSON serializable) +│ ├── agent_config.dart # AI agent definitions +│ ├── custom_shell_config.dart +│ ├── terminal_font_settings.dart +│ ├── app_theme.dart # Light/Dark enum +│ └── shell_type.dart # PowerShell/Bash/WSL/Custom +├── views/ +│ └── settings_dialog.dart # 879-line tab dialog (COMPLEXITY HOTSPOT) +└── widgets/ + ├── agents_content.dart # Agent management + installation + ├── appearance_settings_content.dart + ├── general_settings_content.dart + ├── custom_shells_content.dart + ├── update_settings_content.dart + ├── agent_dialog.dart # Add/edit agent + ├── shell_dialog.dart # Add/edit custom shell + └── settings_section.dart # Reusable layout wrapper +``` + +## WHERE TO LOOK + +| Task | File | Notes | +|------|------|-------| +| Add new setting | `models/app_settings.dart` | Add field + copyWith + JSON | +| Add settings tab | `views/settings_dialog.dart` | `_buildContentForIndex()` switch | +| New agent preset | `models/app_settings.dart` | `getDefaultAgents()` static | +| Persist new field | `bloc/settings_bloc.dart` | Add event handler + fromJson/toJson | + +## CONVENTIONS + +- **Persistence**: HydratedBloc auto-saves to `storage/` directory +- **Clear nullable fields**: Use `clearX: true` pattern in copyWith +- **Tab content**: Each tab is a separate `*_content.dart` widget +- **Dialogs**: ShadDialog with ShadInput/ShadSelect components +- **Validation**: Inline in dialog widgets before emitting events + +## ANTI-PATTERNS + +| Forbidden | Do Instead | +|-----------|------------| +| Add logic to settings_dialog.dart | Create new widget in widgets/ | +| Direct AppSettings mutation | Emit SettingsEvent through Bloc | +| Hardcode agent commands | Use AgentConfig model with env vars | + +## COMPLEXITY NOTES + +`settings_dialog.dart` is 879 lines due to 6 tab sections. Each section handles: +- Async font/theme loading +- File picker for custom themes +- Agent installation with toast feedback +- JSON validation for custom terminal themes + +Consider splitting if adding more tabs. diff --git a/lib/features/terminal/AGENTS.md b/lib/features/terminal/AGENTS.md new file mode 100644 index 0000000..7ee18dc --- /dev/null +++ b/lib/features/terminal/AGENTS.md @@ -0,0 +1,72 @@ +# lib/features/terminal - AGENT GUIDE + +## OVERVIEW +PTY lifecycle management with cross-platform shell support and terminal theming. + +## STRUCTURE +``` +terminal/ +├── bloc/ +│ ├── terminal_bloc.dart # PTY creation, shell resolution (434 lines) +│ ├── terminal_event.dart # Create/Kill/Restart/Resize events +│ └── terminal_state.dart # TerminalNode map management +├── models/ +│ ├── terminal_node.dart # PTY + Terminal + Controller bundle +│ ├── terminal_config.dart # Shell type, working dir, env vars +│ ├── terminal_theme_data.dart +│ └── built_in_themes.dart # 20+ predefined color schemes +├── services/ +│ ├── isolate_pty.dart # Background PTY I/O handling +│ └── terminal_theme_service.dart # Theme JSON parsing +├── views/ +│ ├── terminal_view.dart # BlocBuilder + TerminalComponent +│ └── terminal_component.dart # xterm TerminalView wrapper +└── widgets/ + ├── terminal_search_bar.dart # Regex search with navigation + ├── activity_indicator.dart # Output activity pulse + └── glowing_icon.dart # Agent status indicator +``` + +## WHERE TO LOOK + +| Task | File | Notes | +|------|------|-------| +| Add shell type | `terminal_bloc.dart` | `_createTerminalNode()` switch | +| Custom theme | `terminal_theme_service.dart` | JSON parsing logic | +| PTY resize | `terminal_bloc.dart` | `_onResizeTerminal()` | +| Search feature | `widgets/terminal_search_bar.dart` | TerminalSearchController | + +## CONVENTIONS + +- **TerminalNode**: Bundles Pty + Terminal + TerminalController +- **Shell resolution**: Try pwsh → powershell → bash fallback +- **Environment**: Always set `TERM=xterm-256color`, `LANG=en_US.UTF-8` +- **Isolates**: PTY output streams on dedicated isolates + +## PLATFORM-SPECIFIC LOGIC + +```dart +// Windows shells +'powershell.exe' → ['-NoLogo', '-ExecutionPolicy', 'Bypass'] +'pwsh.exe' → ['-NoLogo', '-ExecutionPolicy', 'Bypass'] +'cmd.exe' → [] +'wsl.exe' → ['--', 'bash', '-l'] + +// Unix shells +'/bin/bash' → ['-l'] +'/bin/zsh' → ['-l'] +``` + +## ANTI-PATTERNS + +| Forbidden | Reason | +|-----------|--------| +| PTY calls on main isolate | UI jank - use IsolatePty | +| Direct Terminal state access | Use TerminalController | +| Hardcoded theme colors | Use TerminalThemeData model | + +## INTER-FEATURE DEPENDENCIES + +- **Imports from settings**: `TerminalFontSettings`, `AppSettings.terminalThemeName` +- **Used by workspace**: `TerminalConfig` stored in Workspace model +- **Uses packages**: `flutter_pty` (PTY), `xterm` (rendering) diff --git a/lib/features/terminal/bloc/terminal_bloc.dart b/lib/features/terminal/bloc/terminal_bloc.dart index f5decc7..b1f4f16 100644 --- a/lib/features/terminal/bloc/terminal_bloc.dart +++ b/lib/features/terminal/bloc/terminal_bloc.dart @@ -420,6 +420,12 @@ class TerminalBloc extends Bloc { pty.write(const Utf8Encoder().convert(data)); }; + // Setup Terminal -> PTY (Resize) + // This is called by xterm's RenderTerminal when autoResize is enabled + terminal.onResize = (width, height, pixelWidth, pixelHeight) { + node.resize(width, height); + }; + return node; } diff --git a/lib/features/terminal/views/terminal_component.dart b/lib/features/terminal/views/terminal_component.dart index 6d16851..c9df9a8 100644 --- a/lib/features/terminal/views/terminal_component.dart +++ b/lib/features/terminal/views/terminal_component.dart @@ -31,14 +31,13 @@ class _TerminalComponentState extends State { late final xterm_ui.TerminalController _terminalController; xterm_ui.TerminalSearchController? _searchController; - int? _lastCols; - int? _lastRows; xterm.TerminalTheme? _cachedTheme; String? _lastThemeName; String? _lastCustomJson; Brightness? _lastBrightness; bool _showSearchBar = false; + bool _themeInitialized = false; @override void initState() { @@ -56,6 +55,22 @@ class _TerminalComponentState extends State { } } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Pre-load theme on first build to avoid flash of default theme + if (!_themeInitialized) { + _themeInitialized = true; + final settings = context.read().state.settings; + final brightness = Theme.of(context).brightness; + _loadTheme( + settings.terminalThemeName, + settings.customTerminalThemeJson, + brightness, + ); + } + } + @override void dispose() { _searchController?.dispose(); @@ -125,13 +140,14 @@ class _TerminalComponentState extends State { final settings = state.settings; final fontSettings = settings.fontSettings; + // Use height: 1.2 (default) to provide space for descenders (y, p, g, etc.) + // height: 1.0 causes descenders to be clipped by the next line final terminalStyle = xterm.TerminalStyle( fontFamily: fontSettings.fontFamily, fontSize: fontSettings.fontSize, fontWeight: fontSettings.isBold ? FontWeight.bold : FontWeight.normal, fontStyle: fontSettings.isItalic ? FontStyle.italic : FontStyle.normal, - height: 1.0, ); // Check if we need to reload the theme @@ -160,22 +176,8 @@ class _TerminalComponentState extends State { .setCursorBlinkMode(settings.terminalCursorBlink); } - if (widget.interactive) { - Future.delayed(const Duration(milliseconds: 200), () { - if (mounted) { - final terminal = widget.terminalNode.terminal; - if (terminal.viewWidth > 0 && terminal.viewHeight > 0) { - if (_lastCols != terminal.viewWidth || - _lastRows != terminal.viewHeight) { - _lastCols = terminal.viewWidth; - _lastRows = terminal.viewHeight; - widget.terminalNode - .resize(terminal.viewWidth, terminal.viewHeight); - } - } - } - }); - } + // PTY resize is now handled automatically via terminal.onResize callback + // which is set in TerminalBloc._createTerminalNode() // Non-interactive (thumbnail) mode - minimal rendering if (!widget.interactive) { @@ -194,22 +196,19 @@ class _TerminalComponentState extends State { onKeyEvent: _handleKeyEvent, child: Stack( children: [ - Container( - color: xtermTheme.background, - padding: const EdgeInsets.all(5), - child: GestureDetector( - onTap: () => _focusNode.requestFocus(), - child: xterm.TerminalView( - widget.terminalNode.terminal, - controller: _terminalController, - autofocus: true, - autoResize: true, - focusNode: _focusNode, - hardwareKeyboardOnly: false, - keyboardType: TextInputType.text, - textStyle: terminalStyle, - theme: xtermTheme, - ), + GestureDetector( + onTap: () => _focusNode.requestFocus(), + child: xterm.TerminalView( + widget.terminalNode.terminal, + controller: _terminalController, + autofocus: true, + autoResize: true, + focusNode: _focusNode, + hardwareKeyboardOnly: false, + keyboardType: TextInputType.text, + textStyle: terminalStyle, + theme: xtermTheme, + padding: const EdgeInsets.all(5), ), ), // Search bar overlay diff --git a/lib/features/workspace/AGENTS.md b/lib/features/workspace/AGENTS.md new file mode 100644 index 0000000..e4212ab --- /dev/null +++ b/lib/features/workspace/AGENTS.md @@ -0,0 +1,49 @@ +# lib/features/workspace - AGENT GUIDE + +## OVERVIEW +Handles workspace management and terminal persistence. + +## STRUCTURE +``` +workspace +├── bloc/ # Workspace Bloc (state management) +│ ├── workspace_bloc.dart +│ ├── workspace_event.dart +│ └── workspace_state.dart +├── models/ # Workspace data models +│ └── workspace.dart +├── views/ # Screens and layouts +│ ├── workspace_view.dart +│ ├── workspace_wrapper_view.dart +│ └── workspace_drawer.dart +└── widgets/ # Modular UI components + ├── main_terminal_content.dart + ├── workspace_search_field.dart + ├── thumbnail_bar.dart + ├── add_workspace_dialog.dart + ├── terminal_top_bar.dart + ├── workspace_context_menu.dart + └── workspace_tag_chips.dart +``` + +## WHERE TO LOOK +- **State Management**: `bloc/workspace_bloc.dart` + - Manages all workspace events (adding, reordering, persistence). +- **Terminal UI**: `widgets/main_terminal_content.dart` + - Displays active terminal or restarting/loading views. + +## CONVENTIONS +- **Persistence**: Use `HydratedBloc` for workspace storage. + - JSON serialization via `WorkspaceState.toJson` and `WorkspaceState.fromJson`. +- **Reordering**: Drag-and-drop updates for terminals in workspace. + - Terminals: `workspace_bloc.dart -> _onReorderTerminals` + - Workspaces: `workspace_bloc.dart -> _onReorderWorkspaces` +- **Context Extensions**: + - Theme via `context.theme`. + - Localizations via `context.t`. + +## ANTI-PATTERNS +- **Direct State Mutation**: Avoid mutating lists directly. Always update with `.copyWith`. +- **Complex Build Logic**: No heavy computations in `StatelessWidget.build`. Use Bloc. +- **Manual Sorting**: Never sort lists inline. Delegate to events. +- **Hardcoded Strings**: Always use `context.t` localized strings. diff --git a/packages/flutter_pty/AGENTS.md b/packages/flutter_pty/AGENTS.md new file mode 100644 index 0000000..d52d8dc --- /dev/null +++ b/packages/flutter_pty/AGENTS.md @@ -0,0 +1,37 @@ +# AGENTS.md + +**Generated:** 2026-01-08 +**Version:** 0.0.7+1 + +## OVERVIEW +Dart FFI bindings and isolate-based pseudo-terminal (PTY) management. + +## STRUCTURE +``` +packages/flutter_pty/ +├── lib/ +│ ├── flutter_pty.dart # Entry point +│ └── src/ +│ ├── flutter_pty_bindings_generated.dart # FFI bindings (auto-generated) +│ └── template.dart # Helper classes +├── src/ +│ ├── include/ # C headers for bindings +│ ├── flutter_pty_unix.c # PTY logic for Unix-like systems +│ ├── flutter_pty_win.c # PTY logic for Windows +│ └── forkpty.c # Unix PTY spawn function +``` + +## WHERE TO LOOK +- **FFI Bindings**: `lib/src/flutter_pty_bindings_generated.dart` +- **Isolate Management**: `lib/src/isolate_pty.dart` +- **Native Logic**: `src/flutter_pty_win.c` (Windows ConPTY) / `src/flutter_pty_unix.c` (POSIX) + +## CONVENTIONS +- **FFI Safety**: Validate pointers before access. +- **Isolate Usage**: Use dedicated isolates for PTY I/O to prevent UI blocking. +- **Regeneration**: Use `ffigen` with `ffigen.yaml` for binding updates. + +## ANTI-PATTERNS +- **Manual Edits**: NEVER modify `flutter_pty_bindings_generated.dart` directly. +- **Blocking**: NEVER execute long-running native calls on the main isolate. +- **Mixed OS Logic**: Keep platform-specific C code in respective source files. diff --git a/packages/xterm/AGENTS.md b/packages/xterm/AGENTS.md new file mode 100644 index 0000000..7863555 --- /dev/null +++ b/packages/xterm/AGENTS.md @@ -0,0 +1,39 @@ +# AGENTS.md + +## OVERVIEW +High-performance terminal emulator core supporting complex buffer management and escape sequence parsing. + +## STRUCTURE +``` +lib/src/ +├── buffer/ # Terminal buffer and scrollback +│ ├── buffer.dart # Main buffer implementation +│ └── line.dart # Efficient cell storage (Uint32List) +├── core/ # Emulation logic +│ ├── escape/ # ANSI/VT sequence parser (CSI, OSC) +│ ├── input/ # Keyboard mapping and keytabs +│ └── mouse/ # Mouse protocol support +└── ui/ # Flutter rendering layer + ├── render.dart # Custom RenderBox for terminal + └── painter.dart # Canvas painting logic (pixel-aligned) +``` + +## WHERE TO LOOK +- **Render Engine**: `lib/src/ui/render.dart` (Layout & DPI) +- **Painting logic**: `lib/src/ui/painter.dart` (Cell rendering) +- **Escape Parser**: `lib/src/core/escape/parser.dart` (ANSI parsing) +- **Buffer logic**: `lib/src/core/buffer/buffer.dart` (Resize/Scroll) + +## CONVENTIONS +- **Storage**: Use `Uint32List` in `BufferLine` for compact memory and speed. +- **Attributes**: Colors and styles packed into 32-bit integers. +- **Private members**: Extensive use of `_` for internal state; exposed via getters. +- **Inlining**: `@pragma('vm:prefer-inline')` on critical hot paths (painter). + +## ANTI-PATTERNS +- **Manual Mocks**: NEVER edit `*.mocks.dart` files manually. +- **Sub-pixel coordinates**: Avoid arbitrary sub-pixel offsets in `Painter`. Use `floorToDouble()` for Y-coordinates to ensure pixel-aligned rows without gaps (at the cost of slight positioning differences versus rounding). + +## NOTES +- **Performance**: Designed for 60fps; avoid tree rebuilds, use `isRepaintBoundary = true`. +- **DPI**: Sensitive to `devicePixelRatio`. Use `TextScaler` for accurate cell measurement. diff --git a/packages/xterm/lib/src/ui/painter.dart b/packages/xterm/lib/src/ui/painter.dart index 64a78e0..f42bdf4 100644 --- a/packages/xterm/lib/src/ui/painter.dart +++ b/packages/xterm/lib/src/ui/painter.dart @@ -66,9 +66,24 @@ class TerminalPainter { final paragraph = builder.build(); paragraph.layout(ParagraphConstraints(width: double.infinity)); + // Cell size calculation strategy: + // - Width: floorToDouble() prevents horizontal stretching in ASCII art. + // Monospaced fonts in terminals are expected to align perfectly, and + // rounding up would cause cumulative errors that misalign box-drawing + // characters and ASCII art across wide terminal windows. + // - Height: ceilToDouble() prevents vertical gaps between lines. + // Rounding up ensures that consecutive lines overlap slightly rather + // than leaving sub-pixel gaps, which is less visually jarring than + // misaligned horizontal content. + // This asymmetry is intentional: horizontal precision is critical for + // terminal content alignment, while vertical gaps are more noticeable + // than slight height increases. + final rawWidth = paragraph.maxIntrinsicWidth / test.length; + final rawHeight = paragraph.height; + final result = Size( - paragraph.maxIntrinsicWidth / test.length, - paragraph.height, + rawWidth.floorToDouble(), + rawHeight.ceilToDouble(), ); paragraph.dispose(); @@ -227,15 +242,20 @@ class TerminalPainter { if (cellData.flags & CellFlags.inverse != 0) { color = resolveForegroundColor(cellData.foreground); } else if (colorType == CellColor.normal) { - return; + // Always paint background for normal cells to prevent gaps. + // Previously this returned early, leaving gaps between lines. + color = _theme.background; } else { color = resolveBackgroundColor(cellData.background); } - final paint = Paint()..color = color; + final paint = Paint() + ..color = color + ..isAntiAlias = false; final doubleWidth = cellData.content >> CellContent.widthShift == 2; final widthScale = doubleWidth ? 2 : 1; - final size = Size(_cellSize.width * widthScale + 1, _cellSize.height); + // Use exact cell size - extra height is already added in _measureCharSize + final size = Size(_cellSize.width * widthScale, _cellSize.height); canvas.drawRect(offset & size, paint); } diff --git a/packages/xterm/lib/src/ui/render.dart b/packages/xterm/lib/src/ui/render.dart index 1c70881..5489e64 100644 --- a/packages/xterm/lib/src/ui/render.dart +++ b/packages/xterm/lib/src/ui/render.dart @@ -172,6 +172,7 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin { void _onTerminalChange() { markNeedsLayout(); + markNeedsPaint(); _notifyEditableRect(); } @@ -452,8 +453,17 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin { void _paint(PaintingContext context, Offset offset) { final canvas = context.canvas; final lines = _terminal.buffer.lines; + // Use the integer cell height for consistent line positioning final charHeight = _painter.cellSize.height; + // Paint background for the entire visible terminal area to prevent + // gaps between lines on first frame before TUI apps fill the buffer. + // Disable anti-aliasing to prevent edge artifacts. + final bgPaint = Paint() + ..color = _painter.theme.background + ..isAntiAlias = false; + canvas.drawRect(offset & size, bgPaint); + final firstLineOffset = _scrollOffset - _padding.top; final lastLineOffset = _scrollOffset + size.height - _padding.bottom; @@ -479,10 +489,14 @@ class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ); } + // Calculate base Y offset once, then use integer multiples of charHeight + // to ensure consecutive lines have no gaps between them. + final baseY = _lineOffset.floorToDouble(); for (var i = effectFirstLine; i <= effectLastLine; i++) { + final lineY = (baseY + i * charHeight).floorToDouble(); _painter.paintLine( canvas, - offset.translate(0, (i * charHeight + _lineOffset).truncateToDouble()), + offset.translate(0, lineY), lines[i], ); }