diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 6c25a01..ed2d63f 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -66,7 +66,7 @@ jobs: run: flutter pub get - name: Generate code - run: dart run lean_builder build --delete-conflicting-outputs + run: dart run lean_builder build - name: Analyze run: flutter analyze --no-fatal-infos @@ -98,7 +98,7 @@ jobs: run: flutter pub get - name: Generate code - run: dart run lean_builder build --delete-conflicting-outputs + run: dart run lean_builder build - name: Analyze run: flutter analyze --no-fatal-infos @@ -135,7 +135,7 @@ jobs: run: flutter pub get - name: Generate code - run: dart run lean_builder build --delete-conflicting-outputs + run: dart run lean_builder build - name: Analyze run: flutter analyze --no-fatal-infos diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0955b6..e49ccbb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,8 +55,7 @@ jobs: | Platform | Asset | Description | | :--- | :--- | :--- | - | **Windows (Setup)** | [EXE](https://github.com/${{ github.repository }}/releases/download/${{ github.event_name == 'workflow_dispatch' && format('v{0}', steps.get_version.outputs.version) || github.ref_name }}/flutter_agent_panel-${{ steps.get_version.outputs.version }}-windows-x86_64-setup.exe) | Standard Windows Installer | - | **Windows (MSIX)** | [MSIX](https://github.com/${{ github.repository }}/releases/download/${{ github.event_name == 'workflow_dispatch' && format('v{0}', steps.get_version.outputs.version) || github.ref_name }}/flutter_agent_panel-${{ steps.get_version.outputs.version }}-windows-x86_64.msix) | Windows Store / Modern Package | + | **Windows** | [EXE](https://github.com/${{ github.repository }}/releases/download/${{ github.event_name == 'workflow_dispatch' && format('v{0}', steps.get_version.outputs.version) || github.ref_name }}/flutter_agent_panel-${{ steps.get_version.outputs.version }}-windows-x86_64-setup.exe) | Standard Windows Installer | | **macOS** | [DMG](https://github.com/${{ github.repository }}/releases/download/${{ github.event_name == 'workflow_dispatch' && format('v{0}', steps.get_version.outputs.version) || github.ref_name }}/flutter_agent_panel-${{ steps.get_version.outputs.version }}-macos-universal.dmg) | Universal Apple Disk Image | | **Linux** | [Tarball](https://github.com/${{ github.repository }}/releases/download/${{ github.event_name == 'workflow_dispatch' && format('v{0}', steps.get_version.outputs.version) || github.ref_name }}/flutter_agent_panel-${{ steps.get_version.outputs.version }}-linux-x86_64.tar.gz) | Gzipped Bundle | @@ -111,14 +110,10 @@ jobs: with: path: windows/installer.iss - - name: Create MSIX - run: dart run msix:create --build-windows false - - name: Rename output files with version run: | $version = "${{ needs.create-release.outputs.version }}" Rename-Item -Path "build/windows/x64/runner/Release/Output/flutter_agent_panel_setup.exe" -NewName "flutter_agent_panel-$version-windows-x86_64-setup.exe" - Rename-Item -Path "build/windows/x64/runner/Release/flutter_agent_panel.msix" -NewName "flutter_agent_panel-$version-windows-x86_64.msix" - name: Upload Release Asset (EXE) uses: softprops/action-gh-release@v2 @@ -126,12 +121,6 @@ jobs: tag_name: ${{ github.event_name == 'workflow_dispatch' && format('v{0}', needs.create-release.outputs.version) || github.ref_name }} files: build/windows/x64/runner/Release/Output/flutter_agent_panel-${{ needs.create-release.outputs.version }}-windows-x86_64-setup.exe - - name: Upload Release Asset (MSIX) - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.event_name == 'workflow_dispatch' && format('v{0}', needs.create-release.outputs.version) || github.ref_name }} - files: build/windows/x64/runner/Release/flutter_agent_panel-${{ needs.create-release.outputs.version }}-windows-x86_64.msix - build-macos: name: Build macOS needs: create-release @@ -162,7 +151,7 @@ jobs: run: flutter pub get - name: Generate code - run: dart run lean_builder build --delete-conflicting-outputs + run: dart run lean_builder build - name: Analyze run: flutter analyze --no-fatal-infos @@ -230,7 +219,7 @@ jobs: run: flutter pub get - name: Generate code - run: dart run lean_builder build --delete-conflicting-outputs + run: dart run lean_builder build - name: Analyze run: flutter analyze --no-fatal-infos diff --git a/analysis_options.yaml b/analysis_options.yaml index 7cccfb4..97fcee9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,6 +2,8 @@ analyzer: exclude: - packages/flutter_pty/** - packages/xterm/** + - lib/core/l10n/**.dart + - lib/core/router/**.gr.dart include: package:flutter_lints/flutter.yaml diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 3e153ae..3da7810 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -19,6 +19,8 @@ "cmd": "Command Prompt", "wsl": "WSL", "gitBash": "Git Bash", + "zsh": "Zsh", + "bash": "Bash", "appearance": "Appearance", "theme": "Theme", "appFontFamily": "App Font Family", diff --git a/assets/l10n/app_zh.arb b/assets/l10n/app_zh.arb index 342ef79..1dfa5f2 100644 --- a/assets/l10n/app_zh.arb +++ b/assets/l10n/app_zh.arb @@ -35,6 +35,8 @@ "cmd": "命令提示字元", "wsl": "WSL", "gitBash": "Git Bash", + "zsh": "Zsh", + "bash": "Bash", "appearance": "外觀", "theme": "主題", "appFontFamily": "App 字型系列", diff --git a/assets/l10n/app_zh_CN.arb b/assets/l10n/app_zh_CN.arb index b722242..94ef89d 100644 --- a/assets/l10n/app_zh_CN.arb +++ b/assets/l10n/app_zh_CN.arb @@ -19,6 +19,8 @@ "cmd": "命令提示符", "wsl": "WSL", "gitBash": "Git Bash", + "zsh": "Zsh", + "bash": "Bash", "appearance": "外观", "theme": "主题", "appFontFamily": "App 字体系列", diff --git a/lib/core/l10n/app_localizations.dart b/lib/core/l10n/app_localizations.dart index a4531f9..4d3fae8 100644 --- a/lib/core/l10n/app_localizations.dart +++ b/lib/core/l10n/app_localizations.dart @@ -201,6 +201,18 @@ abstract class AppLocalizations { /// **'Git Bash'** String get gitBash; + /// No description provided for @zsh. + /// + /// In en, this message translates to: + /// **'Zsh'** + String get zsh; + + /// No description provided for @bash. + /// + /// In en, this message translates to: + /// **'Bash'** + String get bash; + /// No description provided for @appearance. /// /// In en, this message translates to: diff --git a/lib/core/l10n/app_localizations_en.dart b/lib/core/l10n/app_localizations_en.dart index 12a99f2..039c7da 100644 --- a/lib/core/l10n/app_localizations_en.dart +++ b/lib/core/l10n/app_localizations_en.dart @@ -59,6 +59,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get gitBash => 'Git Bash'; + @override + String get zsh => 'Zsh'; + + @override + String get bash => 'Bash'; + @override String get appearance => 'Appearance'; diff --git a/lib/core/l10n/app_localizations_zh.dart b/lib/core/l10n/app_localizations_zh.dart index 8245e2c..f7ffef9 100644 --- a/lib/core/l10n/app_localizations_zh.dart +++ b/lib/core/l10n/app_localizations_zh.dart @@ -59,6 +59,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get gitBash => 'Git Bash'; + @override + String get zsh => 'Zsh'; + + @override + String get bash => 'Bash'; + @override String get appearance => '外觀'; @@ -468,6 +474,12 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get gitBash => 'Git Bash'; + @override + String get zsh => 'Zsh'; + + @override + String get bash => 'Bash'; + @override String get appearance => '外观'; diff --git a/lib/core/router/app_router.gr.dart b/lib/core/router/app_router.gr.dart index d652daa..43a8b0c 100644 --- a/lib/core/router/app_router.gr.dart +++ b/lib/core/router/app_router.gr.dart @@ -14,7 +14,7 @@ part of 'app_router.dart'; /// [AppShellView] class AppShellRoute extends PageRouteInfo { const AppShellRoute({List? children}) - : super(AppShellRoute.name, initialChildren: children); + : super(AppShellRoute.name, initialChildren: children); static const String name = 'AppShellRoute'; @@ -34,11 +34,11 @@ class TerminalRoute extends PageRouteInfo { required String terminalId, List? children, }) : super( - TerminalRoute.name, - args: TerminalRouteArgs(key: key, terminalId: terminalId), - rawPathParams: {'terminalId': terminalId}, - initialChildren: children, - ); + TerminalRoute.name, + args: TerminalRouteArgs(key: key, terminalId: terminalId), + rawPathParams: {'terminalId': terminalId}, + initialChildren: children, + ); static const String name = 'TerminalRoute'; @@ -86,11 +86,11 @@ class WorkspaceRoute extends PageRouteInfo { required String workspaceId, List? children, }) : super( - WorkspaceRoute.name, - args: WorkspaceRouteArgs(key: key, workspaceId: workspaceId), - rawPathParams: {'workspaceId': workspaceId}, - initialChildren: children, - ); + WorkspaceRoute.name, + args: WorkspaceRouteArgs(key: key, workspaceId: workspaceId), + rawPathParams: {'workspaceId': workspaceId}, + initialChildren: children, + ); static const String name = 'WorkspaceRoute'; @@ -135,7 +135,7 @@ class WorkspaceRouteArgs { /// [WorkspaceWrapperView] class WorkspaceWrapperRoute extends PageRouteInfo { const WorkspaceWrapperRoute({List? children}) - : super(WorkspaceWrapperRoute.name, initialChildren: children); + : super(WorkspaceWrapperRoute.name, initialChildren: children); static const String name = 'WorkspaceWrapperRoute'; diff --git a/lib/core/services/app_version_service.dart b/lib/core/services/app_version_service.dart index c5a98c7..4027c20 100644 --- a/lib/core/services/app_version_service.dart +++ b/lib/core/services/app_version_service.dart @@ -16,6 +16,7 @@ class AppVersionService { static final AppVersionService instance = AppVersionService._(); PackageInfo? _packageInfo; + Map? _latestReleaseJson; /// Gets the package info (cached after first call). Future getPackageInfo() async { @@ -61,6 +62,7 @@ class AppVersionService { if (response.statusCode == 200) { final body = await response.transform(utf8.decoder).join(); final json = jsonDecode(body) as Map; + _latestReleaseJson = json; final tagName = json['tag_name'] as String?; if (tagName != null) { // Remove 'Release v' or 'v' prefix if present @@ -157,7 +159,37 @@ class AppVersionService { /// Gets the download URL for the binary based on current platform. Future getBinaryUrl(String version) async { - // Clean version string to ensure it matches GitHub release tag format + // If we have cached release info, try to find the best asset + // Verify that the cached release tag matches the requested version + final cachedTag = _latestReleaseJson?['tag_name'] as String?; + final cleanCachedTag = + cachedTag?.replaceFirst('Release v', '').replaceFirst('v', '').trim(); + + if (_latestReleaseJson != null && cleanCachedTag == version) { + final assets = _latestReleaseJson!['assets'] as List?; + if (assets != null) { + final assetUrls = + assets.map((a) => a['browser_download_url'] as String).toList(); + + switch (Platform.operatingSystem) { + case 'windows': + final setupExe = assetUrls + .where((url) => url.endsWith('-setup.exe')) + .firstOrNull; + if (setupExe != null) return setupExe; + case 'macos': + final dmg = + assetUrls.where((url) => url.endsWith('.dmg')).firstOrNull; + if (dmg != null) return dmg; + case 'linux': + final tarGz = + assetUrls.where((url) => url.endsWith('.tar.gz')).firstOrNull; + if (tarGz != null) return tarGz; + } + } + } + + // Fallback if no assets found or no cache final cleanVersion = version.split('+')[0].split('-')[0]; final baseUrl = 'https://github.com/$_githubOwner/$_githubRepo/releases/download/v$cleanVersion'; @@ -170,7 +202,6 @@ class AppVersionService { return '$baseUrl/flutter_agent_panel-$cleanVersion-linux-x86_64.tar.gz'; } - // Default to Windows return '$baseUrl/flutter_agent_panel-$cleanVersion-windows-x86_64-setup.exe'; } diff --git a/lib/features/settings/bloc/settings_bloc.dart b/lib/features/settings/bloc/settings_bloc.dart index e8e85a3..266980f 100644 --- a/lib/features/settings/bloc/settings_bloc.dart +++ b/lib/features/settings/bloc/settings_bloc.dart @@ -10,7 +10,7 @@ class SettingsBloc extends HydratedBloc { SettingsBloc() : super( SettingsState( - settings: const AppSettings().copyWith( + settings: AppSettings().copyWith( agents: AppSettings.getDefaultAgents(), ), ), diff --git a/lib/features/settings/bloc/settings_state.dart b/lib/features/settings/bloc/settings_state.dart index 0ccecb3..c0b3521 100644 --- a/lib/features/settings/bloc/settings_state.dart +++ b/lib/features/settings/bloc/settings_state.dart @@ -1,11 +1,11 @@ part of 'settings_bloc.dart'; class SettingsState extends Equatable { - const SettingsState({ - this.settings = const AppSettings(), + SettingsState({ + AppSettings? settings, this.isLoading = false, this.error, - }); + }) : settings = settings ?? AppSettings(); final AppSettings settings; final bool isLoading; final String? error; diff --git a/lib/features/settings/models/app_settings.dart b/lib/features/settings/models/app_settings.dart index 7d1b07c..1b61036 100644 --- a/lib/features/settings/models/app_settings.dart +++ b/lib/features/settings/models/app_settings.dart @@ -29,12 +29,12 @@ String? _migrateTerminalThemeName(String? oldName) { /// Application settings model class AppSettings extends Equatable { - const AppSettings({ + AppSettings({ this.appTheme = AppTheme.dark, this.terminalThemeName = 'OneDark', this.customTerminalThemeJson, this.fontSettings = const TerminalFontSettings(), - this.defaultShell = ShellType.pwsh7, + ShellType? defaultShell, this.customShells = const [], this.selectedCustomShellId, this.locale = 'en', @@ -42,7 +42,7 @@ class AppSettings extends Equatable { this.agents = const [], this.appFontFamily, this.globalEnvironmentVariables = const {}, - }); + }) : defaultShell = defaultShell ?? ShellType.platformDefault; factory AppSettings.fromJson(Map json) { // Handle migration from old customShellPath to new customShells list @@ -76,7 +76,7 @@ class AppSettings extends Equatable { : const TerminalFontSettings(), defaultShell: ShellType.values.firstWhere( (e) => e.name == json['defaultShell'], - orElse: () => ShellType.pwsh7, + orElse: () => ShellType.platformDefault, ), customShells: customShells, selectedCustomShellId: json['selectedCustomShellId'] as String?, diff --git a/lib/features/settings/models/shell_type.dart b/lib/features/settings/models/shell_type.dart index 2848b43..8a03100 100644 --- a/lib/features/settings/models/shell_type.dart +++ b/lib/features/settings/models/shell_type.dart @@ -1,10 +1,19 @@ +import 'dart:io' show Platform, Process; + /// Shell types available for terminal creation enum ShellType { + // Unix shells (macOS/Linux) + zsh('Zsh', 'zsh', 'terminal'), + bash('Bash', 'bash', 'terminal'), + + // Windows shells pwsh7('PowerShell 7', 'pwsh', 'terminal'), powershell('Windows PowerShell', 'powershell', 'terminal'), cmd('Command Prompt', 'cmd', 'command'), wsl('WSL', 'wsl', 'server'), gitBash('Git Bash', 'C:\\Program Files\\Git\\bin\\bash.exe', 'gitBranch'), + + // Platform-agnostic custom('Custom...', '', 'settings'); const ShellType(this.displayName, this.command, this.icon); @@ -12,4 +21,93 @@ enum ShellType { final String displayName; final String command; final String icon; + + /// Returns whether this shell is available on the current platform. + bool get isAvailableOnCurrentPlatform { + if (Platform.isWindows) { + // Windows: pwsh7, powershell, cmd, wsl, gitBash, custom + return this == ShellType.pwsh7 || + this == ShellType.powershell || + this == ShellType.cmd || + this == ShellType.wsl || + this == ShellType.gitBash || + this == ShellType.custom; + } else if (Platform.isMacOS || Platform.isLinux) { + // macOS/Linux: zsh, bash, custom + return this == ShellType.zsh || + this == ShellType.bash || + this == ShellType.custom; + } + // Fallback: show all except platform-specific ones + return this == ShellType.custom; + } + + /// Returns the default shell for the current platform. + /// On Windows, provides fallback: pwsh7 > powershell > cmd + static Future getPlatformDefault() async { + if (Platform.isMacOS) return ShellType.zsh; + if (Platform.isLinux) return ShellType.bash; + + // Windows: try pwsh7 first, then powershell, then cmd + if (await _isCommandAvailable('pwsh')) { + return ShellType.pwsh7; + } + if (await _isCommandAvailable('powershell')) { + return ShellType.powershell; + } + return ShellType.cmd; // Always available on Windows + } + + /// Synchronous version that returns pwsh7 as default for Windows. + /// Use getPlatformDefault() for actual availability check. + /// Returns the default shell for the current platform. + static ShellType get platformDefault { + if (Platform.isMacOS) return ShellType.zsh; + if (Platform.isLinux) return ShellType.bash; + return ShellType.pwsh7; // Windows default (will fallback at runtime) + } + + /// Resolves the actual executable for a command name on Windows, + /// implementing a fallback if the primary command is not found. + /// Order: pwsh > powershell > cmd + static String resolveWindowsCommand(String command) { + if (!Platform.isWindows) return command; + + final shell = command.toLowerCase(); + if (shell == 'pwsh' || shell == 'pwsh.exe') { + try { + final result = Process.runSync('where', ['pwsh'], runInShell: true); + if (result.exitCode == 0) return 'pwsh'; + } catch (_) {} + // Fallback to powershell + return resolveWindowsCommand('powershell'); + } + + if (shell == 'powershell' || shell == 'powershell.exe') { + try { + final result = + Process.runSync('where', ['powershell'], runInShell: true); + if (result.exitCode == 0) return 'powershell'; + } catch (_) {} + // Fallback to cmd + return 'cmd'; + } + + return command; + } + + /// Check if a command is available in the system PATH. + static Future _isCommandAvailable(String command) async { + try { + final checkCmd = Platform.isWindows ? 'where' : 'which'; + final result = await Process.run( + checkCmd, + [command], + runInShell: true, + ); + return result.exitCode == 0; + } catch (_) { + return false; + } + } } diff --git a/lib/features/settings/widgets/update_settings_content.dart b/lib/features/settings/widgets/update_settings_content.dart index 7416099..99a8e5f 100644 --- a/lib/features/settings/widgets/update_settings_content.dart +++ b/lib/features/settings/widgets/update_settings_content.dart @@ -1,6 +1,9 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:gap/gap.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:updat/updat.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -26,10 +29,12 @@ class _UpdateSettingsContentState extends State { void initState() { super.initState(); _loadVersion(); + // _cleanupDownloadedFiles() is called inside _loadVersion() } Future _loadVersion() async { final version = await AppVersionService.instance.getVersion(); + _cleanupDownloadedFiles(); // Also cleanup when checking/loading version if (mounted) { setState(() { _currentVersion = version; @@ -45,6 +50,35 @@ class _UpdateSettingsContentState extends State { } } + /// Clean up any leftover downloaded installer files in the temp directory. + Future _cleanupDownloadedFiles() async { + try { + final tempDir = await getTemporaryDirectory(); + final dir = Directory(tempDir.path); + if (await dir.exists()) { + final List entities = await dir.list().toList(); + for (final entity in entities) { + if (entity is File) { + final fileName = entity.path.split(Platform.pathSeparator).last; + // Matches: flutter_agent_panel-0.0.6-windows-x86_64-setup.exe, etc. + if (fileName.startsWith('flutter_agent_panel-') && + (fileName.endsWith('.exe') || + fileName.endsWith('.dmg') || + fileName.endsWith('.tar.gz'))) { + try { + await entity.delete(); + } catch (e) { + // File might be in use, ignore + } + } + } + } + } + } catch (e) { + // Ignore cleanup errors + } + } + @override Widget build(BuildContext context) { final theme = context.theme; @@ -121,6 +155,16 @@ class _UpdateSettingsContentState extends State { latestVersion ?? _currentVersion!, ); }, + getDownloadFileLocation: (latestVersion) async { + final url = await AppVersionService.instance.getBinaryUrl( + latestVersion ?? _currentVersion!, + ); + final fileName = url.split('/').last; + // Download to temp directory instead of Downloads folder + final tempDir = await getTemporaryDirectory(); + final file = File('${tempDir.path}/$fileName'); + return file; + }, appName: 'Flutter Agent Panel', updateChipBuilder: _buildUpdateChip, updateDialogBuilder: _buildUpdateDialog, @@ -130,8 +174,9 @@ class _UpdateSettingsContentState extends State { if (!mounted) return; if (status == UpdatStatus.upToDate) { - // Clear any previous error + // Clear any previous error and cleanup downloaded file setState(() => _errorMessage = null); + _cleanupDownloadedFiles(); ShadToaster.of(context).show( ShadToast( title: Row( @@ -148,10 +193,11 @@ class _UpdateSettingsContentState extends State { ), ); } else if (status == UpdatStatus.error) { - // Set error message for display + // Set error message for display and cleanup setState(() { _errorMessage = l10n.updateErrorMessage; }); + _cleanupDownloadedFiles(); } else if (status == UpdatStatus.available || status == UpdatStatus.checking || status == UpdatStatus.downloading) { diff --git a/lib/features/terminal/bloc/terminal_bloc.dart b/lib/features/terminal/bloc/terminal_bloc.dart index b62116b..f5decc7 100644 --- a/lib/features/terminal/bloc/terminal_bloc.dart +++ b/lib/features/terminal/bloc/terminal_bloc.dart @@ -6,7 +6,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:xterm/xterm.dart'; import '../../../core/services/app_logger.dart'; -import '../../../shared/utils/platform_utils.dart'; +import '../../settings/models/app_settings.dart'; +import '../../settings/models/shell_type.dart'; import '../models/terminal_config.dart'; import '../models/terminal_node.dart'; import '../services/isolate_pty.dart'; @@ -269,7 +270,12 @@ class TerminalBloc extends Bloc { // Resolve shell path and handle spaces shell = config.shellCmd.isNotEmpty ? config.shellCmd - : (PlatformUtils.isWindows ? 'pwsh.exe' : '/bin/bash'); + : ShellType.platformDefault.command; + + // Apply Windows fallback if needed + if (Platform.isWindows) { + shell = ShellType.resolveWindowsCommand(shell); + } // Quote shell path if it contains spaces and is not already quoted // REMOVED: Dart's Process.start handles executable paths with spaces correctly. diff --git a/lib/features/workspace/widgets/shell_selection_popover.dart b/lib/features/workspace/widgets/shell_selection_popover.dart index effafc6..b1dfb0d 100644 --- a/lib/features/workspace/widgets/shell_selection_popover.dart +++ b/lib/features/workspace/widgets/shell_selection_popover.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; @@ -64,10 +62,10 @@ class ShellSelectionPopover extends StatelessWidget { ), ), Divider(height: 1, color: theme.colorScheme.border), - // Built-in shells (excluding custom and WSL on non-Windows) + // Built-in shells (filter by platform availability) ...ShellType.values .where((s) => s != ShellType.custom) - .where((s) => s != ShellType.wsl || Platform.isWindows) + .where((s) => s.isAvailableOnCurrentPlatform) .map((shell) => _buildShellItem(context, theme, shell, l10n)), // Custom shells from settings if (settings.customShells.isNotEmpty) ...[ @@ -155,6 +153,8 @@ class ShellSelectionPopover extends StatelessWidget { String _getShellTypeLocalizedName(ShellType shell, AppLocalizations l10n) => switch (shell) { + ShellType.zsh => l10n.zsh, + ShellType.bash => l10n.bash, ShellType.pwsh7 => l10n.pwsh7, ShellType.powershell => l10n.powershell, ShellType.cmd => l10n.cmd, diff --git a/lib/shared/utils/settings_helpers.dart b/lib/shared/utils/settings_helpers.dart index a613257..b6acd91 100644 --- a/lib/shared/utils/settings_helpers.dart +++ b/lib/shared/utils/settings_helpers.dart @@ -13,6 +13,8 @@ String getAppThemeLocalizedName(AppTheme theme, AppLocalizations l10n) => /// Get localized name for shell type String getShellTypeLocalizedName(ShellType shell, AppLocalizations l10n) => switch (shell) { + ShellType.zsh => l10n.zsh, + ShellType.bash => l10n.bash, ShellType.pwsh7 => l10n.pwsh7, ShellType.powershell => l10n.powershell, ShellType.cmd => l10n.cmd, diff --git a/packages/flutter_pty/src/flutter_pty_win.c b/packages/flutter_pty/src/flutter_pty_win.c index ffde8e3..25b3e3b 100644 --- a/packages/flutter_pty/src/flutter_pty_win.c +++ b/packages/flutter_pty/src/flutter_pty_win.c @@ -7,6 +7,35 @@ #include "include/dart_api_dl.h" #include "include/dart_native_api.h" +/** + * Convert UTF-8 string to Wide Character (UTF-16) string. + * Uses Windows API MultiByteToWideChar for correct multi-byte handling. + * Caller is responsible for freeing the returned string. + */ +static LPWSTR utf8_to_wide(const char *utf8_str) +{ + if (utf8_str == NULL) + return NULL; + + // Get required buffer size (including null terminator) + int wide_len = MultiByteToWideChar(CP_UTF8, 0, utf8_str, -1, NULL, 0); + if (wide_len == 0) + return NULL; + + LPWSTR wide_str = malloc(wide_len * sizeof(WCHAR)); + if (wide_str == NULL) + return NULL; + + // Perform the conversion + if (MultiByteToWideChar(CP_UTF8, 0, utf8_str, -1, wide_str, wide_len) == 0) + { + free(wide_str); + return NULL; + } + + return wide_str; +} + static int extra_for_quotes(char *s) { if (s == NULL) @@ -23,6 +52,7 @@ static int extra_for_quotes(char *s) static LPWSTR build_command(char *executable, char **arguments) { + // Calculate total length needed for the UTF-8 command string int command_length = 0; // If arguments is provided, we assume arguments[0] is the executable name @@ -36,8 +66,9 @@ static LPWSTR build_command(char *executable, char **arguments) i++; } - LPWSTR command = malloc((command_length + 1) * sizeof(WCHAR)); - if (command == NULL) + // Build command in UTF-8 first + char *command_utf8 = malloc(command_length + 1); + if (command_utf8 == NULL) return NULL; int pos = 0; @@ -45,40 +76,50 @@ static LPWSTR build_command(char *executable, char **arguments) while (arguments[j] != NULL) { if (j > 0) - command[pos++] = ' '; + command_utf8[pos++] = ' '; - command[pos++] = '"'; + command_utf8[pos++] = '"'; int k = 0; while (arguments[j][k] != 0) { if (arguments[j][k] == '"') - command[pos++] = '\\'; - command[pos++] = (WCHAR)arguments[j][k++]; + command_utf8[pos++] = '\\'; + command_utf8[pos++] = arguments[j][k++]; } - command[pos++] = '"'; + command_utf8[pos++] = '"'; j++; } - command[pos] = 0; + command_utf8[pos] = 0; + + // Convert to wide string using proper UTF-8 handling + LPWSTR command = utf8_to_wide(command_utf8); + free(command_utf8); return command; } else if (executable != NULL) { command_length = (int)strlen(executable) + extra_for_quotes(executable); - LPWSTR command = malloc((command_length + 1) * sizeof(WCHAR)); - if (command == NULL) + + // Build command in UTF-8 first + char *command_utf8 = malloc(command_length + 1); + if (command_utf8 == NULL) return NULL; int pos = 0; - command[pos++] = '"'; + command_utf8[pos++] = '"'; int j = 0; while (executable[j] != 0) { if (executable[j] == '"') - command[pos++] = '\\'; - command[pos++] = (WCHAR)executable[j++]; + command_utf8[pos++] = '\\'; + command_utf8[pos++] = executable[j++]; } - command[pos++] = '"'; - command[pos] = 0; + command_utf8[pos++] = '"'; + command_utf8[pos] = 0; + + // Convert to wide string using proper UTF-8 handling + LPWSTR command = utf8_to_wide(command_utf8); + free(command_utf8); return command; } @@ -87,79 +128,71 @@ static LPWSTR build_command(char *executable, char **arguments) static LPWSTR build_environment(char **environment) { - LPWSTR environment_block = NULL; - int environment_block_length = 0; + if (environment == NULL) + return NULL; - if (environment != NULL) + // First pass: calculate total wide character length needed + int total_wide_len = 0; + int i = 0; + while (environment[i] != NULL) { - int i = 0; - - while (environment[i] != NULL) - { - environment_block_length += (int)strlen(environment[i]) + 1; - i++; - } + // Get wide char length for each environment variable + int wide_len = MultiByteToWideChar(CP_UTF8, 0, environment[i], -1, NULL, 0); + if (wide_len > 0) + total_wide_len += wide_len; // includes null terminator for each + i++; } - environment_block = malloc((environment_block_length + 1) * sizeof(WCHAR)); + if (total_wide_len == 0) + return NULL; - if (environment_block != NULL) + // Allocate environment block (+1 for final double-null terminator) + LPWSTR environment_block = malloc((total_wide_len + 1) * sizeof(WCHAR)); + if (environment_block == NULL) + return NULL; + + // Second pass: convert and copy each environment variable + int pos = 0; + i = 0; + while (environment[i] != NULL) { - int i = 0; + // Calculate remaining space in the buffer + int remaining = total_wide_len + 1 - pos; + if (remaining <= 0) break; - if (environment != NULL) + int converted = MultiByteToWideChar(CP_UTF8, 0, environment[i], -1, + &environment_block[pos], remaining); + if (converted > 0) { - int j = 0; - - while (environment[j] != NULL) - { - int k = 0; - - while (environment[j][k] != 0) - { - environment_block[i] = (WCHAR)environment[j][k]; - i++; - k++; - } - - environment_block[i++] = 0; - - j++; - } + pos += converted; // includes null terminator } - - environment_block[i] = 0; - } - - return environment_block; -} - -static LPWSTR build_working_directory(char *working_directory) -{ - if (working_directory == NULL) - { - return NULL; + else + { + // If conversion fails, something is wrong with the input. + // But we must ensure the block remains valid (null terminated). + // For now, we skip this entry, but stay safe. + } + i++; } - int working_directory_length = (int)strlen(working_directory); - - LPWSTR working_directory_block = malloc((working_directory_length + 1) * sizeof(WCHAR)); - - if (working_directory_block == NULL) + // Add final null terminator for double-null terminated block + if (pos <= total_wide_len) { - return NULL; + environment_block[pos] = 0; } - - int i = 0; - - while (working_directory[i] != 0) + else { - working_directory_block[i] = (WCHAR)working_directory[i++]; + // Should not happen, but for absolute safety + environment_block[total_wide_len] = 0; } - working_directory_block[i] = 0; + return environment_block; +} - return working_directory_block; +static LPWSTR build_working_directory(char *working_directory) +{ + // Use utf8_to_wide for proper UTF-8 to Wide Char conversion + return utf8_to_wide(working_directory); } typedef struct ReadLoopOptions diff --git a/pubspec.lock b/pubspec.lock index 7c356e6..049e818 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,14 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - console: - dependency: transitive - description: - name: console - sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a - url: "https://pub.dev" - source: hosted - version: "4.1.0" convert: dependency: transitive description: @@ -437,14 +429,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" - get_it: - dependency: transitive - description: - name: get_it - sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9 - url: "https://pub.dev" - source: hosted - version: "8.3.0" glob: dependency: transitive description: @@ -669,14 +653,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.6.1" - msix: - dependency: "direct dev" - description: - name: msix - sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5 - url: "https://pub.dev" - source: hosted - version: "3.16.12" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 992e22c..494e0b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_agent_panel description: "A modern multi terminal application for AI agent." publish_to: 'none' -version: 0.0.6+1 +version: 0.0.7+1 environment: sdk: ^3.6.0 @@ -51,7 +51,6 @@ dev_dependencies: flutter_lints: ^5.0.0 lean_builder: ^0.1.6 auto_route_generator: ^10.4.0 - msix: ^3.16.12 flutter: uses-material-design: true @@ -73,15 +72,6 @@ flutter_launcher_icons: generate: true image_path: "assets/images/app_icon.png" -msix_config: - display_name: Flutter Agent Panel - publisher_display_name: Flutter Agent Panel Team - identity_name: dev.aykahshi.flutteragentpanel - logo_path: assets/images/app_icon.png - capabilities: internetClient - output_name: flutter_agent_panel - install_certificate: false -