diff --git a/.github/workflows/autotag.yml b/.github/workflows/autotag.yml new file mode 100644 index 0000000..c2ffe12 --- /dev/null +++ b/.github/workflows/autotag.yml @@ -0,0 +1,17 @@ +name: Create Tag + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: jacopocarlini/action-autotag@3.0.0 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + with: + tag_prefix: "v" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6ac3d48 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,95 @@ +name: Build + +on: + workflow_dispatch: + release: + types: [created] + +jobs: + build_macos: + runs-on: macos-latest + steps: + # Setup + - uses: actions/checkout@v3 + + - uses: subosito/flutter-action@v2 + with: + channel: "stable" + architecture: x64 + + - uses: actions/setup-node@v2 + + # Install packaging tools + - name: Install flutter_distributor + run: dart pub global activate flutter_distributor + + - name: Install appdmg + run: npm install -g appdmg + + # Build and package + - name: Packaging .dmg .zip + run: flutter_distributor package --platform macos --targets dmg,zip --artifact-name "TerminalStudio-${{github.ref_name}}-macos.{{ext}}" + + # Publish + - name: Publish to GitHub Release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: dist/*/* + + build_windows: + runs-on: windows-latest + steps: + # Setup + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + channel: "stable" + architecture: x64 + + # Install packaging tools + - name: Install flutter_distributor + run: dart pub global activate flutter_distributor + + # Build and package + - name: Packaging .msix .zip + run: flutter_distributor package --platform windows --targets msix,zip --artifact-name "TerminalStudio-${{github.ref_name}}-windows.{{ext}}" + + # Publish + - name: Publish to GitHub Release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: dist/*/* + + build_linux: + runs-on: ubuntu-latest + steps: + # Setup + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + channel: "stable" + architecture: x64 + + - run: | + sudo apt-get update -y + sudo apt-get install -y ninja-build libgtk-3-dev + + # Install packaging tools + - name: Install flutter_distributor + run: dart pub global activate flutter_distributor + + # Build and package + - name: Packaging .deb .zip + run: flutter_distributor package --platform linux --targets deb,zip --artifact-name "TerminalStudio-${{github.ref_name}}-linux.{{ext}}" + + # Publish + - name: Publish to GitHub Release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: dist/*/* diff --git a/.gitignore b/.gitignore index c86d398..a5574bf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,36 @@ *.iws .idea/ +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id .dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +.vscode -# vscode -.vscode/ \ No newline at end of file +dist/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 6e49a23..0000000 --- a/.gitmodules +++ /dev/null @@ -1,8 +0,0 @@ -[submodule "xterm"] - path = xterm - url = https://github.com/TerminalStudio/xterm.dart.git - branch = master -[submodule "pty"] - path = pty - url = https://github.com/TerminalStudio/pty.git - branch = master \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..39f2501 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: android + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: ios + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: linux + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: macos + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: web + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: windows + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/studio/android/.gitignore b/android/.gitignore similarity index 91% rename from studio/android/.gitignore rename to android/.gitignore index 0a741cb..6f56801 100644 --- a/studio/android/.gitignore +++ b/android/.gitignore @@ -9,3 +9,5 @@ GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties +**/*.keystore +**/*.jks diff --git a/studio/android/app/build.gradle b/android/app/build.gradle similarity index 72% rename from studio/android/app/build.gradle rename to android/app/build.gradle index 508d4c4..08131ca 100644 --- a/studio/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,21 +26,29 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' } - lintOptions { - disable 'InvalidPackage' + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.studio" - minSdkVersion 16 - targetSdkVersion 28 + applicationId "com.dartssh.terminalstudio" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/studio/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml similarity index 62% rename from studio/android/app/src/debug/AndroidManifest.xml rename to android/app/src/debug/AndroidManifest.xml index 1120474..76a5fec 100644 --- a/studio/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,6 +1,7 @@ - diff --git a/studio/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml similarity index 59% rename from studio/android/app/src/main/AndroidManifest.xml rename to android/app/src/main/AndroidManifest.xml index 24f599b..04fa004 100644 --- a/studio/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,16 +1,12 @@ - - - - diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..30986e6 Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/studio/android/app/src/main/kotlin/com/example/studio/MainActivity.kt b/android/app/src/main/kotlin/com/example/studio/MainActivity.kt similarity index 100% rename from studio/android/app/src/main/kotlin/com/example/studio/MainActivity.kt rename to android/app/src/main/kotlin/com/example/studio/MainActivity.kt diff --git a/studio/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from studio/android/app/src/main/res/drawable-v21/launch_background.xml rename to android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/studio/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from studio/android/app/src/main/res/drawable/launch_background.xml rename to android/app/src/main/res/drawable/launch_background.xml diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..ce605cd Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..acfdc95 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..65800c8 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..1cadc39 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..0031312 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..b426ec5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..3f11a7e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..277ed87 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..7e07c26 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..8842284 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/studio/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml similarity index 94% rename from studio/android/app/src/main/res/values-night/styles.xml rename to android/app/src/main/res/values-night/styles.xml index 449a9f9..06952be 100644 --- a/studio/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -3,14 +3,14 @@ - diff --git a/studio/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml similarity index 62% rename from studio/android/app/src/profile/AndroidManifest.xml rename to android/app/src/profile/AndroidManifest.xml index 1120474..76a5fec 100644 --- a/studio/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -1,6 +1,7 @@ - diff --git a/studio/android/build.gradle b/android/build.gradle similarity index 76% rename from studio/android/build.gradle rename to android/build.gradle index 3100ad2..83ae220 100644 --- a/studio/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -14,7 +14,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/studio/android/gradle.properties b/android/gradle.properties similarity index 78% rename from studio/android/gradle.properties rename to android/gradle.properties index 38c8d45..94adc3a 100644 --- a/studio/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/studio/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from studio/android/gradle/wrapper/gradle-wrapper.properties rename to android/gradle/wrapper/gradle-wrapper.properties index 296b146..cc5527d 100644 --- a/studio/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/studio/android/settings.gradle b/android/settings.gradle similarity index 100% rename from studio/android/settings.gradle rename to android/settings.gradle diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..312e896 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/icon_macos.png b/assets/icon_macos.png new file mode 100644 index 0000000..7698102 Binary files /dev/null and b/assets/icon_macos.png differ diff --git a/context_menu_macos b/context_menu_macos deleted file mode 160000 index 34113f2..0000000 --- a/context_menu_macos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 34113f2ba45abb0b900368a5d75576dbd1f3e8e4 diff --git a/icons_launcher.yaml b/icons_launcher.yaml new file mode 100644 index 0000000..ae975fe --- /dev/null +++ b/icons_launcher.yaml @@ -0,0 +1,14 @@ +icons_launcher: + image_path: "assets/icon.png" + platforms: + android: + enable: true + ios: + enable: true + macos: + enable: true + image_path: "assets/icon_macos.png" + windows: + enable: true + linux: + enable: true diff --git a/studio/ios/.gitignore b/ios/.gitignore similarity index 95% rename from studio/ios/.gitignore rename to ios/.gitignore index e96ef60..7a7f987 100644 --- a/studio/ios/.gitignore +++ b/ios/.gitignore @@ -1,3 +1,4 @@ +**/dgph *.mode1v3 *.mode2v3 *.moved-aside @@ -18,6 +19,7 @@ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig +Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ diff --git a/studio/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist similarity index 91% rename from studio/ios/Flutter/AppFrameworkInfo.plist rename to ios/Flutter/AppFrameworkInfo.plist index 6b4c0f7..8d4492f 100644 --- a/studio/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) + en CFBundleExecutable App CFBundleIdentifier @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..1e8c3c9 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/studio/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj similarity index 94% rename from studio/ios/Runner.xcodeproj/project.pbxproj rename to ios/Runner.xcodeproj/project.pbxproj index 02a6fc1..91c66d5 100644 --- a/studio/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -127,7 +127,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -272,7 +272,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -288,18 +288,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.studio; + PRODUCT_BUNDLE_IDENTIFIER = com.dartssh.terminalstudio; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -354,7 +350,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -403,11 +399,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -420,18 +417,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.studio; + PRODUCT_BUNDLE_IDENTIFIER = com.dartssh.terminalstudio; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -447,18 +440,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.studio; + PRODUCT_BUNDLE_IDENTIFIER = com.dartssh.terminalstudio; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -492,4 +481,4 @@ /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; -} +} \ No newline at end of file diff --git a/studio/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 71% rename from studio/ios/Runner.xcworkspace/contents.xcworkspacedata rename to ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/studio/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/studio/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from studio/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/studio/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from studio/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/studio/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 95% rename from studio/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140c..c87d15a 100644 --- a/studio/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Studio CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - studio + TerminalStudio CFBundlePackageType APPL CFBundleShortVersionString @@ -41,5 +43,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/studio/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from studio/ios/Runner/Runner-Bridging-Header.h rename to ios/Runner/Runner-Bridging-Header.h diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..da9f571 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,184 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:context_menus/context_menus.dart'; +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/hosts/local_spec.dart'; +import 'package:terminal_studio/src/core/service/tabs_service.dart'; +import 'package:terminal_studio/src/core/state/tabs.dart'; +import 'package:terminal_studio/src/ui/context_menu.dart'; +import 'package:terminal_studio/src/ui/platform_menu.dart'; +import 'package:terminal_studio/src/ui/shared/fluent_menu_card.dart'; +import 'package:terminal_studio/src/ui/shared/macos_titlebar.dart'; +import 'package:terminal_studio/src/ui/shortcut/global_actions.dart'; +import 'package:terminal_studio/src/ui/shortcut/global_shortcuts.dart'; +import 'package:terminal_studio/src/util/provider_logger.dart'; +import 'package:window_manager/window_manager.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + initWindow(); + + runApp( + const ProviderScope( + observers: [ProviderLogger()], + child: MyApp(), + ), + ); +} + +Future initWindow() async { + await windowManager.ensureInitialized(); + await windowManager.setBackgroundColor(const Color(0x00000000)); + await windowManager.setTitle('TerminalStudio'); + + if (defaultTargetPlatform != TargetPlatform.macOS) { + await windowManager.setTitleBarStyle(TitleBarStyle.hidden); + } + + windowManager.waitUntilReadyToShow(null, () async { + await windowManager.show(); + await windowManager.focus(); + }); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget widget = const GlobalShortcuts( + child: GlobalActions( + child: Home(), + ), + ); + + if (defaultTargetPlatform == TargetPlatform.macOS) { + widget = GlobalPlatformMenu( + child: widget, + ); + } + + widget = ContextMenuOverlay( + child: widget, + cardBuilder: (context, children) => FluentMenuCard(children: children), + ); + + return FluentApp( + title: 'TerminalStudio', + debugShowCheckedModeBanner: false, + home: widget, + ); + } +} + +class Home extends ConsumerStatefulWidget { + const Home({super.key}); + + @override + ConsumerState createState() => _HomeState(); +} + +class _HomeState extends ConsumerState { + final tabsTheme = const TabsViewThemeData(); + + @override + void initState() { + SchedulerBinding.instance.addPostFrameCallback((_) async { + await initTabs(); + }); + super.initState(); + } + + Future initTabs() async { + final root = Tabs(); + + ref.read(tabsServiceProvider).openTerminal(LocalHostSpec(), tabs: root); + + final document = ref.watch(tabsProvider); + + document.addListener(_onDocumentChanged); + + document.setRoot(root); + } + + void _onDocumentChanged() { + final document = ref.read(tabsProvider); + + document.root?.addListener(_onRootChanged); + } + + void _onRootChanged() { + final document = ref.read(tabsProvider); + + if (document.root == null || document.root!.children.isEmpty) { + exit(0); + } + } + + @override + Widget build(BuildContext context) { + Widget widget = Column( + children: [ + _buildTitlebar(context), + Expanded( + child: TabsView( + ref.watch(tabsProvider), + theme: tabsTheme, + actionBuilder: buildTabActions, + ), + ), + ], + ); + + // if (defaultTargetPlatform == TargetPlatform.windows) { + // widget = VirtualWindowFrame(child: widget); + // } + + return widget; + } + + Widget _buildTitlebar(BuildContext context) { + if (defaultTargetPlatform == TargetPlatform.macOS) { + return MacosTitlebar( + color: tabsTheme.selectedBackgroundColor, + ); + } + + return SizedBox( + height: kWindowCaptionHeight, + child: WindowCaption( + backgroundColor: tabsTheme.selectedBackgroundColor, + brightness: Brightness.light, + title: const Text('TerminalStudio'), + ), + ); + } + + List buildTabActions(Tabs tabs) { + return [ + TabsViewAction( + icon: CupertinoIcons.chevron_down, + onPressed: () { + context.contextMenuOverlay.show( + DropdownContextMenu(tabs), + ); + }, + ), + TabsViewAction( + icon: CupertinoIcons.add, + onPressed: () { + ref + .watch(tabsServiceProvider) + .openTerminal(LocalHostSpec(), tabs: tabs); + }, + ), + ]; + } +} diff --git a/lib/src/core/conn.dart b/lib/src/core/conn.dart new file mode 100644 index 0000000..6863328 --- /dev/null +++ b/lib/src/core/conn.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/host.dart'; + +abstract class HostSpec { + String get name; + + HostConnector createConnector(); +} + +enum HostConnectorStatus { + initialized, + connecting, + connected, + disconnected, + aborted, +} + +abstract class HostConnector + extends StateNotifier { + HostConnector() : super(HostConnectorStatus.initialized); + + T? _host; + + T? get host => _host; + + @protected + Future createHost(); + + Future connect() async { + if (state == HostConnectorStatus.connected || + state == HostConnectorStatus.connecting) { + return; + } + + state = HostConnectorStatus.connecting; + + try { + _host = await createHost(); + _host!.done.then((_) => _onDone(), onError: _onError); + + state = HostConnectorStatus.connected; + } catch (e) { + state = HostConnectorStatus.disconnected; + } + } + + Future disconnect() async { + await _host?.disconnect(); + _host = null; + state = HostConnectorStatus.disconnected; + } + + void _onDone() { + _host = null; + state = HostConnectorStatus.disconnected; + } + + void _onError(Object error) { + _host = null; + state = HostConnectorStatus.aborted; + } +} diff --git a/lib/src/core/fs.dart b/lib/src/core/fs.dart new file mode 100644 index 0000000..acb20ae --- /dev/null +++ b/lib/src/core/fs.dart @@ -0,0 +1,362 @@ +import 'dart:typed_data'; + +import 'package:path/path.dart' as p; + +enum FileSystemEntityType { + /// The file system entity is a file. + file, + + /// The file system entity is a directory. + directory, + + /// The file system entity is a link. + link, + + /// The file system entity is a socket. + socket, + + /// The file system entity is a pipe. + pipe, + + /// The file system entity is unknown. + unknown, +} + +/// A generic representation of a file system. +abstract class FileSystem { + /// Creates a new `FileSystem`. + const FileSystem(); + + /// Returns a reference to a [Directory] at [path]. + Directory directory(String path); + + /// Returns a reference to a [File] at [path]. + File file(String path); + + /// Returns a reference to a [Link] at [path]. + Link link(String path); + + /// An object for manipulating paths in this file system. + p.Context get path; + + /// Creates a directory object pointing to the current working directory. + Directory get currentDirectory; + + /// Asynchronously calls the operating system's stat() function on [path]. + /// Returns a Future which completes with a [FileStat] object containing + /// the data returned by stat(). + Future stat(String path); + + /// Checks whether two paths refer to the same object in the + /// file system. Returns a [Future] that completes with the result. + /// + /// Comparing a link to its target returns false, as does comparing two links + /// that point to the same target. To check the target of a link, use + /// Link.target explicitly to fetch it. Directory links appearing + /// inside a path are followed, though, to find the file system object. + /// + /// Completes the returned Future with an error if one of the paths points + /// to an object that does not exist. + Future identical(String path1, String path2); + + /// Tests if [FileSystemEntity.watch] is supported on the current system. + bool get isWatchSupported; + + /// Finds the type of file system object that a [path] points to. Returns + /// a Future that completes with the result. + /// + /// [FileSystemEntityType.LINK] will only be returned if [followLinks] is + /// `false`, and [path] points to a link + /// + /// If the [path] does not point to a file system object or an error occurs + /// then [FileSystemEntityType.notFound] is returned. + Future type(String path, {bool followLinks = true}); + + /// Checks if [`type(path)`](type) returns [FileSystemEntityType.FILE]. + Future isFile(String path) async => + await type(path) == FileSystemEntityType.file; + + /// Checks if [`type(path)`](type) returns [FileSystemEntityType.DIRECTORY]. + Future isDirectory(String path) async => + await type(path) == FileSystemEntityType.directory; + + /// Checks if [`type(path)`](type) returns [FileSystemEntityType.LINK]. + Future isLink(String path) async => + await type(path) == FileSystemEntityType.link; +} + +/// The common super class for [File], [Directory], and [Link] objects. +abstract class FileSystemEntity { + String get path; + + /// Returns the file system responsible for this entity. + FileSystem get fileSystem; + + /// Gets the part of this entity's path after the last separator. + /// + /// context.basename('path/to/foo.dart'); // -> 'foo.dart' + /// context.basename('path/to'); // -> 'to' + /// + /// Trailing separators are ignored. + /// + /// context.basename('path/to/'); // -> 'to' + String get basename => fileSystem.path.basename(path); + + /// Gets the part of this entity's path before the last separator. + /// + /// context.dirname('path/to/foo.dart'); // -> 'path/to' + /// context.dirname('path/to'); // -> 'path' + /// context.dirname('foo.dart'); // -> '.' + /// + /// Trailing separators are ignored. + /// + /// context.dirname('path/to/'); // -> 'path' + String get dirname => fileSystem.path.dirname(path); + + /// The parent directory of this entity. + Directory get parent => fileSystem.directory(dirname); + + /// Checks whether the file system entity with this path exists. + /// + /// Returns a `Future` that completes with the result. + /// + /// Since [FileSystemEntity] is abstract, every [FileSystemEntity] object + /// is actually an instance of one of the subclasses [File], + /// [Directory], and [Link]. Calling [exists] on an instance of one + /// of these subclasses checks whether the object exists in the file + /// system object exists *and* is of the correct type (file, directory, + /// or link). To check whether a path points to an object on the + /// file system, regardless of the object's type, use the [type] + /// static method. + Future exists(); + + /// Renames this file system entity. + /// + /// Returns a `Future` that completes with a + /// [FileSystemEntity] instance for the renamed file system entity. + /// + /// If [newPath] identifies an existing entity of the same type, + /// that entity is removed first. + /// If [newPath] identifies an existing entity of a different type, + /// the operation fails and the future completes with an exception. + Future rename(String newPath); + + /// Calls the operating system's `stat()` function on [path]. + /// + /// Returns a `Future` object containing the data returned by + /// `stat()`. + /// + /// If [path] is a symbolic link then it is resolved and results for the + /// resulting file are returned. + Future stat() => fileSystem.stat(path); + + /// Deletes this [FileSystemEntity]. + /// + /// If the [FileSystemEntity] is a directory, and if [recursive] is `false`, + /// the directory must be empty. Otherwise, if [recursive] is true, the + /// directory and all sub-directories and files in the directories are + /// deleted. Links are not followed when deleting recursively. Only the link + /// is deleted, not its target. + /// + /// If [recursive] is true, the [FileSystemEntity] is deleted even if the type + /// of the [FileSystemEntity] doesn't match the content of the file system. + /// This behavior allows [delete] to be used to unconditionally delete any file + /// system object. + /// + /// Returns a `Future` that completes with this + /// [FileSystemEntity] when the deletion is done. If the [FileSystemEntity] + /// cannot be deleted, the future completes with an exception. + Future delete({bool recursive = false}); + + /// Start watching the [FileSystemEntity] for changes. + /// + /// The returned value is an endless broadcast [Stream], that only stops when + /// one of the following happens: + /// + /// * The [Stream] is canceled, e.g. by calling `cancel` on the + /// [StreamSubscription]. + /// * The [FileSystemEntity] being watched is deleted. + /// * System Watcher exits unexpectedly. + /// + /// Use `events` to specify what events to listen for. The constants in + /// [FileSystemEvent] can be or'ed together to mix events. Default is + /// [FileSystemEvent.all]. + /// + /// A move event may be reported as separate delete and create events. + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }); + + /// A [FileSystemEntity] whose path is the absolute path of [path]. + /// + /// The type of the returned instance is the same as the type of + /// this entity. + /// + /// A file system entity with an already absolute path is returned directly. + /// For a non-absolute path, the returned entity is absolute *if possible*, + /// but still refers to the same file system object. + Future get absolute; + + FileStat? get cachedStat => null; +} + +/// Base event class emitted by [FileSystemEntity.watch]. +class FileSystemEvent { + /// Bitfield for [FileSystemEntity.watch], to enable [FileSystemCreateEvent]s. + static const int create = 1 << 0; + + /// Bitfield for [FileSystemEntity.watch], to enable [FileSystemModifyEvent]s. + static const int modify = 1 << 1; + + /// Bitfield for [FileSystemEntity.watch], to enable [FileSystemDeleteEvent]s. + static const int delete = 1 << 2; + + /// Bitfield for [FileSystemEntity.watch], to enable [FileSystemMoveEvent]s. + static const int move = 1 << 3; + + /// Bitfield for [FileSystemEntity.watch], for enabling all of [create], + /// [modify], [delete] and [move]. + static const int all = create | modify | delete | move; + + /// The type of event. See [FileSystemEvent] for a list of events. + final int type; + + /// The path that triggered the event. + /// + /// Depending on the platform and the [FileSystemEntity], the path may be + /// relative. + final String path; + + /// Is `true` if the event target was a directory. + /// + /// Note that if the file has been deleted by the time the event has arrived, + /// this will always be `false` on Windows. In particular, it will always be + /// `false` for `delete` events. + final bool isDirectory; + + FileSystemEvent(this.type, this.path, this.isDirectory); +} + +class FileStat { + /// The time of the last change to the data or metadata of the file system + /// object. + /// + /// On Windows platforms, this is instead the file creation time. + final DateTime changed; + + /// The time of the last change to the data of the file system object. + final DateTime modified; + + /// The time of the last access to the data of the file system object. + /// + /// On Windows platforms, this may have 1 day granularity, and be + /// out of date by an hour. + final DateTime accessed; + + /// The type of the underlying file system object. + final FileSystemEntityType type; + + /// The size of the file system object. + final int size; + + const FileStat({ + required this.changed, + required this.modified, + required this.accessed, + required this.type, + required this.size, + }); +} + +abstract class File with FileSystemEntity { + Future create({bool recursive = false}); + + @override + Future rename(String newPath); + + Future copy(String newPath); + + @override + Future get absolute; + + Future writeAsBytes( + Uint8List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }); + + Future writeAsString( + String contents, { + FileMode mode = FileMode.write, + bool flush = false, + }); + + Future readAsString(); + + Future readAsBytes(); +} + +/// The modes in which a [File] can be opened. +enum FileMode { + /// The mode for opening a file only for reading. + read, + + /// Mode for opening a file for reading and writing. The file is + /// overwritten if it already exists. The file is created if it does not + /// already exist. + write, + + /// Mode for opening a file for reading and writing to the + /// end of it. The file is created if it does not already exist. + append, + + /// Mode for opening a file for writing *only*. The file is + /// overwritten if it already exists. The file is created if it does not + /// already exist. + writeOnly, + + /// Mode for opening a file for writing *only* to the + /// end of it. The file is created if it does not already exist. + writeOnlyAppend, +} + +abstract class Directory with FileSystemEntity { + Future create({bool recursive = false}); + + @override + Future rename(String newPath); + + @override + Future get absolute; + + Stream list({ + bool recursive = false, + bool followLinks = true, + }); +} + +abstract class Link with FileSystemEntity { + Future create(String target, {bool recursive = false}); + + Future update(String target); + + Future target(); + + @override + Future rename(String newPath); + + @override + Future get absolute; +} + +class FileSystemException { + final String message; + + final String path; + + FileSystemException(this.message, this.path); + + @override + String toString() => "FileSystemException: $message, path = '$path'"; +} diff --git a/lib/src/core/host.dart b/lib/src/core/host.dart new file mode 100644 index 0000000..e7dc08d --- /dev/null +++ b/lib/src/core/host.dart @@ -0,0 +1,48 @@ +import 'dart:typed_data'; + +import 'package:terminal_studio/src/core/fs.dart'; + +abstract class Host { + Future connectFileSystem(); + + Future execute( + String executable, { + List args = const [], + bool root = false, + Map? environment, + }); + + Future shell({ + int width = 80, + int height = 25, + Map? environment, + }); + + Future disconnect(); + + Future get done; +} + +/// Result of command execution. +abstract class ExecutionResult { + /// Exit code of the command. + int get exitCode; + + /// Standard output of the command. + String get stdout; + + /// Standard error of the command. + String get stderr; +} + +abstract class ExecutionSession { + Future write(Uint8List data); + + Future resize(int width, int height); + + Future close(); + + Stream get output; + + Future get exitCode; +} diff --git a/lib/src/core/plugin.dart b/lib/src/core/plugin.dart new file mode 100644 index 0000000..8827dc6 --- /dev/null +++ b/lib/src/core/plugin.dart @@ -0,0 +1,165 @@ +import 'package:flutter/widgets.dart'; +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/core/host.dart'; + +abstract class Plugin { + PluginManager? _manager; + + /// The plugin manager that manages the lifecycle of this plugin. + PluginManager get manager { + if (_manager == null) { + throw StateError('Plugin is not attached to a manager'); + } + + return _manager!; + } + + HostSpec? _hostSpec; + + /// The interface through which the plugin can read information about the host. + /// This is available after [didMounted] is called. + HostSpec get hostSpec { + if (_hostSpec == null) { + throw Exception('Plugin has not been mounted'); + } + return _hostSpec!; + } + + Host? _host; + + /// The interface through which the plugin can interact with the host. Access + /// to this property will throw an exception if the host has not been connected. + Host get host { + if (_host == null) { + throw Exception('Plugin has not been connected to a host.'); + } + return _host!; + } + + /// Whether the plugin is currently mounted. + bool get mounted => _manager != null; + + /// Whether the plugin is connected to a host. + bool get connected => _host != null; + + /// The title of the plugin. Usually displayed as the tab title. + final title = ValueNotifier(null); + + /// Called when the plugin is mounted to a host. After this method is called, + /// the [hostSpec] property will be available. + void didMounted() {} + + /// Called when the plugin is unmounted from a host. After this method is + /// called, the [hostSpec] property will no longer be available. + void didUnmounted() {} + + /// Called when the host that this plugin is mounted to is connected. This + /// method may be called multiple times if the host is disconnected and + /// reconnected. + void didConnected() {} + + /// Called when the host that this plugin is mounted to is disconnected. This + /// method may be called multiple times if the host is disconnected and + /// reconnected. + void didDisconnected() {} + + /// Called when the state of the host that this plugin is mounted to changes. + void onConnectionStatus(HostConnectorStatus status) {} + + // void onConnectionMessage(String message) {} + + // void onConnectionError(Object error, StackTrace stackTrace) {} + + /// Builds the UI for this plugin. This may be called multiple times during + /// the lifetime of the plugin, for example when the plugin is moved to a new + /// tab group. + Widget build(BuildContext context); +} + +class PluginManager with ChangeNotifier { + final HostSpec hostSpec; + + PluginManager(this.hostSpec); + + final _plugins = []; + + List get plugins => List.unmodifiable(_plugins); + + Host? _host; + + void add(Plugin plugin) { + if (plugin._manager != null) { + throw Exception('Plugin is already mounted'); + } + _plugins.add(plugin); + + plugin._manager = this; + plugin._hostSpec = hostSpec; + plugin.didMounted(); + + if (_host != null) { + plugin._host = _host; + plugin.didConnected(); + } + + notifyListeners(); + } + + void remove(Plugin plugin) { + if (plugin._manager != this) { + throw Exception('Plugin is not mounted'); + } + _plugins.remove(plugin); + + plugin.didUnmounted(); + plugin._manager = null; + plugin._hostSpec = null; + plugin._host = null; + + notifyListeners(); + } + + void didConnected(Host host) { + if (_host != null) { + throw Exception('plugin manager is already connected to $_host'); + } + + _host = host; + + for (final plugin in _plugins) { + plugin._host = host; + plugin.didConnected(); + } + } + + void didDisconnected() { + if (_host == null) { + throw Exception('plugin manager is not connected'); + } + + _host = null; + + for (final plugin in _plugins) { + plugin._host = null; + plugin.didDisconnected(); + } + } + + void didConnectionStatusChanged(HostConnectorStatus status) { + for (final plugin in _plugins) { + plugin.onConnectionStatus(status); + } + } + + // void didConnectionMessageChanged(String message) { + // for (final plugin in _plugins) { + // plugin.onConnectionMessage(message); + // } + // } + + // void didConnectionError(Object error, StackTrace stackTrace) { + // for (final plugin in _plugins) { + // plugin.onConnectionError(error, stackTrace); + // } + // } +} diff --git a/lib/src/core/record/ssh_host_record.dart b/lib/src/core/record/ssh_host_record.dart new file mode 100644 index 0000000..206e4e3 --- /dev/null +++ b/lib/src/core/record/ssh_host_record.dart @@ -0,0 +1,47 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/hosts/ssh_conn.dart'; +import 'package:terminal_studio/src/util/uuid.dart'; + +part 'ssh_host_record.g.dart'; + +@HiveType(typeId: 0) +class SSHHostRecord extends HiveObject implements HostSpec { + @HiveField(0) + String uuid; + + @override + @HiveField(1) + String name; + + @HiveField(2) + String host; + + @HiveField(3) + int port; + + @HiveField(4) + String? username; + + @HiveField(5) + String? password; + + SSHHostRecord({ + String? uuid, + required this.name, + required this.host, + required this.port, + this.username, + this.password, + }) : uuid = uuid ?? uuidV4(); + + SSHHostRecord.uninitialized() + : this( + name: '', + host: '', + port: 22, + ); + + @override + HostConnector createConnector() => SSHConnector(this); +} diff --git a/lib/src/core/record/ssh_host_record.g.dart b/lib/src/core/record/ssh_host_record.g.dart new file mode 100644 index 0000000..33f992d --- /dev/null +++ b/lib/src/core/record/ssh_host_record.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ssh_host_record.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SSHHostRecordAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + SSHHostRecord read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SSHHostRecord( + uuid: fields[0] as String?, + name: fields[1] as String, + host: fields[2] as String, + port: fields[3] as int, + username: fields[4] as String?, + password: fields[5] as String?, + ); + } + + @override + void write(BinaryWriter writer, SSHHostRecord obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.uuid) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.host) + ..writeByte(3) + ..write(obj.port) + ..writeByte(4) + ..write(obj.username) + ..writeByte(5) + ..write(obj.password); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SSHHostRecordAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/src/core/record/ssh_key_record.dart b/lib/src/core/record/ssh_key_record.dart new file mode 100644 index 0000000..0cb93eb --- /dev/null +++ b/lib/src/core/record/ssh_key_record.dart @@ -0,0 +1,34 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:terminal_studio/src/util/uuid.dart'; + +part 'ssh_key_record.g.dart'; + +@HiveType(typeId: 1) +class SSHKeyRecord extends HiveObject { + @HiveField(0) + String uuid; + + @HiveField(1) + String name; + + @HiveField(2) + String? comment; + + @HiveField(3) + String? passphrase; + + @HiveField(4) + String? privateKey; + + @HiveField(5) + String? publicKey; + + SSHKeyRecord({ + String? uuid, + required this.name, + this.comment, + this.passphrase, + this.privateKey, + this.publicKey, + }) : uuid = uuid ?? uuidV4(); +} diff --git a/lib/src/core/record/ssh_key_record.g.dart b/lib/src/core/record/ssh_key_record.g.dart new file mode 100644 index 0000000..cae13de --- /dev/null +++ b/lib/src/core/record/ssh_key_record.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ssh_key_record.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SSHKeyRecordAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + SSHKeyRecord read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SSHKeyRecord( + uuid: fields[0] as String?, + name: fields[1] as String, + comment: fields[2] as String?, + passphrase: fields[3] as String?, + privateKey: fields[4] as String?, + publicKey: fields[5] as String?, + ); + } + + @override + void write(BinaryWriter writer, SSHKeyRecord obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.uuid) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.comment) + ..writeByte(3) + ..write(obj.passphrase) + ..writeByte(4) + ..write(obj.privateKey) + ..writeByte(5) + ..write(obj.publicKey); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SSHKeyRecordAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/src/core/service/active_tab_service.dart b/lib/src/core/service/active_tab_service.dart new file mode 100644 index 0000000..11757e4 --- /dev/null +++ b/lib/src/core/service/active_tab_service.dart @@ -0,0 +1,22 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/state/tabs.dart'; + +class ActiveTabService { + final Ref ref; + + ActiveTabService(this.ref); + + Tabs? getActiveTabGroup() { + return getActiveTab()?.parent; + } + + TabItem? getActiveTab() { + return ref.read(tabsProvider).activeTab.value; + } +} + +final activeTabServiceProvider = Provider( + name: 'activeTabServiceProvider', + (ref) => ActiveTabService(ref), +); diff --git a/lib/src/core/service/tabs_service.dart b/lib/src/core/service/tabs_service.dart new file mode 100644 index 0000000..bea04ba --- /dev/null +++ b/lib/src/core/service/tabs_service.dart @@ -0,0 +1,58 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/core/fs.dart'; +import 'package:terminal_studio/src/core/plugin.dart'; +import 'package:terminal_studio/src/core/service/active_tab_service.dart'; +import 'package:terminal_studio/src/core/state/plugin.dart'; +import 'package:terminal_studio/src/plugins/terminal/terminal_plugin.dart'; +import 'package:terminal_studio/src/ui/tabs/code_editor_tab.dart'; +import 'package:terminal_studio/src/ui/tabs/plugin_tab.dart'; + +class TabsService { + final Ref ref; + + TabsService(this.ref); + + void openTerminal(HostSpec hostSpec, {Tabs? tabs, bool activate = true}) { + return openPlugin(hostSpec, TerminalPlugin(), + tabs: tabs, activate: activate); + } + + void openPlugin( + HostSpec host, + Plugin plugin, { + Tabs? tabs, + bool activate = true, + }) { + openTab( + PluginTab(plugin, ref.read(pluginManagerProvider(host))), + tabs: tabs, + activate: activate, + ); + } + + void openFile(File file, {Tabs? tabs, bool activate = true}) { + openTab(CodeEditorTab(file), tabs: tabs, activate: activate); + } + + void openTab(TabItem tab, {Tabs? tabs, bool activate = true}) { + final targetTabGroup = + tabs ?? ref.read(activeTabServiceProvider).getActiveTabGroup(); + + if (targetTabGroup == null) { + return; + } + + targetTabGroup.add(tab); + + if (activate) { + tab.activate(); + } + } +} + +final tabsServiceProvider = Provider( + name: 'tabsServiceProvider', + (ref) => TabsService(ref), +); diff --git a/lib/src/core/service/window_service.dart b/lib/src/core/service/window_service.dart new file mode 100644 index 0000000..52e4198 --- /dev/null +++ b/lib/src/core/service/window_service.dart @@ -0,0 +1,18 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowService { + Future createWindow() async { + // final executable = Platform.resolvedExecutable; + // await Process.start(executable, [], mode: ProcessStartMode.detached); + } + + Future setTitle(String title) async { + await windowManager.setTitle(title); + } +} + +final windowServiceProvider = Provider( + name: 'WindowService', + (ref) => WindowService(), +); diff --git a/lib/src/core/state/database.dart b/lib/src/core/state/database.dart new file mode 100644 index 0000000..e19d83a --- /dev/null +++ b/lib/src/core/state/database.dart @@ -0,0 +1,29 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:terminal_studio/src/core/record/ssh_host_record.dart'; +import 'package:terminal_studio/src/core/record/ssh_key_record.dart'; + +final hiveProvider = FutureProvider((ref) async { + await Hive.initFlutter('.TerminalStudio'); + return Hive; +}); + +// typeId: 0 +final sshHostBoxProvider = FutureProvider>((ref) async { + final hive = await ref.watch(hiveProvider.future); + hive.registerAdapter(SSHHostRecordAdapter()); + return hive.openBox('ssh_hosts'); +}); + +final sshHostsProvider = FutureProvider>((ref) async { + final box = await ref.watch(sshHostBoxProvider.future); + box.watch().listen((event) => ref.invalidateSelf()); + return box.values.toList(); +}); + +// typeId: 1 +final sshKeyBoxProvider = FutureProvider>((ref) async { + final hive = await ref.watch(hiveProvider.future); + hive.registerAdapter(SSHKeyRecordAdapter()); + return hive.openBox('ssh_keys'); +}); diff --git a/lib/src/core/state/host.dart b/lib/src/core/state/host.dart new file mode 100644 index 0000000..4b8f798 --- /dev/null +++ b/lib/src/core/state/host.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/core/host.dart'; + +final connectorProvider = Provider.family( + name: 'connectorProvider', + (ref, HostSpec config) => config.createConnector(), +); + +final connectorStatusProvider = + StateNotifierProvider.family( + name: 'connectorStatusProvider', + (ref, HostSpec config) => ref.watch(connectorProvider(config)), +); + +final hostProvider = Provider.family( + name: 'hostProvider', + (ref, spec) { + ref.watch(connectorStatusProvider(spec)); + return ref.watch(connectorProvider(spec)).host; + }, +); diff --git a/lib/src/core/state/plugin.dart b/lib/src/core/state/plugin.dart new file mode 100644 index 0000000..714553d --- /dev/null +++ b/lib/src/core/state/plugin.dart @@ -0,0 +1,34 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/core/plugin.dart'; +import 'package:terminal_studio/src/core/state/host.dart'; + +final pluginManagerProvider = Provider.family( + name: 'pluginManagerProvider', + (ref, spec) { + final manager = PluginManager(spec); + + ref.listen( + hostProvider(spec), + (last, current) { + if (last == null && current != null) { + manager.didConnected(current); + } + + if (last != null && current == null) { + manager.didDisconnected(); + } + }, + fireImmediately: true, + ); + + ref.listen( + connectorStatusProvider(spec), + (last, current) { + manager.didConnectionStatusChanged(current); + }, + ); + + return manager; + }, +); diff --git a/lib/src/core/state/tabs.dart b/lib/src/core/state/tabs.dart new file mode 100644 index 0000000..371a0d8 --- /dev/null +++ b/lib/src/core/state/tabs.dart @@ -0,0 +1,6 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final tabsProvider = Provider((ref) { + return TabsDocument(); +}); diff --git a/lib/src/hosts/local_conn.dart b/lib/src/hosts/local_conn.dart new file mode 100644 index 0000000..e293fa3 --- /dev/null +++ b/lib/src/hosts/local_conn.dart @@ -0,0 +1,11 @@ +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/hosts/local_host.dart'; + +class LocalConnector extends HostConnector { + LocalConnector(); + + @override + Future createHost() async { + return LocalHost(); + } +} diff --git a/lib/src/hosts/local_fs.dart b/lib/src/hosts/local_fs.dart new file mode 100644 index 0000000..aa716b1 --- /dev/null +++ b/lib/src/hosts/local_fs.dart @@ -0,0 +1,334 @@ +import 'dart:io' as io; +import 'dart:typed_data'; + +import 'package:path/path.dart' as p; +import 'package:terminal_studio/src/core/fs.dart'; + +class LocalFileSystem extends FileSystem { + @override + Directory get currentDirectory { + return LocalDirectory(this, io.Directory.current); + } + + @override + Directory directory(String path) { + return LocalDirectory(this, io.Directory(path)); + } + + @override + File file(String path) { + return LocalFile(this, io.File(path)); + } + + @override + Future identical(String path1, String path2) { + return io.FileSystemEntity.identical(path1, path2); + } + + @override + bool get isWatchSupported => io.FileSystemEntity.isWatchSupported; + + @override + Link link(String path) { + return LocalLink(this, io.Link(path)); + } + + @override + p.Context get path => p.Context(); + + @override + Future stat(String path) async { + final stat = await io.FileStat.stat(path); + return LocalFileStat(stat); + } + + @override + Future type( + String path, { + bool followLinks = true, + }) async { + final type = await io.FileSystemEntity.type(path, followLinks: followLinks); + + if (type == io.FileSystemEntityType.notFound) { + throw FileSystemException('Not found', path); + } + + switch (type) { + case io.FileSystemEntityType.directory: + return FileSystemEntityType.directory; + case io.FileSystemEntityType.file: + return FileSystemEntityType.file; + case io.FileSystemEntityType.link: + return FileSystemEntityType.link; + default: + return FileSystemEntityType.unknown; + } + } +} + +class LocalFileStat implements FileStat { + final io.FileStat _stat; + + LocalFileStat(this._stat); + + @override + int get size => _stat.size; + + @override + DateTime get accessed => _stat.accessed; + + @override + DateTime get changed => _stat.changed; + + @override + DateTime get modified => _stat.modified; + + @override + FileSystemEntityType get type { + switch (_stat.type) { + case io.FileSystemEntityType.directory: + return FileSystemEntityType.directory; + case io.FileSystemEntityType.file: + return FileSystemEntityType.file; + case io.FileSystemEntityType.link: + return FileSystemEntityType.link; + default: + return FileSystemEntityType.unknown; + } + } +} + +mixin LocalFileSystemEntity { + io.FileSystemEntity get delegate; + + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }) { + return delegate.watch(events: events, recursive: recursive).map((event) { + if (event is io.FileSystemCreateEvent) { + return FileSystemEvent(FileSystemEvent.create, event.path, true); + } else if (event is io.FileSystemModifyEvent) { + return FileSystemEvent(FileSystemEvent.modify, event.path, true); + } else if (event is io.FileSystemDeleteEvent) { + return FileSystemEvent(FileSystemEvent.delete, event.path, true); + } else if (event is io.FileSystemMoveEvent) { + return FileSystemEvent(FileSystemEvent.move, event.path, true); + } else { + throw StateError('Unknown event type: $event'); + } + }); + } + + Future exists() async { + return await delegate.exists(); + } + + late final cachedStat = LocalFileStat(delegate.statSync()); +} + +class LocalDirectory extends Directory with LocalFileSystemEntity { + @override + final FileSystem fileSystem; + + @override + final io.Directory delegate; + + LocalDirectory(this.fileSystem, this.delegate); + + @override + String get path => delegate.path; + + @override + Future get absolute async => + LocalDirectory(fileSystem, delegate.absolute); + + @override + Future create({bool recursive = false}) async { + final newDelegate = await delegate.create(recursive: recursive); + return LocalDirectory(fileSystem, newDelegate); + } + + @override + Future delete({bool recursive = false}) async { + final newDelegate = await delegate.delete(recursive: recursive); + return LocalDirectory(fileSystem, newDelegate as io.Directory); + } + + @override + Stream list({ + bool recursive = false, + bool followLinks = true, + }) { + return delegate + .list( + recursive: recursive, + followLinks: followLinks, + ) + .map((entity) { + if (entity is io.File) { + return LocalFile(fileSystem, entity); + } else if (entity is io.Directory) { + return LocalDirectory(fileSystem, entity); + } else if (entity is io.Link) { + return LocalLink(fileSystem, entity); + } else { + throw StateError('Unknown entity type: $entity'); + } + }); + } + + @override + Future rename(String newPath) async { + final newDelegate = await delegate.rename(newPath); + return LocalDirectory(fileSystem, newDelegate); + } +} + +class LocalFile extends File with LocalFileSystemEntity { + @override + final FileSystem fileSystem; + + @override + final io.File delegate; + + LocalFile(this.fileSystem, this.delegate); + + @override + String get path => delegate.path; + + @override + Future get absolute async => + LocalFile(fileSystem, delegate.absolute); + + @override + Future create({bool recursive = false}) async { + final newDelegate = await delegate.create(recursive: recursive); + return LocalFile(fileSystem, newDelegate); + } + + @override + Future delete({bool recursive = false}) async { + final newDelegate = await delegate.delete(recursive: recursive); + return LocalFile(fileSystem, newDelegate as io.File); + } + + @override + Future stat() async { + final stat = await delegate.stat(); + return LocalFileStat(stat); + } + + @override + Future rename(String newPath) async { + final newDelegate = await delegate.rename(newPath); + return LocalFile(fileSystem, newDelegate); + } + + @override + Future copy(String newPath) async { + final newDelegate = await delegate.copy(newPath); + return LocalFile(fileSystem, newDelegate); + } + + @override + Future readAsBytes() { + return delegate.readAsBytes(); + } + + @override + Future readAsString() { + return delegate.readAsString(); + } + + @override + Future writeAsBytes( + Uint8List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }) async { + final newDelegate = await delegate.writeAsBytes( + bytes, + mode: _toIoFileMode(mode), + flush: flush, + ); + return LocalFile(fileSystem, newDelegate); + } + + @override + Future writeAsString( + String contents, { + FileMode mode = FileMode.write, + bool flush = false, + }) async { + final newDelegate = await delegate.writeAsString( + contents, + mode: _toIoFileMode(mode), + flush: flush, + ); + return LocalFile(fileSystem, newDelegate); + } + + io.FileMode _toIoFileMode(FileMode mode) { + switch (mode) { + case FileMode.append: + return io.FileMode.append; + case FileMode.write: + return io.FileMode.write; + case FileMode.writeOnly: + return io.FileMode.writeOnly; + case FileMode.writeOnlyAppend: + return io.FileMode.writeOnlyAppend; + case FileMode.read: + return io.FileMode.read; + default: + throw StateError('Unknown file mode: $mode'); + } + } +} + +class LocalLink extends Link with LocalFileSystemEntity { + @override + final FileSystem fileSystem; + + @override + final io.Link delegate; + + LocalLink(this.fileSystem, this.delegate); + + @override + String get path => delegate.path; + + @override + Future get absolute async => + LocalLink(fileSystem, delegate.absolute); + + @override + Future create(String target, {bool recursive = false}) async { + final newDelegate = await delegate.create(target, recursive: recursive); + return LocalLink(fileSystem, newDelegate); + } + + @override + Future delete({bool recursive = false}) async { + final newDelegate = await delegate.delete(recursive: recursive); + return LocalLink(fileSystem, newDelegate as io.Link); + } + + @override + Future rename(String newPath) async { + final newDelegate = await delegate.rename(newPath); + return LocalLink(fileSystem, newDelegate); + } + + @override + Future update(String target) async { + final newDelegate = await delegate.update(target); + return LocalLink(fileSystem, newDelegate); + } + + @override + Future target() async { + return await delegate.target(); + } +} diff --git a/lib/src/hosts/local_host.dart b/lib/src/hosts/local_host.dart new file mode 100644 index 0000000..a73fd85 --- /dev/null +++ b/lib/src/hosts/local_host.dart @@ -0,0 +1,118 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_pty/flutter_pty.dart'; +import 'package:terminal_studio/src/core/fs.dart'; +import 'package:terminal_studio/src/core/host.dart'; +import 'package:terminal_studio/src/hosts/local_fs.dart'; + +class LocalHost implements Host { + @override + Future execute( + String executable, { + List args = const [], + bool root = false, + Map? environment, + }) async { + final result = + await Process.run(executable, args, environment: environment); + return LocalExecutionResult(result); + } + + @override + Future connectFileSystem() async { + return LocalFileSystem(); + } + + @override + Future shell({ + int width = 80, + int height = 25, + Map? environment, + }) async { + final shell = _platformShell; + final pty = Pty.start( + shell.command, + arguments: shell.args, + environment: {...Platform.environment, ...environment ?? {}}, + rows: height, + columns: width, + ); + return LocalExecutionSession(pty); + } + + final _doneCompleter = Completer(); + + @override + Future disconnect() async { + _doneCompleter.complete(); + } + + @override + Future get done => _doneCompleter.future; +} + +class LocalExecutionResult implements ExecutionResult { + final ProcessResult _result; + + LocalExecutionResult(this._result); + + @override + int get exitCode => _result.exitCode; + + @override + String get stderr => _result.stderr; + + @override + String get stdout => _result.stdout; +} + +class LocalExecutionSession implements ExecutionSession { + final Pty _pty; + + LocalExecutionSession(this._pty); + + @override + Future get exitCode => _pty.exitCode; + + @override + Stream get output => _pty.output; + + @override + Future close() async { + _pty.kill(); + } + + @override + Future resize(int width, int height) async { + _pty.resize(height, width); + } + + @override + Future write(Uint8List data) async { + _pty.write(data); + } +} + +class _ShellCommand { + final String command; + + final List args; + + _ShellCommand(this.command, this.args); +} + +_ShellCommand get _platformShell { + if (Platform.isMacOS) { + final user = Platform.environment['USER']; + return _ShellCommand('login', ['-fp', user!]); + } + + if (Platform.isWindows) { + return _ShellCommand('powershell.exe', []); + } + + final shell = Platform.environment['SHELL'] ?? 'sh'; + return _ShellCommand(shell, []); +} diff --git a/lib/src/hosts/local_spec.dart b/lib/src/hosts/local_spec.dart new file mode 100644 index 0000000..cdd8be4 --- /dev/null +++ b/lib/src/hosts/local_spec.dart @@ -0,0 +1,10 @@ +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/hosts/local_conn.dart'; + +class LocalHostSpec implements HostSpec { + @override + final name = 'Local'; + + @override + HostConnector createConnector() => LocalConnector(); +} diff --git a/lib/src/hosts/ssh_conn.dart b/lib/src/hosts/ssh_conn.dart new file mode 100644 index 0000000..afcdc0b --- /dev/null +++ b/lib/src/hosts/ssh_conn.dart @@ -0,0 +1,41 @@ +import 'package:dartssh2/dartssh2.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/hosts/ssh_host.dart'; +import 'package:terminal_studio/src/core/record/ssh_host_record.dart'; + +class SSHConnector extends HostConnector { + final SSHHostRecord record; + + SSHConnector(this.record); + + @override + Future createHost() async { + final socket = await AsyncValue.guard( + () => SSHSocket.connect( + record.host, + record.port, + ), + ); + + if (socket.hasError) { + final error = socket.error!; + throw 'Failed to connect: $error'; + } + + final client = SSHClient( + socket.value!, + username: record.username!, + onPasswordRequest: () => record.password, + ); + + final authenticated = await AsyncValue.guard(() => client.authenticated); + + if (authenticated.hasError) { + final error = authenticated.error!; + throw 'Failed to authenticate: $error'; + } + + return SSHHost(client); + } +} diff --git a/lib/src/hosts/ssh_fs.dart b/lib/src/hosts/ssh_fs.dart new file mode 100644 index 0000000..f126c62 --- /dev/null +++ b/lib/src/hosts/ssh_fs.dart @@ -0,0 +1,366 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dartssh2/dartssh2.dart'; +import 'package:path/path.dart' as p; +import 'package:terminal_studio/src/core/fs.dart'; + +class SSHFileSystem extends FileSystem { + final SftpClient client; + + SSHFileSystem(this.client, String currentDirectory) { + this.currentDirectory = directory(currentDirectory); + } + + @override + late final SSHDirectory currentDirectory; + + @override + SSHDirectory directory(String path) { + return SSHDirectory(this, path); + } + + @override + File file(String path) { + return SSHFile(this, path); + } + + @override + Link link(String path) { + return SSHLink(this, path); + } + + @override + Future identical(String path1, String path2) async { + final absolute1 = await client.absolute(path1); + final absolute2 = await client.absolute(path2); + return absolute1 == absolute2; + } + + @override + final bool isWatchSupported = false; + + @override + p.Context get path => p.Context(style: p.Style.posix); + + @override + Future stat(String path) async { + final stat = await client.stat(path); + return SSHFileStat(stat); + } + + @override + Future type( + String path, { + bool followLinks = true, + }) async { + final stat = await client.stat(path); + return _toFileSystemEntityType(stat.type); + } +} + +FileSystemEntityType _toFileSystemEntityType(SftpFileType? type) { + switch (type) { + case SftpFileType.directory: + return FileSystemEntityType.directory; + case SftpFileType.regularFile: + case SftpFileType.blockDevice: + case SftpFileType.characterDevice: + case SftpFileType.whiteout: + return FileSystemEntityType.file; + case SftpFileType.symbolicLink: + return FileSystemEntityType.link; + case SftpFileType.pipe: + return FileSystemEntityType.pipe; + case SftpFileType.socket: + return FileSystemEntityType.socket; + case SftpFileType.unknown: + case null: + return FileSystemEntityType.unknown; + } +} + +class SSHFileStat implements FileStat { + final SftpFileAttrs stat; + + SSHFileStat(this.stat); + + @override + DateTime get accessed => + DateTime.fromMillisecondsSinceEpoch((stat.accessTime ?? 0) * 1000); + + @override + DateTime get changed => modified; + + @override + DateTime get modified => + DateTime.fromMillisecondsSinceEpoch((stat.modifyTime ?? 0) * 1000); + + @override + int get size => stat.size ?? 0; + + @override + FileSystemEntityType get type => _toFileSystemEntityType(stat.type); +} + +class SSHDirectory extends Directory { + @override + final SSHFileSystem fileSystem; + + @override + final String path; + + @override + final SSHFileStat? cachedStat; + + SSHDirectory(this.fileSystem, this.path, [this.cachedStat]); + + SftpClient get client => fileSystem.client; + + @override + Future get absolute async { + final absolute = await fileSystem.client.absolute(path); + return SSHDirectory(fileSystem, absolute); + } + + @override + Future create({bool recursive = false}) async { + await client.mkdir(path); + return this; + } + + @override + Future delete({bool recursive = false}) async { + await client.rmdir(path); + return this; + } + + @override + Future exists() async { + try { + final stat = await client.stat(path); + return stat.type == SftpFileType.directory; + } catch (e) { + return false; + } + } + + @override + Stream list({ + bool recursive = false, + bool followLinks = true, + }) async* { + await for (final chunk in client.readdir(path)) { + for (final file in chunk) { + final path = fileSystem.path.join(this.path, file.filename); + if (file.attr.isDirectory) { + yield SSHDirectory(fileSystem, path, SSHFileStat(file.attr)); + } else if (file.attr.isSymbolicLink) { + yield SSHLink(fileSystem, path, SSHFileStat(file.attr)); + } else { + yield SSHFile(fileSystem, path, SSHFileStat(file.attr)); + } + } + } + } + + @override + Future rename(String newPath) async { + await client.rename(path, newPath); + return SSHDirectory(fileSystem, newPath); + } + + @override + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }) { + throw FileSystemException('Watch on SSHDirectory is not supported', path); + } +} + +class SSHFile extends File { + @override + final SSHFileSystem fileSystem; + + @override + final String path; + + @override + final SSHFileStat? cachedStat; + + SSHFile(this.fileSystem, this.path, [this.cachedStat]); + + SftpClient get client => fileSystem.client; + + @override + Future get absolute async { + final absolute = await fileSystem.client.absolute(path); + return SSHFile(fileSystem, absolute); + } + + @override + Future create({bool recursive = false}) async { + final file = await client.open(path, mode: SftpFileOpenMode.create); + await file.close(); + return this; + } + + @override + Future delete({bool recursive = false}) async { + await client.remove(path); + return this; + } + + @override + Future exists() async { + try { + final stat = await client.stat(path); + return stat.type != SftpFileType.directory && + stat.type != SftpFileType.symbolicLink; + } catch (e) { + return false; + } + } + + @override + Future rename(String newPath) async { + await client.rename(path, newPath); + return SSHFile(fileSystem, newPath); + } + + @override + Future copy(String newPath) async { + final file1 = await client.open(path); + final file2 = await client.open(newPath, mode: SftpFileOpenMode.create); + await file2.write(file1.read()); + return SSHFile(fileSystem, newPath); + } + + @override + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }) { + throw FileSystemException('Watch on SSHFile is not supported', path); + } + + @override + Future readAsBytes() async { + final file = await client.open(path); + final bytes = await file.readBytes(); + await file.close(); + return bytes; + } + + @override + Future readAsString() async { + final bytes = await readAsBytes(); + return utf8.decode(bytes, allowMalformed: true); + } + + @override + Future writeAsBytes( + Uint8List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }) async { + final file = await client.open(path, mode: _toSftpFileMode(mode)); + await file.writeBytes(bytes); + await file.close(); + return this; + } + + @override + Future writeAsString( + String contents, { + FileMode mode = FileMode.write, + bool flush = false, + }) async { + final encoded = const Utf8Encoder().convert(contents); + return await writeAsBytes(encoded, mode: mode, flush: flush); + } + + static SftpFileOpenMode _toSftpFileMode(FileMode mode) { + switch (mode) { + case FileMode.append: + return SftpFileOpenMode.append; + case FileMode.write: + return SftpFileOpenMode.write | SftpFileOpenMode.read; + case FileMode.writeOnly: + return SftpFileOpenMode.write; + case FileMode.writeOnlyAppend: + return SftpFileOpenMode.write | SftpFileOpenMode.append; + case FileMode.read: + return SftpFileOpenMode.read; + } + } +} + +class SSHLink extends Link { + @override + final SSHFileSystem fileSystem; + + @override + final String path; + + @override + final SSHFileStat? cachedStat; + + SSHLink(this.fileSystem, this.path, [this.cachedStat]); + + SftpClient get client => fileSystem.client; + + @override + Future get absolute async { + final absolute = await fileSystem.client.absolute(path); + return SSHLink(fileSystem, absolute); + } + + @override + Future create(String target, {bool recursive = false}) async { + await client.link(path, target); + return this; + } + + @override + Future rename(String newPath) async { + await client.rename(path, newPath); + return SSHLink(fileSystem, newPath); + } + + @override + Future update(String target) async { + await client.link(path, target); + return this; + } + + @override + Future delete({bool recursive = false}) async { + await client.remove(path); + return this; + } + + @override + Future exists() async { + try { + final stat = await client.stat(path); + return stat.type == SftpFileType.symbolicLink; + } catch (e) { + return false; + } + } + + @override + Future target() async { + return await client.readlink(path); + } + + @override + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }) { + throw FileSystemException('Watch on SSHLink is not supported', path); + } +} diff --git a/lib/src/hosts/ssh_host.dart b/lib/src/hosts/ssh_host.dart new file mode 100644 index 0000000..d1cf6cb --- /dev/null +++ b/lib/src/hosts/ssh_host.dart @@ -0,0 +1,125 @@ +import 'dart:typed_data'; + +import 'package:dartssh2/dartssh2.dart'; +import 'package:terminal_studio/src/core/fs.dart'; +import 'package:terminal_studio/src/core/host.dart'; +import 'package:terminal_studio/src/hosts/ssh_fs.dart'; + +class SSHHost implements Host { + SSHHost(this.client); + + final SSHClient client; + + @override + Future execute( + String executable, { + List args = const [], + bool root = false, + Map? environment, + }) async { + final command = [executable, ...args].join(' '); + final result = await client.execute(command, environment: environment); + return _collectResult(result); + } + + @override + Future connectFileSystem() async { + final sftp = await client.sftp(); + final currentDirectory = await sftp.absolute('.'); + return SSHFileSystem(sftp, currentDirectory); + } + + @override + Future shell({ + int width = 80, + int height = 25, + Map? environment, + }) async { + final session = await client.shell( + environment: environment, + pty: SSHPtyConfig( + height: height, + width: width, + ), + ); + return _SSHExecutionSession(session); + } + + @override + Future disconnect() async { + client.close(); + } + + @override + Future get done => client.done; +} + +Future<_SSHExecutionResult> _collectResult(SSHSession session) async { + final stdout = StringBuffer(); + final stderr = StringBuffer(); + + final stdoutStream = session.stdout.listen( + (data) => stdout.write(String.fromCharCodes(data)), + ); + + final stderrStream = session.stderr.listen( + (data) => stderr.write(String.fromCharCodes(data)), + ); + + await session.done; + await stdoutStream.asFuture(); + await stderrStream.asFuture(); + + return _SSHExecutionResult( + exitCode: session.exitCode ?? 0, + stdout: stdout.toString(), + stderr: stderr.toString(), + ); +} + +class _SSHExecutionResult implements ExecutionResult { + _SSHExecutionResult({ + required this.exitCode, + required this.stdout, + required this.stderr, + }); + + @override + final int exitCode; + + @override + final String stderr; + + @override + final String stdout; +} + +class _SSHExecutionSession implements ExecutionSession { + _SSHExecutionSession(this.session); + + final SSHSession session; + + @override + Future close() async { + session.close(); + } + + @override + Future resize(int width, int height) async { + session.resizeTerminal(width, height); + } + + @override + Stream get output => session.stdout; + + @override + Future write(Uint8List data) async { + session.stdin.add(data); + } + + @override + Future get exitCode async { + await session.done; + return session.exitCode ?? 0; + } +} diff --git a/lib/src/plugins/file_manager/file_manager_plugin.dart b/lib/src/plugins/file_manager/file_manager_plugin.dart new file mode 100644 index 0000000..4ff6701 --- /dev/null +++ b/lib/src/plugins/file_manager/file_manager_plugin.dart @@ -0,0 +1,320 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/fs.dart'; +import 'package:terminal_studio/src/core/plugin.dart'; +import 'package:terminal_studio/src/core/service/tabs_service.dart'; +import 'package:terminal_studio/src/plugins/file_manager/navigation_breadcrumbs.dart'; +import 'package:terminal_studio/src/plugins/file_manager/navigation_stack.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; + +class FileManagerPlugin with Plugin { + late FileSystem fs; + + String? homePath; + + final currentPath = ValueNotifier(null); + + String? get currentDirectory => + currentPath.value == null ? null : fs.path.basename(currentPath.value!); + + final files = ValueNotifier([]); + + late final navigationStack = NavigationStack(onNavigate: _onNavigate); + + final _filesCache = >{}; + + Future _fetchFiles() async { + final path = currentPath.value; + + if (path == null) { + return; + } + + this.files.value = _filesCache[path] ?? []; + + final files = await fs.directory(path).list().fold( + [], + (files, file) => [...files, file], + ); + + _filesCache[path] = files; + + if (currentPath.value == path) { + this.files.value = files; + } + } + + Future _onNavigate(String path) async { + currentPath.value = fs.path.normalize(path); + title.value = + currentDirectory == null ? null : currentDirectory! + fs.path.separator; + _fetchFiles(); + } + + Future goto(String target) async { + late final String path; + + if (fs.path.isAbsolute(target)) { + path = target; + } else { + path = fs.path.normalize(fs.path.join(currentPath.value!, target)); + } + + navigationStack.push(path); + } + + List get breadcrumbs { + final path = currentPath.value; + if (path == null) return []; + final parts = fs.path.split(path); + return parts; + } + + bool get canGoUp => breadcrumbs.length > 1; + + Future goUp() async { + if (!canGoUp) return; + await goto('..'); + } + + Future goHome() async { + if (homePath == null) return; + await goto(homePath!); + } + + @override + void didMounted() { + title.value = 'Files'; + } + + @override + void didConnected() async { + fs = await host.connectFileSystem(); + + homePath = (await fs.directory('.').absolute).path; + + goto(homePath!); + } + + @override + Widget build(BuildContext context) { + return NavigationView( + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 40, child: FileListToolbar(this)), + Expanded(child: FileListView(this)), + const Divider(), + SizedBox(height: 30, child: FileListNavigator(this)), + ], + ), + ); + } +} + +class FileListToolbar extends StatelessWidget { + const FileListToolbar(this.plugin, {super.key}); + + final FileManagerPlugin plugin; + + NavigationStack get stack => plugin.navigationStack; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: plugin.currentPath, + builder: (context, currentPath, _) => _buildToolbar(context), + ); + } + + Widget _buildToolbar(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + IconButton( + icon: const Icon(FluentIcons.back), + onPressed: stack.canGoBack ? stack.back : null, + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(FluentIcons.forward), + onPressed: stack.canGoForward ? stack.forward : null, + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(FluentIcons.up), + onPressed: plugin.canGoUp ? plugin.goUp : null, + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(FluentIcons.home), + onPressed: plugin.homePath == null ? null : plugin.goHome, + ), + const SizedBox(width: 16), + const Divider(direction: Axis.vertical), + const SizedBox(width: 16), + Expanded(child: Text(plugin.currentDirectory ?? '')), + const SizedBox(width: 8), + IconButton( + icon: const Icon(FluentIcons.refresh), + onPressed: () => plugin._fetchFiles(), + ), + ], + ), + ); + } +} + +class FileListNavigator extends StatelessWidget { + const FileListNavigator(this.plugin, {super.key}); + + final FileManagerPlugin plugin; + + NavigationStack get stack => plugin.navigationStack; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ValueListenableBuilder( + valueListenable: plugin.currentPath, + builder: (context, currentPath, _) { + return NavigationBreadcrumbs( + breadcrumbs: plugin.breadcrumbs, + onTap: (breadcrumbs) { + plugin.goto(plugin.fs.path.joinAll(breadcrumbs)); + }, + ); + }, + ), + ); + } +} + +class FileListView extends ConsumerStatefulWidget { + const FileListView(this.plugin, {super.key}); + + final FileManagerPlugin plugin; + + @override + FileListViewState createState() => FileListViewState(); +} + +class FileListViewState extends ConsumerState { + FileManagerPlugin get plugin => widget.plugin; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: plugin.files, + builder: (context, files, child) { + return _buildTable(context, files); + }, + ); + } + + Widget _buildTable(BuildContext context, List files) { + final source = _FilesDataSource(files); + return SfDataGrid( + headerRowHeight: 40, + allowSorting: true, + allowFiltering: true, + horizontalScrollPhysics: const NeverScrollableScrollPhysics(), + onCellTap: (details) { + final rowIndex = details.rowColumnIndex.rowIndex; + + if (rowIndex == 0) return; + + final row = source.effectiveRows[rowIndex - 1] as _FileDataGridRow; + + final file = row.file; + + if (file is Directory) { + plugin.goto(file.path); + } else if (file is File) { + ref.read(tabsServiceProvider).openFile(file); + } + }, + columns: [ + GridColumn( + columnName: 'name', + label: Container( + padding: const EdgeInsets.all(8), + child: const Text('Name'), + ), + columnWidthMode: ColumnWidthMode.fill, + allowFiltering: true, + ), + GridColumn( + columnName: 'date', + label: Container( + padding: const EdgeInsets.all(8), + child: const Text('Date'), + ), + minimumWidth: 210, + allowFiltering: false, + ), + ], + source: source, + ); + } +} + +class _FilesDataSource extends DataGridSource { + _FilesDataSource(this.files); + + final List files; + + @override + late final rows = files + .where((file) => file.basename != '.' && file.basename != '..') + .map((file) => _FileDataGridRow(file)) + .toList(); + + @override + DataGridRowAdapter buildRow(covariant _FileDataGridRow row) { + return DataGridRowAdapter( + cells: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 8), + Icon( + row.file is Directory + ? FluentIcons.folder + : FluentIcons.file_code, + size: 16, + ), + Container( + padding: const EdgeInsets.all(8), + child: Text(row.name), + ), + ], + ), + Container( + padding: const EdgeInsets.all(8), + child: Text(row.date), + ), + ], + ); + } +} + +class _FileDataGridRow implements DataGridRow { + _FileDataGridRow(this.file); + + final FileSystemEntity file; + + late final name = file.basename; + + late final date = '${file.cachedStat?.modified ?? ''}'; + + @override + List getCells() { + return [ + DataGridCell(columnName: 'name', value: name), + DataGridCell(columnName: 'date', value: date), + ]; + } +} diff --git a/lib/src/plugins/file_manager/navigation_breadcrumbs.dart b/lib/src/plugins/file_manager/navigation_breadcrumbs.dart new file mode 100644 index 0000000..4e13dae --- /dev/null +++ b/lib/src/plugins/file_manager/navigation_breadcrumbs.dart @@ -0,0 +1,75 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class NavigationBreadcrumbs extends StatelessWidget { + const NavigationBreadcrumbs({ + super.key, + required this.breadcrumbs, + this.onTap, + }); + + final List breadcrumbs; + + final void Function(List breadcrumbs)? onTap; + + @override + Widget build(BuildContext context) { + final widgets = []; + + for (var i = 0; i < breadcrumbs.length; i++) { + final breadcrumb = breadcrumbs[i]; + + widgets.add( + BreadcrumbButton( + breadcrumb: breadcrumb, + onPressed: () => onTap?.call(breadcrumbs.sublist(0, i + 1)), + ), + ); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: widgets, + ), + ), + ); + } +} + +class BreadcrumbButton extends StatelessWidget { + const BreadcrumbButton({ + super.key, + this.onPressed, + required this.breadcrumb, + this.isPrimary = false, + }); + + final String breadcrumb; + + final bool isPrimary; + + final void Function()? onPressed; + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: onPressed, + child: Text(breadcrumb), + ); + } +} + +class BreadcrumbSeprator extends StatelessWidget { + const BreadcrumbSeprator({super.key}); + + @override + Widget build(BuildContext context) { + return const Icon( + FluentIcons.chevron_right, + size: 8, + // color: CupertinoColors.secondaryLabel, + ); + } +} diff --git a/lib/src/plugins/file_manager/navigation_stack.dart b/lib/src/plugins/file_manager/navigation_stack.dart new file mode 100644 index 0000000..a457774 --- /dev/null +++ b/lib/src/plugins/file_manager/navigation_stack.dart @@ -0,0 +1,55 @@ +import 'package:flutter/widgets.dart'; + +class NavigationStack with ChangeNotifier { + final void Function(T item)? onNavigate; + + NavigationStack({this.onNavigate}); + + final List _stack = []; + + /// Pointer to the current position in the stack. -1 if the stack is empty. + var _current = -1; + + /// The current path in the stack. + T? get current => _current >= 0 ? _stack[_current] : null; + + /// Weather calling [back] will have any effect. + bool get canGoBack => _current > 0; + + /// Weather calling [forward] will have any effect. + bool get canGoForward => _current < _stack.length - 1; + + /// Pushes a new path to the stack at the current position, clears the stack + /// after the current position. + void push(T path) { + if (_current < _stack.length - 1) { + _stack.removeRange(_current + 1, _stack.length); + } + + _stack.add(path); + _current = _stack.length - 1; + + onNavigate?.call(path); + notifyListeners(); + } + + /// Navigates back in the stack. + void back() { + if (_current > 0) { + _current--; + + onNavigate?.call(_stack[_current]); + notifyListeners(); + } + } + + /// Navigates forward in the stack. + void forward() { + if (_current < _stack.length - 1) { + _current++; + + onNavigate?.call(_stack[_current]); + notifyListeners(); + } + } +} diff --git a/lib/src/plugins/starter/starter_plugin.dart b/lib/src/plugins/starter/starter_plugin.dart new file mode 100644 index 0000000..8757c7f --- /dev/null +++ b/lib/src/plugins/starter/starter_plugin.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/plugin.dart'; + +class StarterPlugin extends Plugin { + final _uptime = ValueNotifier(null); + + Future _updateUptime() async { + final result = await AsyncValue.guard(() => host.execute('uptime')); + + result.when( + data: (data) => _uptime.value = data.stdout, + loading: () => _uptime.value = 'Loading...', + error: (error, stackTrace) => _uptime.value = 'Error: $error', + ); + } + + Future _startUpdate() async { + while (connected) { + await _updateUptime(); + await Future.delayed(const Duration(seconds: 1)); + } + } + + @override + void didMounted() { + title.value = 'Uptime'; + super.didMounted(); + } + + @override + void didConnected() { + _startUpdate(); + super.didConnected(); + } + + @override + void didDisconnected() { + _uptime.value = 'Disconnected'; + super.didDisconnected(); + } + + @override + Widget build(BuildContext context) { + return NavigationView( + pane: NavigationPane( + displayMode: PaneDisplayMode.top, + selected: 0, + items: [ + PaneItem( + icon: const Icon(FluentIcons.server), + title: const Text('Uptime'), + body: Center( + child: ValueListenableBuilder( + valueListenable: _uptime, + builder: (context, value, child) { + return Text(value ?? 'Waiting...'); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/plugins/terminal/terminal_menu.dart b/lib/src/plugins/terminal/terminal_menu.dart new file mode 100644 index 0000000..7bd1311 --- /dev/null +++ b/lib/src/plugins/terminal/terminal_menu.dart @@ -0,0 +1,164 @@ +import 'package:context_menus/context_menus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/service/tabs_service.dart'; +import 'package:terminal_studio/src/plugins/file_manager/file_manager_plugin.dart'; +import 'package:terminal_studio/src/plugins/starter/starter_plugin.dart'; +import 'package:terminal_studio/src/plugins/terminal/terminal_plugin.dart'; +import 'package:xterm/xterm.dart'; + +class TerminalContextMenu extends ConsumerStatefulWidget { + const TerminalContextMenu({ + super.key, + required this.plugin, + }); + + final TerminalPlugin plugin; + + @override + TerminalContextMenuState createState() => TerminalContextMenuState(); +} + +class TerminalContextMenuState extends ConsumerState + with ContextMenuStateMixin { + TerminalPlugin get plugin => widget.plugin; + + Terminal get terminal => plugin.terminal; + + TerminalController get terminalController => plugin.terminalController; + + @override + void initState() { + terminalController.addListener(_onSelectionChanged); + super.initState(); + } + + @override + void dispose() { + terminalController.removeListener(_onSelectionChanged); + super.dispose(); + } + + @override + void didUpdateWidget(covariant TerminalContextMenu oldWidget) { + if (oldWidget.plugin.terminalController != + widget.plugin.terminalController) { + oldWidget.plugin.terminalController.removeListener(_onSelectionChanged); + widget.plugin.terminalController.addListener(_onSelectionChanged); + } + super.didUpdateWidget(oldWidget); + } + + void _onSelectionChanged() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return cardBuilder( + context, + [ + buttonBuilder( + context, + ContextMenuButtonConfig( + "Copy", + icon: const Icon(Icons.copy), + shortcutLabel: 'Ctrl+C', + onPressed: terminalController.selection != null + ? () => handlePressed(context, _handleCopy) + : null, + ), + ), + buttonBuilder( + context, + ContextMenuButtonConfig( + "Paste", + icon: const Icon(Icons.paste), + shortcutLabel: 'Ctrl+V', + onPressed: () => handlePressed(context, _handlePaste), + ), + ), + buttonBuilder( + context, + ContextMenuButtonConfig( + "Select All", + icon: const Icon(Icons.select_all), + shortcutLabel: 'Ctrl+A', + onPressed: () => handlePressed(context, _handleSelectAll), + ), + ), + buildDivider(), + buttonBuilder( + context, + ContextMenuButtonConfig( + "File Manager", + icon: const Icon(Icons.folder_open), + shortcutLabel: 'Ctrl+Shift+F', + onPressed: () => handlePressed(context, _handleOpenFileManager), + ), + ), + buttonBuilder( + context, + ContextMenuButtonConfig( + "Uptime", + icon: const Icon(Icons.keyboard_double_arrow_up_outlined), + // shortcutLabel: 'Ctrl+Shift+F', + onPressed: () => handlePressed(context, _handleStarterPlugin), + ), + ), + ], + ); + } + + Future _handleCopy() async { + final selection = terminalController.selection; + + if (selection == null) { + return; + } + + final text = terminal.buffer.getText(selection); + + await Clipboard.setData(ClipboardData(text: text)); + } + + Future _handlePaste() async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + + if (data == null) { + return; + } + + final text = data.text; + + if (text == null) { + return; + } + + terminal.paste(text); + } + + Future _handleSelectAll() async { + terminalController.setSelection( + BufferRangeLine( + CellOffset(0, terminal.buffer.height - terminal.viewHeight), + CellOffset(terminal.viewWidth, terminal.buffer.height - 1), + ), + ); + } + + Future _handleOpenFileManager() async { + ref.read(tabsServiceProvider).openPlugin( + plugin.hostSpec, + FileManagerPlugin(), + ); + } + + Future _handleStarterPlugin() async { + ref.read(tabsServiceProvider).openPlugin( + plugin.hostSpec, + StarterPlugin(), + ); + } +} diff --git a/lib/src/plugins/terminal/terminal_plugin.dart b/lib/src/plugins/terminal/terminal_plugin.dart new file mode 100644 index 0000000..9ed37f3 --- /dev/null +++ b/lib/src/plugins/terminal/terminal_plugin.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; + +import 'package:context_menus/context_menus.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart' show Colors; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/conn.dart'; +import 'package:terminal_studio/src/core/host.dart'; +import 'package:terminal_studio/src/core/plugin.dart'; +import 'package:terminal_studio/src/plugins/terminal/terminal_menu.dart'; +import 'package:xterm/xterm.dart'; + +class TerminalPlugin extends Plugin { + final terminal = Terminal(maxLines: 10000); + + final terminalController = TerminalController(); + + var terminalTitle = ''; + + ExecutionSession? session; + + void _updateTitle() { + if (session != null) { + title.value = + '$terminalTitle — ${terminal.viewWidth}x${terminal.viewHeight}'; + } + } + + @override + void didMounted() { + title.value = 'Connecting'; + + terminal.onTitleChange = (title) { + terminalTitle = title; + _updateTitle(); + }; + + terminal.onOutput = (data) { + session?.write(const Utf8Encoder().convert(data)); + }; + + terminal.onResize = (w, h, pw, ph) { + session?.resize(w, h); + SchedulerBinding.instance.addPostFrameCallback((_) { + _updateTitle(); + }); + }; + + super.didMounted(); + } + + @override + void didConnected() async { + title.value = 'Terminal'; + + session = await host.shell( + width: terminal.viewWidth, + height: terminal.viewHeight, + ); + + session!.output + .cast>() + .transform(const Utf8Decoder()) + .listen(terminal.write); + + session!.exitCode.then((code) { + session = null; + if (mounted) { + manager.remove(this); + } + }); + } + + @override + void didDisconnected() { + session = null; + title.value = 'Disconnected'; + } + + @override + void onConnectionStatus(HostConnectorStatus status) { + switch (status) { + case HostConnectorStatus.connecting: + title.value = 'Connecting'; + break; + case HostConnectorStatus.connected: + title.value = 'Terminal'; + break; + case HostConnectorStatus.disconnected: + title.value = 'Disconnected'; + break; + default: + } + } + + @override + Widget build(BuildContext context) { + return TerminalTabView(this); + } +} + +class TerminalTabView extends ConsumerStatefulWidget { + const TerminalTabView(this.plugin, {super.key}); + + final TerminalPlugin plugin; + + @override + ConsumerState createState() => + _TerminalTabViewState(); +} + +class _TerminalTabViewState extends ConsumerState { + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + key: ValueKey(widget.plugin), + backgroundColor: Colors.transparent, + child: SafeArea( + child: ClipRect( + child: TerminalView( + widget.plugin.terminal, + controller: widget.plugin.terminalController, + onSecondaryTapDown: (_, __) => showMenu(), + backgroundOpacity: 0.8, + autofocus: true, + ), + ), + ), + ); + } + + void showMenu() { + final menu = TerminalContextMenu(plugin: widget.plugin); + context.contextMenuOverlay.show(menu); + } +} diff --git a/lib/src/ui/context_menu.dart b/lib/src/ui/context_menu.dart new file mode 100644 index 0000000..94025dc --- /dev/null +++ b/lib/src/ui/context_menu.dart @@ -0,0 +1,96 @@ +import 'package:context_menus/context_menus.dart'; +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/hosts/local_spec.dart'; +import 'package:terminal_studio/src/core/service/tabs_service.dart'; +import 'package:terminal_studio/src/core/state/database.dart'; +import 'package:terminal_studio/src/ui/tabs/add_host_tab.dart'; +import 'package:terminal_studio/src/ui/tabs/settings_tab/settings_tab.dart'; +import 'package:terminal_studio/src/util/tabs_extension.dart'; + +class DropdownContextMenu extends ConsumerStatefulWidget { + const DropdownContextMenu(this.tabs, {super.key}); + + final Tabs tabs; + + @override + DropdownContextMenuState createState() => DropdownContextMenuState(); +} + +class DropdownContextMenuState extends ConsumerState + with ContextMenuStateMixin { + Tabs get tabs => widget.tabs; + + @override + Widget build(BuildContext context) { + return cardBuilder( + context, + [ + buttonBuilder( + context, + ContextMenuButtonConfig( + 'Local', + icon: const Icon(FluentIcons.tablet), + onPressed: () => handlePressed(context, () { + final tabsService = ref.read(tabsServiceProvider); + tabsService.openTerminal(LocalHostSpec()); + }), + ), + ), + ...buildHosts(), + buttonBuilder( + context, + ContextMenuButtonConfig( + 'Add New', + icon: const Icon(FluentIcons.add), + onPressed: () => handlePressed( + context, + () => ref.openTab(AddHostTab()), + ), + ), + ), + buildDivider(), + buttonBuilder( + context, + ContextMenuButtonConfig( + 'Settings', + icon: const Icon(FluentIcons.settings), + onPressed: () => handlePressed( + context, + () => ref.openTab(SettingsTab()), + ), + ), + ), + ], + ); + } + + List buildHosts() { + final sshHosts = ref.watch(sshHostBoxProvider).asData; + + if (sshHosts == null || sshHosts.value.isEmpty) { + return []; + } + + final items = []; + + for (final host in sshHosts.value.values) { + items.add( + buttonBuilder( + context, + ContextMenuButtonConfig( + host.name, + icon: const Icon(FluentIcons.cloud), + onPressed: () => handlePressed(context, () async { + final tabsService = ref.read(tabsServiceProvider); + tabsService.openTerminal(host, tabs: tabs); + }), + ), + ), + ); + } + + return items; + } +} diff --git a/lib/src/ui/pages/host_edit_page.dart b/lib/src/ui/pages/host_edit_page.dart new file mode 100644 index 0000000..e65b55a --- /dev/null +++ b/lib/src/ui/pages/host_edit_page.dart @@ -0,0 +1,244 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/record/ssh_host_record.dart'; +import 'package:terminal_studio/src/core/state/database.dart'; +import 'package:terminal_studio/src/ui/shared/fluent_back_button.dart'; +import 'package:terminal_studio/src/ui/shared/fluent_form.dart'; +import 'package:terminal_studio/src/ui/shared/fluent_navigator.dart'; +import 'package:terminal_studio/src/util/validators.dart'; + +class HostEditPage extends ConsumerStatefulWidget { + const HostEditPage({super.key, this.record}); + + final SSHHostRecord? record; + + @override + ConsumerState createState() => _HostEditDialogState(); +} + +class _HostEditDialogState extends ConsumerState { + bool get isEditing => widget.record != null; + + @override + Widget build(BuildContext context) { + return NavigationView( + appBar: NavigationAppBar( + title: Text(isEditing ? 'Edit Host' : 'Add Host'), + leading: + Navigator.of(context).canPop() ? const FluentBackButton() : null, + actions: FluentNavigatorCommandBar( + primaryItems: [ + if (isEditing) + CommandBarButton( + icon: const Icon(FluentIcons.delete), + label: const Text('Delete'), + onPressed: () async { + if (widget.record != null) { + await widget.record!.delete(); + } + close(); + }, + ), + ], + ), + ), + pane: NavigationPane( + displayMode: PaneDisplayMode.top, + selected: 0, + items: [ + PaneItem( + icon: const Icon(FluentIcons.server), + title: const Text('Host'), + body: SSHHostEditForm( + record: widget.record, + onSaved: _onSaved, + ), + ), + ], + ), + // content: SSHHostEditForm( + // record: widget.record, + // onSaved: _onSaved, + // ), + ); + } + + Future _onSaved(record) async { + final box = await ref.read(sshHostBoxProvider.future); + if (record.isInBox) { + record.save(); + } else { + box.add(record); + } + close(); + } + + void close() { + if (mounted) { + if (Navigator.of(context).canPop()) { + return Navigator.of(context).pop(); + } + + if (TabScope.of(context) != null) { + return TabScope.of(context)!.dispose(); + } + } + } +} + +class SSHHostEditForm extends ConsumerStatefulWidget { + const SSHHostEditForm({super.key, this.record, this.onSaved}); + + final SSHHostRecord? record; + + final void Function(SSHHostRecord record)? onSaved; + + @override + ConsumerState createState() => _HostEditFormState(); +} + +class _HostEditFormState extends ConsumerState { + final formKey = GlobalKey(); + + late final record = widget.record ?? SSHHostRecord.uninitialized(); + + @override + Widget build(BuildContext context) { + Widget widget = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const FluentFormHeader('Protocol'), + ComboboxFormField( + value: 'ssh', + items: const [ + ComboBoxItem( + value: 'ssh', + child: Text('SSH'), + ), + ], + onChanged: (value) {}, + ), + const FluentFormDivider(), + TextFormBox( + header: 'Label', + initialValue: record.name, + onSaved: (value) => record.name = value!, + ), + ], + ), + ), + const FluentFormSeparator(), + Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormBox( + header: 'Host', + initialValue: record.host, + placeholder: 'example.com / 1.2.3.4', + validator: (value) { + if (value == null || value.isEmpty) return 'Host is required'; + return isHostOrIP(value) ? null : 'Invalid host or IP'; + }, + onSaved: (value) => record.host = value!, + ), + const FluentFormDivider(), + TextFormBox( + header: 'Port', + initialValue: record.port.toString(), + validator: (value) { + if (value == null || value.isEmpty) return 'Port is required'; + return isPort(value) ? null : 'Invalid port'; + }, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + onSaved: (value) => record.port = int.parse(value!), + ), + const FluentFormDivider(), + TextFormBox( + header: 'User', + initialValue: record.username, + placeholder: 'root', + onSaved: (value) => record.username = value, + ), + const FluentFormDivider(), + TextFormBox( + header: 'Password', + placeholder: '', + initialValue: record.password, + obscureText: true, + onSaved: (value) => record.password = value, + ), + ], + ), + ), + const FluentFormSeparator(), + // Card( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // TextFormBox( + // header: 'Private Key', + // placeholder: '', + // ), + // const FluentFormDivider(), + // TextFormBox( + // header: 'Passphrase', + // placeholder: '', + // ), + // ], + // ), + // ), + Card( + child: Row( + children: [ + FilledButton( + onPressed: _submitForm, + child: const Text('Save'), + ), + const SizedBox(width: 8), + Button( + child: const Text('Test Connection'), + onPressed: () {}, + ), + ], + ), + ), + ], + ); + + widget = Form( + key: formKey, + child: widget, + ); + + widget = Container( + alignment: Alignment.center, + child: SizedBox( + width: 500, + child: widget, + ), + ); + + widget = SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: widget, + ); + + return widget; + } + + void _submitForm() { + if (formKey.currentState!.validate()) { + formKey.currentState!.save(); + widget.onSaved?.call(record); + } + } +} diff --git a/lib/src/ui/platform_menu.dart b/lib/src/ui/platform_menu.dart new file mode 100644 index 0000000..3b6a58a --- /dev/null +++ b/lib/src/ui/platform_menu.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/service/active_tab_service.dart'; +import 'package:terminal_studio/src/ui/shortcuts.dart' as shortcuts; +import 'package:terminal_studio/src/ui/tabs/devtools_tab.dart'; +import 'package:terminal_studio/src/ui/tabs/settings_tab/settings_tab.dart'; +import 'package:terminal_studio/src/util/tabs_extension.dart'; + +class GlobalPlatformMenu extends ConsumerStatefulWidget { + const GlobalPlatformMenu({super.key, required this.child}); + + final Widget child; + + @override + ConsumerState createState() => _GlobalPlatformMenuState(); +} + +class _GlobalPlatformMenuState extends ConsumerState { + @override + Widget build(BuildContext context) { + return PlatformMenuBar( + menus: [ + PlatformMenu( + label: 'TerminalStudio', + menus: [ + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.about)) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.about, + ), + PlatformMenuItemGroup( + members: [ + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.servicesSubmenu)) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.servicesSubmenu, + ), + ], + ), + PlatformMenuItemGroup( + members: [ + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.hide)) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.hide, + ), + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.hideOtherApplications)) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.hideOtherApplications, + ), + ], + ), + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.quit)) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.quit, + ), + ], + ), + PlatformMenu( + label: 'Edit', + menus: [ + PlatformMenuItemGroup( + members: [ + PlatformMenuItem( + label: 'Copy', + shortcut: shortcuts.terminalCopy, + onSelected: () { + final primaryContext = primaryFocus?.context; + if (primaryContext == null) { + return; + } + Actions.invoke( + primaryContext, + CopySelectionTextIntent.copy, + ); + }, + ), + PlatformMenuItem( + label: 'Paste', + shortcut: shortcuts.terminalPaste, + onSelected: () { + final primaryContext = primaryFocus?.context; + if (primaryContext == null) { + return; + } + Actions.invoke( + primaryContext, + const PasteTextIntent(SelectionChangedCause.keyboard), + ); + }, + ), + PlatformMenuItem( + label: 'Select All', + shortcut: shortcuts.terminalSelectAll, + onSelected: () { + final primaryContext = primaryFocus?.context; + if (primaryContext == null) { + return; + } + try { + Actions.maybeFind( + primaryContext, + intent: const SelectAllTextIntent( + SelectionChangedCause.keyboard), + ); + } catch (e, st) { + print(e); + print(st); + } + Actions.invoke( + primaryContext, + const SelectAllTextIntent(SelectionChangedCause.keyboard), + ); + }, + ), + ], + ), + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.quit)) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.quit), + ], + ), + PlatformMenu( + label: 'View', + menus: [ + PlatformMenuItemGroup( + members: [ + PlatformMenuItem( + label: 'Close Tab', + shortcut: shortcuts.tabClose, + onSelected: () { + return ref + .read(activeTabServiceProvider) + .getActiveTab() + ?.detach(); + }, + ), + if (PlatformProvidedMenuItem.hasMenu( + PlatformProvidedMenuItemType.toggleFullScreen)) + const PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.toggleFullScreen), + ], + ), + PlatformMenuItem( + label: 'Settings', + shortcut: shortcuts.openSettings, + onSelected: () => ref.openTab(SettingsTab()), + ), + PlatformMenuItem( + label: 'DevTools', + shortcut: shortcuts.openDevTools, + onSelected: () => ref.openTab(DevToolsTab()), + ), + ], + ), + ], + child: widget.child, + ); + } +} diff --git a/lib/src/ui/shared/fluent_back_button.dart b/lib/src/ui/shared/fluent_back_button.dart new file mode 100644 index 0000000..cd6cda9 --- /dev/null +++ b/lib/src/ui/shared/fluent_back_button.dart @@ -0,0 +1,28 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class FluentBackButton extends StatelessWidget { + const FluentBackButton({super.key, this.onPressed}); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return PaneItem( + icon: const Icon(FluentIcons.back, size: 14.0), + body: const SizedBox.shrink(), + ).build( + context, + false, + () => _onPressed(context), + displayMode: PaneDisplayMode.compact, + ); + } + + void _onPressed(BuildContext context) { + if (onPressed != null) { + onPressed!(); + } else { + Navigator.of(context).maybePop(); + } + } +} diff --git a/lib/src/ui/shared/fluent_form.dart b/lib/src/ui/shared/fluent_form.dart new file mode 100644 index 0000000..55e36ef --- /dev/null +++ b/lib/src/ui/shared/fluent_form.dart @@ -0,0 +1,41 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class FluentFormDivider extends StatelessWidget { + const FluentFormDivider({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: const [ + SizedBox(height: 8), + Divider(), + SizedBox(height: 8), + ], + ); + } +} + +class FluentFormHeader extends StatelessWidget { + const FluentFormHeader(this.header, {super.key}); + + final String header; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(header), + const SizedBox(height: 8), + ], + ); + } +} + +class FluentFormSeparator extends StatelessWidget { + const FluentFormSeparator({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox(height: 8); + } +} diff --git a/lib/src/ui/shared/fluent_menu_card.dart b/lib/src/ui/shared/fluent_menu_card.dart new file mode 100644 index 0000000..9f6fb68 --- /dev/null +++ b/lib/src/ui/shared/fluent_menu_card.dart @@ -0,0 +1,58 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class FluentMenuCard extends StatelessWidget { + const FluentMenuCard({ + Key? key, + required this.children, + this.borderRadius, + this.bgColor, + this.border, + this.shadows, + this.padding, + }) : super(key: key); + + final List children; + final Border? border; + final BorderRadius? borderRadius; + final Color? bgColor; + final List? shadows; + final EdgeInsets? padding; + + @override + Widget build(BuildContext context) { + final shadowColor = FluentTheme.of(context).shadowColor; + final radius = borderRadius ?? BorderRadius.circular(4); + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 250), + child: ClipRRect( + borderRadius: radius, + child: Container( + padding: padding ?? const EdgeInsets.symmetric(vertical: 5), + decoration: BoxDecoration( + color: bgColor ?? FluentTheme.of(context).menuColor, + border: border ?? Border.all(color: Colors.grey[100]), + borderRadius: radius, + boxShadow: shadows ?? + [ + BoxShadow( + color: shadowColor.withOpacity(.05), + blurRadius: 4, + offset: const Offset(2, 2), + ), + BoxShadow( + color: shadowColor.withOpacity(.02), + blurRadius: 2, + offset: const Offset(2, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ), + ), + ); + } +} diff --git a/lib/src/ui/shared/fluent_navigator.dart b/lib/src/ui/shared/fluent_navigator.dart new file mode 100644 index 0000000..e69da60 --- /dev/null +++ b/lib/src/ui/shared/fluent_navigator.dart @@ -0,0 +1,19 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class FluentNavigatorCommandBar extends StatelessWidget { + const FluentNavigatorCommandBar({super.key, required this.primaryItems}); + + final List primaryItems; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(right: 8), + child: CommandBar( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + primaryItems: primaryItems, + ), + ); + } +} diff --git a/lib/src/ui/shared/macos_form.dart b/lib/src/ui/shared/macos_form.dart new file mode 100644 index 0000000..03ce7e3 --- /dev/null +++ b/lib/src/ui/shared/macos_form.dart @@ -0,0 +1,85 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class MacosFormRow extends StatelessWidget { + static const labelWidth = 100.0; + + final Widget? label; + + final Widget child; + + final double spaceBetween; + + const MacosFormRow({ + super.key, + this.label, + required this.child, + this.spaceBetween = 8.0, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + children: [ + if (label != null) + Container( + width: labelWidth, + alignment: Alignment.centerRight, + child: label, + ), + if (label != null) + SizedBox( + width: spaceBetween, + ), + Expanded( + child: child, + ), + ], + ), + ); + } +} + +class MacosTextFormRow extends ConsumerWidget { + final Widget? label; + + final String? placeholder; + + final bool obscureText; + + final void Function(String)? onChanged; + + final TextEditingController? controller; + + const MacosTextFormRow({ + super.key, + this.label, + this.placeholder, + this.obscureText = false, + this.onChanged, + this.controller, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MacosFormRow( + label: label, + spaceBetween: 6.0, + child: MacosTextField( + placeholder: placeholder, + obscureText: obscureText, + decoration: kDefaultRoundedBorderDecoration.copyWith( + borderRadius: BorderRadius.circular(2), + ), + focusedDecoration: kDefaultFocusedBorderDecoration.copyWith( + borderRadius: BorderRadius.circular(5), + ), + onChanged: onChanged, + controller: controller, + ), + ); + } +} diff --git a/lib/src/ui/shared/macos_titlebar.dart b/lib/src/ui/shared/macos_titlebar.dart new file mode 100644 index 0000000..842822c --- /dev/null +++ b/lib/src/ui/shared/macos_titlebar.dart @@ -0,0 +1,49 @@ +import 'package:flutter/widgets.dart'; +import 'package:window_manager/window_manager.dart'; + +const kMacosTitlebarHeight = 28.0; + +class MacosTitlebar extends StatefulWidget { + const MacosTitlebar({super.key, required this.color}); + + final Color color; + + @override + State createState() => _MacosTitlebarState(); +} + +class _MacosTitlebarState extends State with WindowListener { + var fullScreen = false; + + @override + void onWindowEnterFullScreen() { + setState(() => fullScreen = true); + super.onWindowEnterFullScreen(); + } + + @override + void onWindowLeaveFullScreen() { + setState(() => fullScreen = false); + super.onWindowLeaveFullScreen(); + } + + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: fullScreen ? 0 : kMacosTitlebarHeight, + color: widget.color, + ); + } +} diff --git a/lib/src/ui/shortcut/global_actions.dart b/lib/src/ui/shortcut/global_actions.dart new file mode 100644 index 0000000..6816e5a --- /dev/null +++ b/lib/src/ui/shortcut/global_actions.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/service/window_service.dart'; +import 'package:terminal_studio/src/ui/shortcut/intents.dart'; + +class GlobalActions extends ConsumerWidget { + const GlobalActions({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Actions( + actions: { + NewWindowIntent: CallbackAction( + onInvoke: (NewWindowIntent intent) async { + await ref.read(windowServiceProvider).createWindow(); + return null; + }, + ), + }, + child: child, + ); + } +} diff --git a/lib/src/ui/shortcut/global_shortcuts.dart b/lib/src/ui/shortcut/global_shortcuts.dart new file mode 100644 index 0000000..2c5a9f4 --- /dev/null +++ b/lib/src/ui/shortcut/global_shortcuts.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:terminal_studio/src/ui/shortcut/intents.dart'; +import 'package:terminal_studio/src/ui/shortcuts.dart' as shortcuts; + +class GlobalShortcuts extends StatelessWidget { + const GlobalShortcuts({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Shortcuts( + shortcuts: { + shortcuts.openNewWindow: const NewWindowIntent(), + }, + child: child, + ); + } +} diff --git a/lib/src/ui/shortcut/intents.dart b/lib/src/ui/shortcut/intents.dart new file mode 100644 index 0000000..6d546c4 --- /dev/null +++ b/lib/src/ui/shortcut/intents.dart @@ -0,0 +1,5 @@ +import 'package:flutter/widgets.dart'; + +class NewWindowIntent extends Intent { + const NewWindowIntent(); +} diff --git a/lib/src/ui/shortcuts.dart b/lib/src/ui/shortcuts.dart new file mode 100644 index 0000000..a2728bc --- /dev/null +++ b/lib/src/ui/shortcuts.dart @@ -0,0 +1,86 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:terminal_studio/src/util/target_platform.dart'; + +SingleActivator get openSettings { + return defaultTargetPlatform.isApple + ? const SingleActivator( + LogicalKeyboardKey.comma, + meta: true, + ) + : const SingleActivator( + LogicalKeyboardKey.comma, + control: true, + ); +} + +SingleActivator get openDevTools { + return const SingleActivator( + LogicalKeyboardKey.f12, + ); +} + +SingleActivator get openNewWindow { + return defaultTargetPlatform.isApple + ? const SingleActivator( + LogicalKeyboardKey.keyN, + meta: true, + ) + : const SingleActivator( + LogicalKeyboardKey.keyN, + control: true, + ); +} + +SingleActivator get tabClose { + return defaultTargetPlatform.isApple + ? const SingleActivator( + LogicalKeyboardKey.keyW, + meta: true, + ) + : const SingleActivator( + LogicalKeyboardKey.keyW, + meta: true, + shift: true, + ); +} + +SingleActivator get terminalCopy { + return defaultTargetPlatform.isApple + ? const SingleActivator( + LogicalKeyboardKey.keyC, + meta: true, + ) + : const SingleActivator( + LogicalKeyboardKey.keyC, + control: true, + shift: true, + ); +} + +SingleActivator get terminalPaste { + return defaultTargetPlatform.isApple + ? const SingleActivator( + LogicalKeyboardKey.keyV, + meta: true, + ) + : const SingleActivator( + LogicalKeyboardKey.keyV, + control: true, + shift: true, + ); +} + +SingleActivator get terminalSelectAll { + return defaultTargetPlatform.isApple + ? const SingleActivator( + LogicalKeyboardKey.keyA, + meta: true, + ) + : const SingleActivator( + LogicalKeyboardKey.keyA, + control: true, + shift: true, + ); +} diff --git a/lib/src/ui/tabs/add_host_tab.dart b/lib/src/ui/tabs/add_host_tab.dart new file mode 100644 index 0000000..c197266 --- /dev/null +++ b/lib/src/ui/tabs/add_host_tab.dart @@ -0,0 +1,151 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/ui/pages/host_edit_page.dart'; + +class AddHostTab extends TabItem { + AddHostTab() { + title.value = const Text('Connect'); + content.value = const AddHostTabView(); + } +} + +class AddHostTabView extends ConsumerStatefulWidget { + const AddHostTabView({super.key}); + + @override + ConsumerState createState() => _AddHostTabViewState(); +} + +class _AddHostTabViewState extends ConsumerState { + @override + Widget build(BuildContext context) { + return const HostEditPage(); + // return Container( + // constraints: const BoxConstraints.expand(), + // color: const Color.fromARGB(255, 245, 245, 245), + // alignment: Alignment.center, + // child: Container( + // constraints: const BoxConstraints.tightFor(width: 550), + // child: const AddHostForm(), + // ), + // ); + } +} + +// class AddHostForm extends ConsumerStatefulWidget { +// const AddHostForm({Key? key}) : super(key: key); + +// @override +// ConsumerState createState() => _AddHostFormState(); +// } + +// class _AddHostFormState extends ConsumerState { +// final formLabel = TextEditingController(); +// final formHost = TextEditingController(); +// final formPort = TextEditingController(); +// final formUsername = TextEditingController(); +// final formPassword = TextEditingController(); + +// @override +// Widget build(BuildContext context) { +// return ListView( +// padding: const EdgeInsets.all(32), +// children: [ +// MacosFormRow( +// label: const Text('Protocol'), +// child: Container( +// constraints: const BoxConstraints.tightFor(width: 200), +// alignment: Alignment.centerLeft, +// child: const MacosPulldownButton( +// title: 'SSH', +// items: [ +// MacosPulldownMenuItem(title: Text('SSH')), +// ], +// ), +// ), +// ), +// MacosTextFormRow( +// label: const Text('Label:'), +// placeholder: 'Optional', +// controller: formLabel, +// ), +// MacosTextFormRow( +// label: const Text('Host:'), +// placeholder: 'example.com / 1.2.3.4', +// controller: formHost, +// ), +// MacosTextFormRow( +// label: const Text('Port:'), +// placeholder: '22', +// controller: formPort, +// ), +// MacosTextFormRow( +// label: const Text('User:'), +// placeholder: 'root', +// controller: formUsername, +// ), +// MacosTextFormRow( +// label: const Text('Password:'), +// obscureText: true, +// placeholder: '', +// controller: formPassword, +// ), +// const SizedBox(height: 8), +// MacosFormRow( +// child: Row( +// mainAxisAlignment: MainAxisAlignment.end, +// children: [ +// PushButton( +// buttonSize: ButtonSize.small, +// onPressed: submit, +// child: const Text('Connect'), +// ) +// ], +// ), +// ), +// ], +// ); +// } + +// Future submit() async { +// final sshHosts = await ref.read(sshHostBoxProvider.future); +// await sshHosts.add( +// SSHHostRecord( +// name: formLabel.text, +// host: formHost.text, +// port: int.parse(formPort.text), +// username: formUsername.text, +// password: formPassword.text, +// ), +// ); + +// await alert('Success', 'Host added successfully'); + +// closeTab(); +// } + +// Future alert(String title, String message) async { +// await showMacosAlertDialog( +// context: context, +// builder: (context) { +// return MacosAlertDialog( +// appIcon: const FlutterLogo(), +// title: Text(title), +// message: Text(message), +// primaryButton: PushButton( +// buttonSize: ButtonSize.small, +// onPressed: () { +// Navigator.of(context).pop(); +// }, +// child: const Text('OK'), +// ), +// ); +// }, +// ); +// } + +// void closeTab() { +// TabScope.of(context)?.dispose(); +// } +// } diff --git a/lib/src/ui/tabs/code_editor_tab.dart b/lib/src/ui/tabs/code_editor_tab.dart new file mode 100644 index 0000000..e884957 --- /dev/null +++ b/lib/src/ui/tabs/code_editor_tab.dart @@ -0,0 +1,116 @@ +// Import the language & theme +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_highlight/themes/github.dart'; +import 'package:highlight/highlight.dart'; +import 'package:code_text_field/code_text_field.dart'; +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:highlight/languages/all.dart'; +import 'package:terminal_studio/src/core/fs.dart'; + +class CodeEditorTab extends TabItem { + final File file; + + CodeEditorTab(this.file) { + title.value = Text(file.basename); + content.value = CodeEditorView(this); + loadContent(); + } + + final codeController = ValueNotifier(null); + + Future loadContent() async { + final content = await file.readAsString(); + codeController.value = CodeController( + text: content, + language: _detectLanguage(content), + theme: githubTheme, + ); + } +} + +class CodeEditorView extends StatelessWidget { + const CodeEditorView(this.tab, {super.key}); + + final CodeEditorTab tab; + + static const fontFamily = 'SourceCode'; + + static const fontFamilyFallback = [ + 'Menlo', // macos + 'Consolas', // windows + 'monospace', + ]; + + @override + Widget build(BuildContext context) { + return NavigationView( + content: Column( + children: [ + _buildToolbar(), + const Divider(), + Expanded(child: _buildEditor()), + ], + ), + ); + } + + Widget _buildToolbar() { + return Padding( + padding: const EdgeInsets.all(8), + child: CommandBar( + mainAxisAlignment: MainAxisAlignment.end, + primaryItems: [ + // CommandBarButton( + // icon: const Icon(FluentIcons.back), + // label: const Text('Back'), + // onPressed: () {}, + // ), + CommandBarButton( + icon: const Icon(FluentIcons.save), + label: const Text('Save'), + onPressed: () async { + final controller = tab.codeController.value; + if (controller == null) return; + await tab.file.writeAsString(controller.value.text); + }, + ), + ], + ), + ); + } + + Widget _buildEditor() { + return ValueListenableBuilder( + valueListenable: tab.codeController, + builder: (context, codeController, child) { + if (codeController == null) { + return const Center( + child: ProgressRing(), + ); + } + + return SingleChildScrollView( + child: CodeField( + controller: codeController, + lineNumberStyle: const LineNumberStyle( + textStyle: TextStyle( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + ), + textStyle: const TextStyle( + fontFamily: fontFamily, + fontSize: 12, + fontFamilyFallback: fontFamilyFallback, + ), + ), + ); + }, + ); + } +} + +Mode _detectLanguage(String source) { + final result = highlight.parse(source, autoDetection: true); + return allLanguages[result.language]!; +} diff --git a/lib/src/ui/tabs/devtools_tab.dart b/lib/src/ui/tabs/devtools_tab.dart new file mode 100644 index 0000000..d342eae --- /dev/null +++ b/lib/src/ui/tabs/devtools_tab.dart @@ -0,0 +1,95 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:terminal_studio/src/core/state/database.dart'; +import 'package:terminal_studio/src/ui/tabs/playground.dart'; +import 'package:xterm/xterm.dart'; + +class DevToolsTab extends TabItem { + DevToolsTab() { + title.value = const Text('DevTools'); + content.value = DevToolsTabView(this); + } + + final terminal = Terminal(); +} + +class DevToolsTabView extends ConsumerStatefulWidget { + const DevToolsTabView(this.tab, {super.key}); + + final DevToolsTab tab; + + @override + ConsumerState createState() => + _DevToolsTabViewState(); +} + +class _DevToolsTabViewState extends ConsumerState { + DevToolsTab get tab => widget.tab; + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + child: Container( + constraints: const BoxConstraints.expand(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + children: [ + PushButton( + buttonSize: ButtonSize.large, + onPressed: _openAddHostTab, + child: const Text('Add SSH host'), + ), + PushButton( + buttonSize: ButtonSize.large, + onPressed: _clearHosts, + child: const Text('Clear SSH hosts'), + ), + PushButton( + buttonSize: ButtonSize.large, + onPressed: () => tab.replace(PlaygroundTab()), + child: const Text('Playground'), + ), + ], + ), + + const SizedBox(height: 16), + Expanded( + child: TerminalView(tab.terminal), + ), + // const PlayGround(), + ], + ), + ), + ); + } + + void _openAddHostTab() { + // ref.openTab(AddHostTab()); + } + + void _clearHosts() async { + final sshHosts = await ref.read(sshHostBoxProvider.future); + await sshHosts.clear(); + tab.terminal.write('Cleared SSH hosts\r\n'); + } +} + +class PlayGround extends ConsumerWidget { + const PlayGround({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + constraints: const BoxConstraints(maxWidth: 500), + padding: const EdgeInsets.all(16), + color: const Color.fromARGB(255, 251, 251, 251), + // child: + ); + } +} diff --git a/lib/src/ui/tabs/playground.dart b/lib/src/ui/tabs/playground.dart new file mode 100644 index 0000000..2766bab --- /dev/null +++ b/lib/src/ui/tabs/playground.dart @@ -0,0 +1,34 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class PlaygroundTab extends TabItem { + PlaygroundTab() { + title.value = const Text('Playground'); + content.value = const PlaygroundView(); + } +} + +class PlaygroundView extends StatefulWidget { + const PlaygroundView({super.key}); + + @override + State createState() => _PlaygroundViewState(); +} + +class _PlaygroundViewState extends State { + var topIndex = 0; + + @override + Widget build(BuildContext context) { + return Stack( + children: const [ + SizedBox( + width: 300, + height: 300, + child: Text('Hello'), + ), + Acrylic(), + ], + ); + } +} diff --git a/lib/src/ui/tabs/plugin_tab.dart b/lib/src/ui/tabs/plugin_tab.dart new file mode 100644 index 0000000..c7bbac5 --- /dev/null +++ b/lib/src/ui/tabs/plugin_tab.dart @@ -0,0 +1,93 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/plugin.dart'; +import 'package:terminal_studio/src/core/state/host.dart'; + +class PluginTab extends TabItem { + final Plugin plugin; + + final PluginManager manager; + + PluginTab(this.plugin, this.manager) { + manager.add(plugin); + + _updateTitle(); + + plugin.title.addListener(_updateTitle); + + manager.addListener(_onPluginManagerChanged); + + content.value = PluginTabView( + key: ValueKey(plugin), + plugin, + ); + } + + @override + void didDispose() { + if (plugin.mounted) { + manager.remove(plugin); + } + plugin.title.removeListener(_updateTitle); + manager.removeListener(_onPluginManagerChanged); + super.didDispose(); + } + + void _updateTitle() { + final titleWidget = plugin.title.value; + title.value = Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (titleWidget != null) + Flexible( + child: Text( + titleWidget, + softWrap: false, + ), + ), + if (titleWidget != null) const SizedBox(width: 4), + Text( + manager.hostSpec.name, + style: const TextStyle( + fontSize: 10, + color: CupertinoColors.systemGrey, + ), + ), + ], + ); + } + + void _onPluginManagerChanged() { + if (!plugin.mounted) { + detach(); + } + } +} + +class PluginTabView extends ConsumerStatefulWidget { + const PluginTabView(this.plugin, {super.key}); + + final Plugin plugin; + + @override + ConsumerState createState() => _PluginTabViewState(); +} + +class _PluginTabViewState extends ConsumerState { + Plugin get plugin => widget.plugin; + + @override + void initState() { + SchedulerBinding.instance!.addPostFrameCallback((_) { + ref.read(connectorProvider(plugin.hostSpec)).connect(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return plugin.build(context); + } +} diff --git a/lib/src/ui/tabs/settings_tab/settings_tab.dart b/lib/src/ui/tabs/settings_tab/settings_tab.dart new file mode 100644 index 0000000..808c4ef --- /dev/null +++ b/lib/src/ui/tabs/settings_tab/settings_tab.dart @@ -0,0 +1,56 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/ui/tabs/settings_tab/settings_tab_hosts.dart'; + +class SettingsTab extends TabItem { + SettingsTab() { + title.value = const Text('Settings'); + content.value = const SettingsView(); + } +} + +class SettingsView extends ConsumerStatefulWidget { + const SettingsView({Key? key}) : super(key: key); + + @override + ConsumerState createState() => _SettingsViewState(); +} + +class _SettingsViewState extends ConsumerState { + var _selectedIndex = 0; + + @override + Widget build(BuildContext context) { + return CupertinoTabView( + builder: (context) => NavigationView( + pane: NavigationPane( + selected: _selectedIndex, + onChanged: (index) { + setState(() => _selectedIndex = index); + }, + displayMode: PaneDisplayMode.open, + size: const NavigationPaneSize( + openWidth: 200, + openMinWidth: 200, + ), + items: [ + PaneItemHeader(header: const Text('Settings')), + PaneItemSeparator(), + PaneItem( + icon: const Icon(FluentIcons.server), + title: const Text('Hosts'), + body: const HostsSettingView(), + ), + PaneItem( + icon: const Icon(FluentIcons.key_phrase_extraction), + title: const Text('SSH keys'), + body: const SizedBox.expand(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/ui/tabs/settings_tab/settings_tab_hosts.dart b/lib/src/ui/tabs/settings_tab/settings_tab_hosts.dart new file mode 100644 index 0000000..c9932b3 --- /dev/null +++ b/lib/src/ui/tabs/settings_tab/settings_tab_hosts.dart @@ -0,0 +1,77 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:terminal_studio/src/core/state/database.dart'; +import 'package:terminal_studio/src/ui/pages/host_edit_page.dart'; + +class HostsSettingView extends ConsumerStatefulWidget { + const HostsSettingView({Key? key}) : super(key: key); + + @override + ConsumerState createState() => + _HostsSettingViewState(); +} + +class _HostsSettingViewState extends ConsumerState { + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: PageHeader( + title: const Text('Hosts'), + commandBar: Expanded( + child: _buildCommandBar(context), + ), + ), + children: [ + _buildSSHHosts(), + ], + ); + } + + Widget _buildCommandBar(BuildContext context) { + return CommandBar( + mainAxisAlignment: MainAxisAlignment.end, + primaryItems: [ + CommandBarButton( + icon: const Icon(FontAwesomeIcons.plus), + label: const Text('Add'), + onPressed: () { + Navigator.of(context).push( + FluentPageRoute( + builder: (context) => const HostEditPage(), + ), + ); + }, + ), + ], + ); + } + + Widget _buildSSHHosts() { + final hosts = ref.watch(sshHostsProvider); + + return hosts.when( + loading: () => const Center(child: ProgressRing()), + error: (e, st) => Text('Error: $e'), + data: (box) => ListView.builder( + shrinkWrap: true, + itemCount: box.length, + itemBuilder: (context, index) { + final record = box[index]; + return ListTile( + title: Text(record.name), + subtitle: Text('${record.host}:${record.port}'), + leading: const FaIcon(FontAwesomeIcons.computer), + onPressed: () { + Navigator.of(context).push( + FluentPageRoute( + builder: (context) => HostEditPage(record: record), + ), + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/src/util/internet_address.dart b/lib/src/util/internet_address.dart new file mode 100644 index 0000000..f4a5d3e --- /dev/null +++ b/lib/src/util/internet_address.dart @@ -0,0 +1,392 @@ +// ------------------------------------------------------------------ +// THIS FILE WAS DERIVED FROM SOURCE CODE UNDER THE FOLLOWING LICENSE +// ------------------------------------------------------------------ +// +// Copyright 2012, the Dart project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// --------------------------------------------------------- +// THIS, DERIVED FILE IS LICENSE UNDER THE FOLLOWING LICENSE +// --------------------------------------------------------- +// Copyright 2020 terrier989@gmail.com. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; + +/// The type, or address family, of an [InternetAddress]. +/// +/// Currently, IP version 4 (IPv4), IP version 6 (IPv6) +/// and Unix domain address are supported. +/// Unix domain sockets are available only on Linux, MacOS and Android. +class InternetAddressType { + static const InternetAddressType ipv4 = InternetAddressType._(0); + static const InternetAddressType ipv6 = InternetAddressType._(1); + static const InternetAddressType unix = InternetAddressType._(2); + static const InternetAddressType any = InternetAddressType._(-1); + + final int _value; + + const InternetAddressType._(this._value); + + // factory InternetAddressType._from(int value) { + // if (value == ipv4._value) return ipv4; + // if (value == ipv6._value) return ipv6; + // if (value == unix._value) return unix; + // throw ArgumentError("Invalid type: $value"); + // } + + /// Get the name of the type, e.g. "IPv4" or "IPv6". + String get name => const ["ANY", "IPv4", "IPv6", "Unix"][_value + 1]; + + @override + String toString() => "InternetAddressType: $name"; +} + +String _stringFromIp(Uint8List bytes) { + switch (bytes.length) { + case 4: + return bytes.map((item) => item.toString()).join('.'); + case 16: + return _stringFromIp6(bytes); + default: + throw ArgumentError.value(bytes); + } +} + +String _stringFromIp6(Uint8List bytes) { + // --------------------------- + // Find longest span of zeroes + // --------------------------- + + // Longest seen span + int? longestStart; + var longestLength = 0; + + // Current span + int? start; + var length = 0; + + // Iterate + for (var i = 0; i < 16; i++) { + if (bytes[i] == 0) { + // Zero byte + if (start == null) { + if (i % 2 == 0) { + // First byte of a span + start = i; + length = 1; + } + } else { + length++; + } + } else if (start != null) { + // End of a span + if (length > longestLength) { + // Longest so far + longestStart = start; + longestLength = length; + } + start = null; + } + } + if (start != null && length > longestLength) { + // End of the longest span + longestStart = start; + longestLength = length; + } + + // Longest length must be a whole group + longestLength -= longestLength % 2; + + // Ignore longest zero span if it's less than 4 bytes. + if (longestLength < 4) { + longestStart = null; + } + + // ---- + // Print + // ----- + final sb = StringBuffer(); + var colon = false; + for (var i = 0; i < 16; i++) { + if (i == longestStart) { + sb.write('::'); + i += longestLength - 1; + colon = false; + continue; + } + final byte = bytes[i]; + if (i % 2 == 0) { + // + // First byte of a group + // + if (colon) { + sb.write(':'); + } else { + colon = true; + } + if (byte != 0) { + sb.write(byte.toRadixString(16)); + } + } else { + // + // Second byte of a group + // + // If this is a single-digit number and the previous byte was non-zero, + // we must add zero + if (byte < 16 && bytes[i - 1] != 0) { + sb.write('0'); + } + sb.write(byte.toRadixString(16)); + } + } + return sb.toString(); +} + +/// Parses IPv4/IPv6 address. +/// +Uint8List? _tryParseRawAddress(String source) { + // Find first '.' or ':' + for (var i = 0; i < source.length; i++) { + final c = source.substring(i, i + 1); + switch (c) { + case ':': + return Uri.parseIPv6Address(source) as Uint8List; + case '.': + return Uri.parseIPv4Address(source) as Uint8List; + } + } + return null; +} + +InternetAddressType _type(String address) { + for (var i = 0; i < address.length; i++) { + final c = address.substring(i, i + 1); + switch (c) { + case ':': + return InternetAddressType.ipv6; + case '.': + return InternetAddressType.ipv4; + } + } + throw ArgumentError.value(address); +} + +/// An internet address or a Unix domain address. +/// +/// This object holds an internet address. If this internet address +/// is the result of a DNS lookup, the address also holds the hostname +/// used to make the lookup. +/// An Internet address combined with a port number represents an +/// endpoint to which a socket can connect or a listening socket can +/// bind. +class InternetAddress { + /// IP version 4 any address. Use this address when listening on + /// all adapters IP addresses using IP version 4 (IPv4). + static final InternetAddress anyIPv4 = InternetAddress('0.0.0.0'); + + /// IP version 6 any address. Use this address when listening on + /// all adapters IP addresses using IP version 6 (IPv6). + static final InternetAddress anyIPv6 = InternetAddress('::'); + + /// IP version 4 loopback address. Use this address when listening on + /// or connecting to the loopback adapter using IP version 4 (IPv4). + static final InternetAddress loopbackIPv4 = InternetAddress('127.0.0.1'); + + /// IP version 6 loopback address. Use this address when listening on + /// or connecting to the loopback adapter using IP version 6 (IPv6). + static final InternetAddress loopbackIPv6 = InternetAddress('::1'); + + /// The numeric address of the host. + /// + /// For IPv4 addresses this is using the dotted-decimal notation. + /// For IPv6 it is using the hexadecimal representation. + /// For Unix domain addresses, this is a file path. + final String address; + + /// The raw address of this [InternetAddress]. + /// + /// For an IP address, the result is either a 4 or 16 byte long list. + /// For a Unix domain address, UTF-8 encoded byte sequences that represents + /// [address] is returned. + /// + /// The returned list is a fresh copy, making it possible to change the list without + /// modifying the [InternetAddress]. + final Uint8List rawAddress; + + /// The address family of the [InternetAddress]. + final InternetAddressType type; + + /// Creates a new [InternetAddress] from a numeric address or a file path. + /// + /// If [type] is [InternetAddressType.ipv4], [address] must be a numeric IPv4 + /// address (dotted-decimal notation). + /// If [type] is [InternetAddressType.ipv6], [address] must be a numeric IPv6 + /// address (hexadecimal notation). + /// If [type] is [InternetAddressType.unix], [address] must be a a valid file + /// path. + /// If [type] is omitted, [address] must be either a numeric IPv4 or IPv6 + /// address and the type is inferred from the format. + /// + /// To create a Unix domain address, [type] should be + /// [InternetAddressType.unix] and [address] should be a string. + factory InternetAddress(String address, {InternetAddressType? type}) { + if (type == InternetAddressType.unix) { + if (!address.startsWith('/')) { + throw ArgumentError.value(address, 'address'); + } + return InternetAddress._( + address: address, + rawAddress: Uint8List(0), + type: InternetAddressType.unix, + ); + } + final parsed = tryParse(address); + if (parsed == null) { + throw ArgumentError.value(address, 'address'); + } + return parsed; + } + + /// Creates a new [InternetAddress] from the provided raw address bytes. + /// + /// If the [type] is [InternetAddressType.ipv4], the [rawAddress] must have + /// length 4. + /// If the [type] is [InternetAddressType.ipv6], the [rawAddress] must have + /// length 16. + /// If the [type] is [InternetAddressType.ipv4], the [rawAddress] must be a + /// valid UTF-8 encoded file path. + /// + /// If [type] is omitted, the [rawAddress] must have a length of either 4 or + /// 16, in which case the type defaults to [InternetAddressType.ipv4] or + /// [InternetAddressType.ipv6] respectively. + factory InternetAddress.fromRawAddress(Uint8List rawAddress, + {InternetAddressType? type}) { + if (type == InternetAddressType.unix) { + return InternetAddress(utf8.decode(rawAddress), type: type); + } + final address = _stringFromIp(rawAddress); + type = _type(address); + return InternetAddress._( + address: address, + rawAddress: rawAddress, + type: type, + ); + } + + InternetAddress._({ + required this.address, + required this.rawAddress, + required this.type, + }); + + @override + int get hashCode => const ListEquality().hash(rawAddress); + + /// The host used to lookup the address. + /// + /// If there is no host associated with the address this returns the [address]. + String get host => address; + + /// Returns true if the [InternetAddress]s scope is a link-local. + bool get isLinkLocal { + final rawAddress = this.rawAddress; + if (type == InternetAddressType.ipv6) { + // First 10 bits is 0xFE80 + return rawAddress[0] == 0xFE && ((0x80 | 0x40) & rawAddress[1]) == 0x80; + } + return false; + } + + /// Returns true if the [InternetAddress] is a loopback address. + bool get isLoopback => this == loopbackIPv4 || this == loopbackIPv6; + + /// Returns true if the [InternetAddress]s scope is multicast. + bool get isMulticast => this == anyIPv4 || this == anyIPv6; + + @override + bool operator ==(other) { + if (other is InternetAddress) { + if (type == InternetAddressType.unix) { + return address == other.address; + } + return const ListEquality().equals(rawAddress, other.rawAddress); + } + return false; + } + + /// Perform a reverse DNS lookup on this [address] + /// + /// Returns a new [InternetAddress] with the same address, but where the [host] + /// field set to the result of the lookup. + /// + /// If this address is Unix domain addresses, no lookup is performed and this + /// address is returned directly. + Future reverse() { + throw UnimplementedError(); + } + + /// Lookup a host, returning a Future of a list of + /// [InternetAddress]s. If [type] is [InternetAddressType.any], it + /// will lookup both IP version 4 (IPv4) and IP version 6 (IPv6) + /// addresses. If [type] is either [InternetAddressType.ipv4] or + /// [InternetAddressType.ipv6] it will only lookup addresses of the + /// specified type. The order of the list can, and most likely will, + /// change over time. + static Future> lookup(String host, + {InternetAddressType type = InternetAddressType.any}) => + throw UnimplementedError(); + + /// Attempts to parse [address] as a numeric address. + /// + /// Returns `null` If [address] is not a numeric IPv4 (dotted-decimal + /// notation) or IPv6 (hexadecimal representation) address. + static InternetAddress? tryParse(String address) { + final rawAddress = _tryParseRawAddress(address); + if (rawAddress == null) { + return null; + } + final type = _type(address); + return InternetAddress._( + address: address, + rawAddress: rawAddress, + type: type, + ); + } +} diff --git a/lib/src/util/provider_logger.dart b/lib/src/util/provider_logger.dart new file mode 100644 index 0000000..2114ff0 --- /dev/null +++ b/lib/src/util/provider_logger.dart @@ -0,0 +1,46 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ProviderLogger implements ProviderObserver { + const ProviderLogger(); + + @override + void didAddProvider( + ProviderBase provider, + Object? value, + ProviderContainer container, + ) { + print('Provider+: ${provider.describe}'); + } + + @override + void didDisposeProvider( + ProviderBase provider, + ProviderContainer container, + ) { + print('Provider-: ${provider.describe}'); + } + + @override + void didUpdateProvider( + ProviderBase provider, + Object? previousValue, + Object? newValue, + ProviderContainer container, + ) { + print('Provider*: ${provider.describe}'); + } + + @override + void providerDidFail( + ProviderBase provider, + Object error, + StackTrace stackTrace, + ProviderContainer container, + ) { + print('Provider!: ${provider.describe}'); + } +} + +extension _ProviderName on ProviderBase { + String get describe => name ?? toString(); +} diff --git a/lib/src/util/single_activator.dart b/lib/src/util/single_activator.dart new file mode 100644 index 0000000..89bf71f --- /dev/null +++ b/lib/src/util/single_activator.dart @@ -0,0 +1,49 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:terminal_studio/src/util/target_platform.dart'; + +extension SingleActivatorExtension on SingleActivator { + String get platformLabel { + return defaultTargetPlatform.isApple ? appleLabel : windowsLabel; + } + + String get windowsLabel { + final StringBuffer buffer = StringBuffer(); + + if (control) { + buffer.write('Ctrl+'); + } + if (meta) { + buffer.write('Meta+'); + } + if (shift) { + buffer.write('Shift+'); + } + if (alt) { + buffer.write('Alt+'); + } + buffer.write(trigger.keyLabel); + + return buffer.toString(); + } + + String get appleLabel { + final StringBuffer buffer = StringBuffer(); + + if (control) { + buffer.write('⌃'); + } + if (alt) { + buffer.write('⌥'); + } + if (shift) { + buffer.write('⇧'); + } + if (meta) { + buffer.write('⌘'); + } + buffer.write(trigger.keyLabel); + + return buffer.toString(); + } +} diff --git a/lib/src/util/tabs_extension.dart b/lib/src/util/tabs_extension.dart new file mode 100644 index 0000000..879b537 --- /dev/null +++ b/lib/src/util/tabs_extension.dart @@ -0,0 +1,25 @@ +import 'package:flex_tabs/flex_tabs.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:terminal_studio/src/core/state/tabs.dart'; + +extension RefTabsExtension on WidgetRef { + void openTab(TabItem tab) { + final document = read(tabsProvider); + document.root?.add(tab); + tab.activate(); + } +} + +extension TabItemExtension on TabItem { + void addToSide(TabItem item) { + final parent = this.parent; + + if (parent == null) { + return; + } + + parent.insert(parent.indexOf(this) + 1, item); + + parent.activate(item); + } +} diff --git a/lib/src/util/target_platform.dart b/lib/src/util/target_platform.dart new file mode 100644 index 0000000..992bccc --- /dev/null +++ b/lib/src/util/target_platform.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; + +extension TargetPlatformExtension on TargetPlatform { + bool get isApple => + this == TargetPlatform.iOS || this == TargetPlatform.macOS; + + bool get isDesktop => + this == TargetPlatform.linux || + this == TargetPlatform.macOS || + this == TargetPlatform.windows; +} diff --git a/lib/src/util/uuid.dart b/lib/src/util/uuid.dart new file mode 100644 index 0000000..40abd65 --- /dev/null +++ b/lib/src/util/uuid.dart @@ -0,0 +1,5 @@ +import 'package:uuid/uuid.dart'; + +const _uuid = Uuid(); + +String uuidV4() => _uuid.v4(); diff --git a/lib/src/util/validators.dart b/lib/src/util/validators.dart new file mode 100644 index 0000000..aad4563 --- /dev/null +++ b/lib/src/util/validators.dart @@ -0,0 +1,35 @@ +import 'package:terminal_studio/src/util/internet_address.dart'; + +bool isIPv4(String address) { + try { + InternetAddress(address, type: InternetAddressType.ipv4); + return true; + } catch (e) { + return false; + } +} + +bool isIPv6(String address) { + try { + InternetAddress(address, type: InternetAddressType.ipv6); + return true; + } catch (e) { + return false; + } +} + +bool isDomain(String address) { + const pattern = + r'^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*$'; + return RegExp(pattern).hasMatch(address); +} + +bool isHostOrIP(String value) { + return isIPv4(value) || isIPv6(value) || isDomain(value); +} + +bool isPort(String input) { + final value = int.tryParse(input); + if (value == null) return false; + return value >= 0 && value <= 65535; +} diff --git a/studio/linux/.gitignore b/linux/.gitignore similarity index 100% rename from studio/linux/.gitignore rename to linux/.gitignore diff --git a/studio/linux/CMakeLists.txt b/linux/CMakeLists.txt similarity index 59% rename from studio/linux/CMakeLists.txt rename to linux/CMakeLists.txt index e9122db..b9f8cbc 100644 --- a/studio/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -1,14 +1,32 @@ +# Project-level configuration. cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) -set(BINARY_NAME "studio") -set(APPLICATION_ID "com.example.studio") +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "TerminalStudio") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.dartssh.terminalstudio") +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. cmake_policy(SET CMP0063 NEW) +# Load bundled libraries from the lib/ directory relative to the binary. set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") -# Configure build options. +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) @@ -17,6 +35,10 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) endif() # Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) @@ -24,9 +46,8 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. @@ -35,17 +56,36 @@ pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") -# Application build +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} "main.cc" "my_application.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) @@ -76,11 +116,11 @@ install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR} install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) -endif() +endforeach(bundled_library) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. diff --git a/studio/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt similarity index 95% rename from studio/linux/flutter/CMakeLists.txt rename to linux/flutter/CMakeLists.txt index f5814e6..d5bd016 100644 --- a/studio/linux/flutter/CMakeLists.txt +++ b/linux/flutter/CMakeLists.txt @@ -1,3 +1,4 @@ +# This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -78,7 +79,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - linux-x64 ${CMAKE_BUILD_TYPE} + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e379152 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_acrylic_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAcrylicPlugin"); + flutter_acrylic_plugin_register_with_registrar(flutter_acrylic_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); +} diff --git a/studio/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h similarity index 93% rename from studio/linux/flutter/generated_plugin_registrant.h rename to linux/flutter/generated_plugin_registrant.h index 9bf7478..e0f0a47 100644 --- a/studio/linux/flutter/generated_plugin_registrant.h +++ b/linux/flutter/generated_plugin_registrant.h @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/studio/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake similarity index 55% rename from studio/linux/flutter/generated_plugins.cmake rename to linux/flutter/generated_plugins.cmake index 51436ae..0d5940b 100644 --- a/studio/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,14 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_acrylic + screen_retriever + url_launcher_linux + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_pty ) set(PLUGIN_BUNDLED_LIBRARIES) @@ -13,3 +21,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/studio/linux/main.cc b/linux/main.cc similarity index 52% rename from studio/linux/main.cc rename to linux/main.cc index 058e617..e7c5c54 100644 --- a/studio/linux/main.cc +++ b/linux/main.cc @@ -1,10 +1,6 @@ #include "my_application.h" int main(int argc, char** argv) { - // Only X11 is currently supported. - // Wayland support is being developed: https://github.com/flutter/flutter/issues/57932. - gdk_set_allowed_backends("x11"); - g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..b81b184 --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "studio"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "studio"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/studio/linux/my_application.h b/linux/my_application.h similarity index 100% rename from studio/linux/my_application.h rename to linux/my_application.h diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml new file mode 100644 index 0000000..f4814c9 --- /dev/null +++ b/linux/packaging/deb/make_config.yaml @@ -0,0 +1,111 @@ +# the name used to display in the OS. Specifically desktop +# entry name +display_name: TerminalStudio + +# package name for debian/apt repository +# the name should be all lowercase with -+. +package_name: terminalstudio + +maintainer: + name: xuty + email: xty50337@hotmail.com + +co_authors: + - name: Michael Lamers + email: info@devmil.de + +# enum options -> required, important, standard, optional, extra +# refer: https://www.debian.org/doc/debian-policy/ch-archive.html#s-priorities +priority: optional + +# enum options: admin, cli-mono, comm, database, debug, devel, doc, editors, education, electronics, embedded, fonts, games, gnome, gnu-r, gnustep, graphics, hamradio, haskell, httpd, interpreters, introspection, java, javascript, kde, kernel, libdevel, libs, lisp, localization, mail, math, metapackages, misc, net, news, ocaml, oldlibs, otherosfs, perl, php, python, ruby, rust, science, shells, sound, tasks, tex, text, utils, vcs, video, web, x11, xfce, zope +# refer: https://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections +section: x11 + +# the size of binary in kilobyte +installed_size: 24400 + +# direct dependencies required by the application +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# dependencies: +# - libkeybinder-3.0-0 (>= 0.3.2) + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# build_dependencies_indep: +# - texinfo + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# build_dependencies: +# - kernel-headers-2.2.10 [!hurd-i386] +# - gnumach-dev [hurd-i386] +# - libluajit5.1-dev [i386 amd64 kfreebsd-i386 armel armhf powerpc mips] + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# recommended_dependencies: +# - neofetch + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# suggested_dependencies: +# - libkeybinder-3.0-0 (>= 0.3.2) + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# enhances: +# - spotube + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# pre_dependencies: +# - libc6 + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#packages-which-break-other-packages-breaks +# breaks: +# - libspotify (<< 3.0.0) + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#conflicting-binary-packages-conflicts +# conflicts: +# - spotify + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#virtual-packages-provides +# provides: +# - libx11 + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#overwriting-files-and-replacing-packages-replaces +# replaces: +# - spotify + +essential: false + +# postinstall_scripts: +# - echo `Installed my awesome app` + +# postuninstall_scripts: +# - echo `Surprised Pickachu face` +# application icon path relative to project url +icon: assets/icon.png + +keywords: + - Terminal + - Shell + - SSH + +# a name to categorize the app into a section of application +# generic_name: Hobby Application + +# supported mime types that can be opened using this application +# supported_mime_type: +# - audio/mpeg + +# shown when right clicked the desktop entry icons +# actions: +# - Gallery +# - Create + +# the categories the application belong to +# refer: https://specifications.freedesktop.org/menu-spec/latest/ +# categories: +# - Music +# - Media + +# let OS know if the application can be run on start_up. If it's false +# the application will deny to the OS if it was added as a start_up +# application +startup_notify: true diff --git a/studio/macos/.gitignore b/macos/.gitignore similarity index 91% rename from studio/macos/.gitignore rename to macos/.gitignore index d2fd377..746adbb 100644 --- a/studio/macos/.gitignore +++ b/macos/.gitignore @@ -3,4 +3,5 @@ **/Pods/ # Xcode-related +**/dgph **/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..79719a0 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,22 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_acrylic +import macos_ui +import path_provider_macos +import screen_retriever +import url_launcher_macos +import window_manager + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterAcrylicPlugin.register(with: registry.registrar(forPlugin: "FlutterAcrylicPlugin")) + MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..dade8df --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..cc9cdb8 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,58 @@ +PODS: + - flutter_acrylic (0.1.0): + - FlutterMacOS + - flutter_pty (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - macos_ui (0.1.0): + - FlutterMacOS + - path_provider_macos (0.0.1): + - FlutterMacOS + - screen_retriever (0.0.1): + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS + +DEPENDENCIES: + - flutter_acrylic (from `Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos`) + - flutter_pty (from `Flutter/ephemeral/.symlinks/plugins/flutter_pty/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - macos_ui (from `Flutter/ephemeral/.symlinks/plugins/macos_ui/macos`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + +EXTERNAL SOURCES: + flutter_acrylic: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos + flutter_pty: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_pty/macos + FlutterMacOS: + :path: Flutter/ephemeral + macos_ui: + :path: Flutter/ephemeral/.symlinks/plugins/macos_ui/macos + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + screen_retriever: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + +SPEC CHECKSUMS: + flutter_acrylic: c3df24ae52ab6597197837ce59ef2a8542640c17 + flutter_pty: 41b6f848ade294be726a6b94cdd4a67c3bc52f59 + FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 + macos_ui: 125c911559d646194386d84c017ad6819122e2db + path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + +PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c + +COCOAPODS: 1.11.3 diff --git a/studio/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj similarity index 84% rename from studio/macos/Runner.xcodeproj/project.pbxproj rename to macos/Runner.xcodeproj/project.pbxproj index d549b03..fbb9df1 100644 --- a/studio/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + E53DC2299DFC84C2032D5B92 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D7B9FA76132D3B9C0F1BB36 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,7 +55,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* studio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "studio.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* studio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = studio.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -66,8 +67,12 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4584300D8181C011CE86C298 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8D7B9FA76132D3B9C0F1BB36 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AD40269EA316C34C1F218626 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + AE2BFCCA80158FFE7814D4CC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,6 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E53DC2299DFC84C2032D5B92 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +105,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 7BE375411EFC82CE9741B006 /* Pods */, ); sourceTree = ""; }; @@ -145,9 +152,21 @@ path = Runner; sourceTree = ""; }; + 7BE375411EFC82CE9741B006 /* Pods */ = { + isa = PBXGroup; + children = ( + AD40269EA316C34C1F218626 /* Pods-Runner.debug.xcconfig */, + 4584300D8181C011CE86C298 /* Pods-Runner.release.xcconfig */, + AE2BFCCA80158FFE7814D4CC /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 8D7B9FA76132D3B9C0F1BB36 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -159,11 +178,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 17FC3076E7577A7CB89904F5 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + EF31537EDB7E1DACD68CB5B8 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -182,8 +203,8 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "The Flutter Authors"; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; @@ -202,7 +223,7 @@ }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 8.0"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -233,6 +254,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 17FC3076E7577A7CB89904F5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -268,7 +311,24 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + EF31537EDB7E1DACD68CB5B8 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -361,10 +421,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -491,10 +547,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -515,10 +567,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/studio/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from studio/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/studio/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 83% rename from studio/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0f621e8..7e4f1d2 100644 --- a/studio/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - - - - - + + - - + + diff --git a/studio/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from studio/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/studio/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift similarity index 100% rename from studio/macos/Runner/AppDelegate.swift rename to macos/Runner/AppDelegate.swift diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..7b4d860 --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images": [ + { + "filename": "app_icon_16.png", + "idiom": "mac", + "scale": "1x", + "size": "16x16" + }, + { + "filename": "app_icon_32.png", + "idiom": "mac", + "scale": "2x", + "size": "16x16" + }, + { + "filename": "app_icon_32.png", + "idiom": "mac", + "scale": "1x", + "size": "32x32" + }, + { + "filename": "app_icon_64.png", + "idiom": "mac", + "scale": "2x", + "size": "32x32" + }, + { + "filename": "app_icon_128.png", + "idiom": "mac", + "scale": "1x", + "size": "128x128" + }, + { + "filename": "app_icon_256.png", + "idiom": "mac", + "scale": "2x", + "size": "128x128" + }, + { + "filename": "app_icon_256.png", + "idiom": "mac", + "scale": "1x", + "size": "256x256" + }, + { + "filename": "app_icon_512.png", + "idiom": "mac", + "scale": "2x", + "size": "256x256" + }, + { + "filename": "app_icon_512.png", + "idiom": "mac", + "scale": "1x", + "size": "512x512" + }, + { + "filename": "app_icon_1024.png", + "idiom": "mac", + "scale": "2x", + "size": "512x512" + } + ], + "info": { + "author": "icons_launcher", + "version": 1 + } +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..69f37c2 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..bc753d7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..d8c62d7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..744e210 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..9f30738 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..d562433 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..f210872 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/studio/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib similarity index 95% rename from studio/macos/Runner/Base.lproj/MainMenu.xib rename to macos/Runner/Base.lproj/MainMenu.xib index 537341a..d50c402 100644 --- a/studio/macos/Runner/Base.lproj/MainMenu.xib +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -13,7 +13,7 @@ - + @@ -323,17 +323,22 @@ + + + + - - + + - + + diff --git a/studio/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig similarity index 75% rename from studio/macos/Runner/Configs/AppInfo.xcconfig rename to macos/Runner/Configs/AppInfo.xcconfig index 4b88053..9ba4430 100644 --- a/studio/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = studio +PRODUCT_NAME = TerminalStudio // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.studio +PRODUCT_BUNDLE_IDENTIFIER = com.dartssh.terminalstudio // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2020 com.example. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2022 xuty. All rights reserved. diff --git a/studio/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from studio/macos/Runner/Configs/Debug.xcconfig rename to macos/Runner/Configs/Debug.xcconfig diff --git a/studio/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from studio/macos/Runner/Configs/Release.xcconfig rename to macos/Runner/Configs/Release.xcconfig diff --git a/studio/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from studio/macos/Runner/Configs/Warnings.xcconfig rename to macos/Runner/Configs/Warnings.xcconfig diff --git a/studio/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements similarity index 100% rename from studio/macos/Runner/DebugProfile.entitlements rename to macos/Runner/DebugProfile.entitlements diff --git a/studio/macos/Runner/Info.plist b/macos/Runner/Info.plist similarity index 100% rename from studio/macos/Runner/Info.plist rename to macos/Runner/Info.plist diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/studio/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements similarity index 100% rename from studio/macos/Runner/Release.entitlements rename to macos/Runner/Release.entitlements diff --git a/macos/packaging/dmg/make_config.yaml b/macos/packaging/dmg/make_config.yaml new file mode 100644 index 0000000..49b3c38 --- /dev/null +++ b/macos/packaging/dmg/make_config.yaml @@ -0,0 +1,10 @@ +title: TerminalStudio +contents: + - x: 448 + y: 344 + type: link + path: "/Applications" + - x: 192 + y: 344 + type: file + path: TerminalStudio.app diff --git a/pty b/pty deleted file mode 160000 index 852d8d2..0000000 --- a/pty +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 852d8d2da06ea77f41ed1faf04b090d22897f1aa diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..348c439 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,936 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "47.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.7.0" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.1" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + asn1lib: + dependency: transitive + description: + name: asn1lib + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.4" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.1" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" + code_text_field: + dependency: "direct main" + description: + name: code_text_field + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + collection: + dependency: "direct main" + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + console: + dependency: transitive + description: + name: console + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + context_menus: + dependency: "direct main" + description: + name: context_menus + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + dart_console: + dependency: transitive + description: + name: dart_console + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.4" + dartssh2: + dependency: "direct main" + description: + name: dartssh2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.7.2+3" + equatable: + dependency: transitive + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + fading_edge_scrollview: + dependency: transitive + description: + name: fading_edge_scrollview + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + file: + dependency: "direct main" + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flex_tabs: + dependency: "direct main" + description: + name: flex_tabs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0-pre" + fluent_ui: + dependency: "direct main" + description: + name: fluent_ui + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_acrylic: + dependency: "direct main" + description: + name: flutter_acrylic + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0+2" + flutter_highlight: + dependency: "direct main" + description: + name: flutter_highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_pty: + dependency: "direct main" + description: + name: flutter_pty + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "10.2.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + get_it: + dependency: transitive + description: + name: get_it + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + highlight: + dependency: "direct main" + description: + name: highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.0" + hive: + dependency: transitive + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + icons_launcher: + dependency: "direct dev" + description: + name: icons_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.7.0" + linked_scroll_controller: + dependency: transitive + description: + name: linked_scroll_controller + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + macos_ui: + dependency: "direct main" + description: + name: macos_ui + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.5" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + msix: + dependency: "direct dev" + description: + name: msix + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.6" + multi_split_view: + dependency: transitive + description: + name: multi_split_view + url: "https://pub.dartlang.org" + source: hosted + version: "1.13.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.20" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + pinenacl: + dependency: transitive + description: + name: pinenacl + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + platform_info: + dependency: transitive + description: + name: platform_info + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.1" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + recase: + dependency: transitive + description: + name: recase + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + riverpod: + dependency: transitive + description: + name: riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" + scroll_pos: + dependency: transitive + description: + name: scroll_pos + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.5" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.2+1" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + url: "https://pub.dartlang.org" + source: hosted + version: "20.3.47" + syncfusion_flutter_datagrid: + dependency: "direct main" + description: + name: syncfusion_flutter_datagrid + url: "https://pub.dartlang.org" + source: hosted + version: "20.3.47" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.12" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + url_launcher: + dependency: transitive + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.5" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + uuid: + dependency: "direct main" + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.7.0" + window_manager: + dependency: "direct main" + description: + name: window_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.7" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+2" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + xterm: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: baa72b3fc5f6a3edf6df3b0dc77b083d2511c091 + url: "https://github.com/TerminalStudio/xterm.dart.git" + source: git + version: "3.2.7" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/studio/pubspec.yaml b/pubspec.yaml similarity index 59% rename from studio/pubspec.yaml rename to pubspec.yaml index 9635d17..502e002 100644 --- a/studio/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ -name: studio -description: A new Flutter project. +name: terminal_studio +description: The Flutter based terminal emulator and more! # The following line prevents the package from being accidentally published to -# pub.dev using `pub publish`. This is preferred for private packages. +# pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. @@ -15,65 +15,112 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+1 +version: 0.0.1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.18.0 <3.0.0" +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + + dartssh2: ^2.7.2+3 + + # xterm: ^3.2.7 xterm: - path: ../xterm + git: https://github.com/TerminalStudio/xterm.dart.git - pty: - path: ../pty + flutter_riverpod: ^2.0.0 - tabs: - path: ../tabs + flutter_pty: ^0.3.1 - context_menu_macos: - path: ../context_menu_macos + window_manager: ^0.2.7 - web_socket_channel: ^1.2.0 - http: ^0.12.2 + flutter_acrylic: ^1.0.0+2 - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + context_menus: ^1.0.2 + + flex_tabs: ^0.2.0-pre + + file: ^6.1.4 + + path: ^1.8.2 + + hive_flutter: ^1.1.0 + + uuid: ^3.0.6 + + macos_ui: ^1.7.5 + + syncfusion_flutter_datagrid: ^20.3.47 + + code_text_field: ^1.0.2 + + highlight: ^0.7.0 + + flutter_highlight: ^0.7.0 + + fluent_ui: ^4.0.1 + + font_awesome_flutter: ^10.2.1 + + collection: ^1.16.0 dev_dependencies: flutter_test: sdk: flutter + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + + hive_generator: ^1.1.3 + + build_runner: ^2.2.1 + + icons_launcher: ^2.0.5 + + msix: ^3.6.6 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec -# The following section is specific to Flutter. +# The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg + # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. + # https://flutter.dev/assets-and-images/#resolution-aware + # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages + # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - - fonts: - - family: Cascadia Mono - fonts: - - asset: fonts/CascadiaMonoPL.ttf # fonts: # - family: Schyler # fonts: diff --git a/snap/gui/app_icon.desktop b/snap/gui/app_icon.desktop new file mode 100644 index 0000000..92a01f6 --- /dev/null +++ b/snap/gui/app_icon.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Flutter Linux App +Comment=Flutter Linux launcher icon +Exec=app_icon +Icon=app_icon.png +Terminal=false +Type=Application +Categories=Entertainment; diff --git a/snap/gui/app_icon.png b/snap/gui/app_icon.png new file mode 100644 index 0000000..a1d8588 Binary files /dev/null and b/snap/gui/app_icon.png differ diff --git a/studio/.gitignore b/studio/.gitignore deleted file mode 100644 index 9d532b1..0000000 --- a/studio/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Web related -lib/generated_plugin_registrant.dart - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json diff --git a/studio/.metadata b/studio/.metadata deleted file mode 100644 index e43ea92..0000000 --- a/studio/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 71aa7395b9dbbc4ef5c830a363f209333585c63e - channel: master - -project_type: app diff --git a/studio/README.md b/studio/README.md deleted file mode 100644 index 0ed5e61..0000000 --- a/studio/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# studio - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/studio/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/studio/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4..0000000 Binary files a/studio/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/studio/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/studio/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b7..0000000 Binary files a/studio/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/studio/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/studio/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391..0000000 Binary files a/studio/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/studio/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/studio/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d..0000000 Binary files a/studio/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/studio/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/studio/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372e..0000000 Binary files a/studio/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/studio/fonts/CascadiaMonoPL.ttf b/studio/fonts/CascadiaMonoPL.ttf deleted file mode 100644 index 801448e..0000000 Binary files a/studio/fonts/CascadiaMonoPL.ttf and /dev/null differ diff --git a/studio/ios/Flutter/Debug.xcconfig b/studio/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/studio/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/studio/ios/Flutter/Release.xcconfig b/studio/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/studio/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf0..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd9..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde121..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc230..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd9..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b86..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b86..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d16..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f58..0000000 Binary files a/studio/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/studio/lib/main.dart b/studio/lib/main.dart deleted file mode 100644 index 23cef25..0000000 --- a/studio/lib/main.dart +++ /dev/null @@ -1,590 +0,0 @@ -import 'dart:io'; - -import 'package:context_menu_macos/context_menu_macos.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:pty/pty.dart'; -import 'package:studio/shortcut/intents.dart'; -import 'package:studio/shortcut/terminal_shortcut.dart'; -import 'package:studio/terminal_search_bar.dart'; -import 'package:studio/utils/build_mode.dart'; -import 'package:studio/utils/pty_terminal_backend.dart'; -import 'package:tabs/tabs.dart'; - -import 'package:flutter/material.dart' hide Tab, TabController; -import 'package:xterm/flutter.dart'; -import 'package:xterm/isolate.dart'; -import 'package:xterm/theme/terminal_style.dart'; -import 'package:xterm/xterm.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Terminal Lite', - debugShowCheckedModeBanner: false, - theme: ThemeData( - brightness: Brightness.dark, - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: MyHomePage(), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key}) : super(key: key); - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final tabs = TabsController(); - - final group = TabGroupController(); - - var tabCount = 0; - - @override - void initState() { - addTab(); - - final group = TabsGroup(controller: this.group); - - tabs.replaceRoot(group); - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.transparent, - body: SafeArea( - child: Container( - // color: Color(0xFF3A3D3F), - color: Colors.transparent, - child: TabsView( - controller: tabs, - actions: [ - TabsGroupAction( - icon: CupertinoIcons.add, - onTap: (group) async { - final tab = await buildTab(); - group.addTab(tab, activate: true); - }, - ) - ], - ), - ), - ), - ); - } - - void addTab() async { - this.group.addTab(await buildTab(), activate: true); - } - - Future buildTab() async { - tabCount++; - var tabIsClosed = false; - - final tab = TabController(); - - if (!Platform.isWindows) { - Directory.current = Platform.environment['HOME'] ?? '/'; - } - - // terminal.debug.enable(); - - final shell = getShell(); - - final backend = PtyTerminalBackend( - PseudoTerminal.start( - shell, - // ['-l'], - [], - blocking: - false, //!BuildMode.isDebug, //disabled for now due to problems with the blocking pseudo terminal - ackProcessed: !BuildMode.isDebug, - ), - ); - - // pty.write('cd\n'); - - final terminal = (!BuildMode.isDebug) - ? TerminalIsolate( - onTitleChange: tab.setTitle, - backend: backend, - platform: getPlatform(true), - minRefreshDelay: Duration(milliseconds: 50), - maxLines: 10000, - ) - : Terminal( - onTitleChange: tab.setTitle, - backend: backend, - platform: getPlatform(true), - maxLines: 10000, - ); - - //terminal.debug.enable(true); - if (terminal is TerminalIsolate) { - await terminal.start(); - } - - final focusNode = FocusNode( - skipTraversal: - true, //this is needed so that Tabs in the Terminal don't lead to a focus jump - ); - - SchedulerBinding.instance!.addPostFrameCallback((timeStamp) { - focusNode.requestFocus(); - }); - - terminal.backendExited.then((_) => tab.requestClose()); - - return Tab( - controller: tab, - title: 'Terminal', - content: TerminalTab( - terminal: terminal, - focusNode: focusNode, - ), - onActivate: () { - focusNode.requestFocus(); - }, - onDrop: () { - SchedulerBinding.instance!.addPostFrameCallback((timeStamp) { - focusNode.requestFocus(); - }); - }, - onClose: () { - // this handler can be called multiple times. - // e.g. click to close tab => handler => terminateBackend => exitedEvent => close tab - // which leads to an inconsistent tabCount value - if (tabIsClosed) { - return; - } - tabIsClosed = true; - terminal.terminateBackend(); - - tabCount--; - - if (tabCount <= 0) { - exit(0); - } - }, - ); - } - - String getShell() { - if (Platform.isWindows) { - // return r'C:\windows\system32\cmd.exe'; - return r'C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe'; - } - - return Platform.environment['SHELL'] ?? 'sh'; - } - - PlatformBehavior getPlatform([bool forLocalShell = false]) { - if (Platform.isWindows) { - return PlatformBehaviors.windows; - } - - if (forLocalShell && Platform.isMacOS) { - return PlatformBehaviors.mac; - } - - return PlatformBehaviors.unix; - } -} - -class TerminalTab extends StatefulWidget { - TerminalTab({ - required this.terminal, - required this.focusNode, - }) : super(key: UniqueKey()); - - final TerminalUiInteraction terminal; - final FocusNode focusNode; - final scrollController = ScrollController(); - - @override - State createState() => _TerminalTabState(); -} - -class _TerminalTabState extends State { - var fontSize = 14.0; - - final searchTextController = TextEditingController(); - late final FocusNode focusNodeUserSearchInput; - var _isUserSearchActive = false; - - void _onTerminalChanges() { - if (widget.terminal.isUserSearchActive != _isUserSearchActive) { - setState(() { - _isUserSearchActive = widget.terminal.isUserSearchActive; - }); - } - } - - @override - void initState() { - searchTextController.text = widget.terminal.userSearchPattern ?? ""; - searchTextController.addListener(() { - if (searchTextController.text == '') { - widget.terminal.userSearchPattern = null; - } else { - widget.terminal.userSearchPattern = searchTextController.text; - } - }); - focusNodeUserSearchInput = FocusNode( - onKeyEvent: (node, event) { - if (event.logicalKey == LogicalKeyboardKey.escape) { - disableSearch(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - ); - widget.terminal.addListener(_onTerminalChanges); - super.initState(); - } - - @override - void dispose() { - widget.terminal.removeListener(_onTerminalChanges); - super.dispose(); - } - - List getShortcuts() => [ - TerminalShortcut( - name: 'Zoom In', - onExecute: (terminal) async => onZoomIn(), - keyCombinations: [ - _withModifier(LogicalKeyboardKey.add), - _withModifier(LogicalKeyboardKey.equal), - ], - intent: ZoomInIntent()), - TerminalShortcut( - name: 'Zoom Out', - onExecute: (terminal) async => onZoomOut(), - keyCombinations: [ - _withModifier(LogicalKeyboardKey.minus), - ], - intent: ZoomOutIntent()), - TerminalShortcut( - name: 'Copy', - onExecute: (terminal) async => onCopy(terminal), - onIsAvailable: (terminal) async { - final hasSelection = - !(widget.terminal.selection?.isEmpty ?? true); - return hasSelection; - }, - keyCombinations: [ - _withModifier(LogicalKeyboardKey.keyC, needsExtraModifier: true), - ], - intent: CopyIntent()), - TerminalShortcut( - name: 'Paste', - onExecute: (terminal) async => onPaste(terminal), - onIsAvailable: (terminal) async { - final clipboardData = await Clipboard.getData('text/plain'); - - final clipboardHasData = clipboardData?.text?.isNotEmpty == true; - - return clipboardHasData; - }, - keyCombinations: [ - _withModifier(LogicalKeyboardKey.keyV, needsExtraModifier: true), - ], - intent: PasteIntent()), - TerminalShortcut( - name: 'Select all', - onExecute: (terminal) async => onSelectAll(terminal), - keyCombinations: [ - _withModifier(LogicalKeyboardKey.keyA, needsExtraModifier: true), - ], - intent: SelectAllIntent()), - TerminalShortcut( - name: 'Clear', - onExecute: (terminal) async => onClear(terminal), - keyCombinations: [ - _withModifier(LogicalKeyboardKey.keyK, needsExtraModifier: true), - ], - intent: ClearIntent()), - TerminalShortcut( - name: 'Kill', - onExecute: (terminal) async => onKill(terminal), - keyCombinations: [ - _withModifier(LogicalKeyboardKey.keyE, needsExtraModifier: true), - ], - intent: KillIntent()), - TerminalShortcut( - name: 'Search', - onExecute: (terminal) async => onSearch(), - keyCombinations: [ - _withModifier(LogicalKeyboardKey.keyF, needsExtraModifier: false), - ], - intent: SearchIntent()), - ]; - - String _shortcutKeysToString(Iterable? triggers) { - if (triggers == null) { - return ''; - } - String specialKeySequence = ''; - String normalKeySequence = ''; - var metaHandled = false; - var controlHandled = false; - var altHandled = false; - var shiftHandled = false; - for (final trigger in triggers) { - if (trigger == LogicalKeyboardKey.meta || - trigger == LogicalKeyboardKey.metaLeft || - trigger == LogicalKeyboardKey.metaRight) { - if (metaHandled) { - continue; - } - specialKeySequence += '⌘'; - metaHandled = true; - } else if (trigger == LogicalKeyboardKey.shift || - trigger == LogicalKeyboardKey.shiftLeft || - trigger == LogicalKeyboardKey.shiftRight) { - if (shiftHandled) { - continue; - } - specialKeySequence += '⇧'; - shiftHandled = true; - } else if (trigger == LogicalKeyboardKey.control || - trigger == LogicalKeyboardKey.controlLeft || - trigger == LogicalKeyboardKey.controlRight) { - if (controlHandled) { - continue; - } - specialKeySequence += '⌃'; - controlHandled = true; - } else if (trigger == LogicalKeyboardKey.alt || - trigger == LogicalKeyboardKey.altLeft || - trigger == LogicalKeyboardKey.altRight) { - if (altHandled) { - continue; - } - specialKeySequence += '⌥'; - altHandled = true; - } else { - normalKeySequence += trigger.keyLabel; - } - } - return '$specialKeySequence $normalKeySequence'; - } - - Future> createContextMenuItems( - BuildContext context, TerminalUiInteraction terminal) async { - final result = List.empty(growable: true); - final shortcuts = getShortcuts(); - - for (final shortcut in shortcuts) { - final firstAlternative = shortcut.keyCombinations[0]; - result.add(MacosContextMenuItem( - content: Text(shortcut.name), - trailing: Text(_shortcutKeysToString(firstAlternative.triggers)), - enabled: await shortcut.isAvailable(terminal), - onTap: () async { - await shortcut.execute(terminal); - Navigator.of(context).pop(); - }, - )); - } - return result; - } - - static Map shortcutsToActivatorMap( - List shortcuts) { - final result = Map(); - - for (final shortcut in shortcuts) { - for (final keyCombination in shortcut.keyCombinations) { - result.putIfAbsent(keyCombination, () => shortcut.intent); - } - } - - return result; - } - - static Map> shortcutsToActions( - List shortcuts, TerminalUiInteraction terminal) { - final result = Map>(); - - for (final shortcut in shortcuts) { - result.putIfAbsent( - shortcut.intent.runtimeType, - () => CallbackAction( - onInvoke: (intent) => shortcut.execute(terminal), - )); - } - - return result; - } - - @override - Widget build(BuildContext context) { - final shortcuts = getShortcuts(); - return Stack(children: [ - Shortcuts( - shortcuts: shortcutsToActivatorMap(shortcuts), - child: Actions( - actions: shortcutsToActions(shortcuts, widget.terminal), - child: GestureDetector( - onLongPress: () { - print('onLongPress'); - }, - // onDoubleTapDown: (details) { - onDoubleTap: () { - print('onDoubleTap \$details'); - }, - // print('onDoubleTapDown \$details'); - // }, - // onTertiaryTapDown: (details) { - // print('onTertiaryTapDown $details'); - // }, - onSecondaryTapDown: (details) async { - showMacosContextMenu( - context: context, - globalPosition: details.globalPosition, - children: - await createContextMenuItems(context, widget.terminal), - ); - }, - child: CupertinoScrollbar( - controller: widget.scrollController, - isAlwaysShown: true, - child: TerminalView( - scrollController: widget.scrollController, - terminal: widget.terminal, - focusNode: widget.focusNode, - opacity: 0.85, - style: TerminalStyle( - fontSize: fontSize, - fontFamily: const ['Cascadia Mono'], - ), - ), - ), - ), - ), - ), - Visibility( - child: TerminalSearchBar( - terminal: widget.terminal, - focusNode: focusNodeUserSearchInput, - searchTextController: searchTextController, - closeRequestHandler: () => disableSearch(), - ), - visible: _isUserSearchActive, - ), - ]); - } - - void updateFontSize(int delta) { - final minFontSize = 4; - final maxFontSize = 40; - - final newFontSize = fontSize + delta; - - if (newFontSize < minFontSize || newFontSize > maxFontSize) { - return; - } - - setState(() => fontSize = newFontSize); - } - - void onZoomIn() { - updateFontSize(1); - } - - void onZoomOut() { - updateFontSize(-1); - } - - void onCopy(TerminalUiInteraction terminal) { - final text = terminal.selectedText ?? ''; - Clipboard.setData(ClipboardData(text: text)); - terminal.clearSelection(); - //terminal.debug.onMsg('copy ┤$text├'); - terminal.refresh(); - } - - void onPaste(TerminalUiInteraction terminal) async { - final clipboardData = await Clipboard.getData('text/plain'); - - final clipboardHasData = clipboardData?.text?.isNotEmpty == true; - - if (clipboardHasData) { - terminal.paste(clipboardData!.text!); - //terminal.debug.onMsg('paste ┤${clipboardData.text}├'); - } - } - - void onSelectAll(TerminalUiInteraction terminal) { - print('Select All is currently not implemented.'); - } - - void onClear(TerminalUiInteraction terminal) { - print('Clear is currently not implemented.'); - } - - void onKill(TerminalUiInteraction terminal) { - terminal.terminateBackend(); - } - - void disableSearch() { - if (!widget.terminal.isUserSearchActive) { - return; - } - widget.terminal.isUserSearchActive = false; - widget.focusNode.requestFocus(); - } - - void onSearch() { - widget.terminal.isUserSearchActive = true; - // sets the initial search to the currently selected text if - // there is something selected and if there is no search term already - if (widget.terminal.selectedText != null && - widget.terminal.userSearchPattern == null) { - searchTextController.text = widget.terminal.selectedText!; - widget.terminal.userSearchPattern = searchTextController.text; - } else if (widget.terminal.userSearchPattern != null) { - searchTextController.text = widget.terminal.userSearchPattern!; - } else { - searchTextController.text = ''; - } - focusNodeUserSearchInput.requestFocus(); - } -} - -LogicalKeySet _withModifier(LogicalKeyboardKey key, - {needsExtraModifier = false}) { - final modifier = List.empty(growable: true); - - if (Platform.isMacOS) { - modifier.add(LogicalKeyboardKey.meta); - } else { - modifier.add(LogicalKeyboardKey.control); - if (needsExtraModifier) { - modifier.add(LogicalKeyboardKey.shift); - } - } - return modifier.length == 1 - ? LogicalKeySet(modifier[0], key) - : modifier.length == 2 - ? LogicalKeySet(modifier[0], modifier[1], key) - : throw ArgumentError.value( - modifier.length, 'modifier', 'Unexpected number of modifiers!'); -} diff --git a/studio/lib/shortcut/intents.dart b/studio/lib/shortcut/intents.dart deleted file mode 100644 index 2792ba7..0000000 --- a/studio/lib/shortcut/intents.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:xterm/terminal/terminal_ui_interaction.dart'; - -class ZoomInIntent extends Intent {} - -class ZoomOutIntent extends Intent {} - -class CopyIntent extends Intent {} - -class PasteIntent extends Intent {} - -class SelectAllIntent extends Intent {} - -class ClearIntent extends Intent {} - -class KillIntent extends Intent {} - -class SearchIntent extends Intent {} diff --git a/studio/lib/shortcut/terminal_shortcut.dart b/studio/lib/shortcut/terminal_shortcut.dart deleted file mode 100644 index 5091a7a..0000000 --- a/studio/lib/shortcut/terminal_shortcut.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:xterm/terminal/terminal_ui_interaction.dart'; - -class TerminalShortcut { - final Future Function(TerminalUiInteraction) _onExecute; - final Future Function(TerminalUiInteraction)? _onIsAvailable; - - final Intent intent; - - const TerminalShortcut( - {required this.name, - required Future Function(TerminalUiInteraction) onExecute, - Future Function(TerminalUiInteraction)? onIsAvailable, - required this.keyCombinations, - required this.intent}) - : _onExecute = onExecute, - _onIsAvailable = onIsAvailable; - - final List keyCombinations; - final String name; - - Future isAvailable(TerminalUiInteraction terminal) async => - _onIsAvailable == null ? true : await _onIsAvailable!(terminal); - - Future execute(TerminalUiInteraction terminal) async => - await _onExecute(terminal); -} diff --git a/studio/lib/terminal_search_bar.dart b/studio/lib/terminal_search_bar.dart deleted file mode 100644 index a8769ac..0000000 --- a/studio/lib/terminal_search_bar.dart +++ /dev/null @@ -1,264 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:xterm/terminal/terminal_ui_interaction.dart'; -import 'package:xterm/terminal/terminal_search.dart'; - -typedef SearchCloseRequestHandler = void Function(); - -class TerminalSearchBar extends StatefulWidget { - const TerminalSearchBar({ - Key? key, - required this.terminal, - required this.searchTextController, - required this.focusNode, - required this.closeRequestHandler, - this.itemSize = 20, - }) : super(key: key); - - final TerminalUiInteraction terminal; - final int itemSize; - final TextEditingController searchTextController; - final FocusNode focusNode; - final SearchCloseRequestHandler closeRequestHandler; - - @override - _TerminalSearchBarState createState() { - return _TerminalSearchBarState(); - } -} - -class _TerminalSearchBarState extends State { - int? _currentSearchHit = 0; - int _numberOfSearchHits = 0; - TerminalSearchOptions _options = TerminalSearchOptions(); - String _searchText = ''; - - void _onTerminalChanges() { - if (widget.terminal.currentSearchHit != _currentSearchHit || - widget.terminal.numberOfSearchHits != _numberOfSearchHits || - widget.terminal.userSearchOptions != _options) { - setState(() { - _currentSearchHit = widget.terminal.currentSearchHit; - _numberOfSearchHits = widget.terminal.numberOfSearchHits; - _options = widget.terminal.userSearchOptions; - }); - } - } - - void _onSearchTextChanges() { - if (_searchText != widget.searchTextController.text) { - setState(() { - _searchText = widget.searchTextController.text; - }); - } - } - - @override - void initState() { - widget.terminal.addListener(_onTerminalChanges); - widget.searchTextController.addListener(_onSearchTextChanges); - _searchText = widget.searchTextController.text; - _currentSearchHit = widget.terminal.currentSearchHit; - _numberOfSearchHits = widget.terminal.numberOfSearchHits; - _options = widget.terminal.userSearchOptions; - super.initState(); - } - - @override - void dispose() { - widget.terminal.removeListener(_onTerminalChanges); - widget.searchTextController.removeListener(_onSearchTextChanges); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final textColor = CupertinoColors.label; - final itemColor = CupertinoColors.secondaryLabel; - final itemColorSuffixActive = CupertinoColors.label; - final itemColorSuffixInactive = CupertinoColors.tertiaryLabel; - const searchBackgroundColor = CupertinoColors.secondarySystemBackground; - final resolvedSearchBackgroundColor = - CupertinoDynamicColor.resolve(searchBackgroundColor, context); - const placeholderColor = CupertinoColors.systemGrey; - final resolvedPlaceholderColor = - CupertinoDynamicColor.resolve(placeholderColor, context); - - final double scaledIconSize = - MediaQuery.textScaleFactorOf(context) * widget.itemSize; - final IconThemeData iconThemeData = IconThemeData( - color: CupertinoDynamicColor.resolve(itemColor, context), - size: scaledIconSize, - ); - final IconThemeData iconThemeDataSuffixActive = IconThemeData( - color: CupertinoDynamicColor.resolve(itemColorSuffixActive, context), - size: scaledIconSize, - ); - final IconThemeData iconThemeDataSuffixInactive = IconThemeData( - color: CupertinoDynamicColor.resolve(itemColorSuffixInactive, context), - size: scaledIconSize, - ); - const suffixPadding = EdgeInsets.fromLTRB(0, 0, 5, 0); - - final isUpEnabled = _currentSearchHit != null && _currentSearchHit! > 1; - final isDownEnabled = - _currentSearchHit != null && _currentSearchHit! < _numberOfSearchHits; - - return Column( - children: [ - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: CupertinoTextField( - controller: widget.searchTextController, - placeholder: "Search", - placeholderStyle: TextStyle(color: resolvedPlaceholderColor), - style: TextStyle(color: textColor), - prefix: Padding( - padding: const EdgeInsets.only(left: 5), - child: IconTheme( - data: iconThemeData, - child: const Icon(CupertinoIcons.search), - ), - ), - suffix: Row( - children: [ - Text("${_currentSearchHit ?? 0}/$_numberOfSearchHits"), - _TerminalSearchBarSuffixIcon( - padding: suffixPadding, - enabled: isUpEnabled, - active: isUpEnabled, - onPressed: () { - if (widget.terminal.currentSearchHit != null) { - widget.terminal.currentSearchHit = - widget.terminal.currentSearchHit! - 1; - } - widget.focusNode.requestFocus(); - }, - icon: CupertinoIcons.arrow_up, - tooltip: 'Previous search hit', - themeActive: iconThemeDataSuffixActive, - themeInactive: iconThemeDataSuffixInactive, - ), - _TerminalSearchBarSuffixIcon( - padding: suffixPadding, - enabled: isDownEnabled, - active: isDownEnabled, - onPressed: () { - if (widget.terminal.currentSearchHit != null) { - widget.terminal.currentSearchHit = - widget.terminal.currentSearchHit! + 1; - } - - widget.focusNode.requestFocus(); - }, - icon: CupertinoIcons.arrow_down, - tooltip: 'Next search hit', - themeActive: iconThemeDataSuffixActive, - themeInactive: iconThemeDataSuffixInactive, - ), - _TerminalSearchBarSuffixIcon( - padding: suffixPadding, - enabled: true, - active: _options.caseSensitive, - onPressed: () { - widget.terminal.userSearchOptions = - widget.terminal.userSearchOptions.copyWith( - caseSensitive: !widget.terminal - .userSearchOptions.caseSensitive); - widget.focusNode.requestFocus(); - }, - icon: CupertinoIcons.textformat_size, - tooltip: 'Case sensitivity', - themeActive: iconThemeDataSuffixActive, - themeInactive: iconThemeDataSuffixInactive, - ), - _TerminalSearchBarSuffixIcon( - padding: suffixPadding, - enabled: true, - active: _options.matchWholeWord, - onPressed: () { - widget.terminal.userSearchOptions = - widget.terminal.userSearchOptions.copyWith( - matchWholeWord: !widget.terminal - .userSearchOptions.matchWholeWord); - widget.focusNode.requestFocus(); - }, - icon: CupertinoIcons.textbox, - tooltip: 'Whole word', - themeActive: iconThemeDataSuffixActive, - themeInactive: iconThemeDataSuffixInactive, - ), - _TerminalSearchBarSuffixIcon( - padding: suffixPadding, - enabled: true, - active: true, - onPressed: widget.closeRequestHandler, - icon: CupertinoIcons.xmark_circle_fill, - tooltip: 'Close search', - themeActive: iconThemeDataSuffixActive, - themeInactive: iconThemeDataSuffixInactive, - ), - ], - ), - decoration: BoxDecoration( - color: resolvedSearchBackgroundColor, - borderRadius: BorderRadius.circular(9)), - autocorrect: false, - focusNode: widget.focusNode, - ), - ) - ], - ), - ), - ], - ); - } -} - -typedef OnPressedHandler = void Function(); - -class _TerminalSearchBarSuffixIcon extends StatelessWidget { - const _TerminalSearchBarSuffixIcon({ - Key? key, - required this.padding, - this.onPressed, - required this.enabled, - required this.active, - required this.themeActive, - required this.themeInactive, - required this.icon, - required this.tooltip, - }) : super(key: key); - - final EdgeInsetsGeometry padding; - final OnPressedHandler? onPressed; - final bool enabled; - final bool active; - final IconThemeData themeActive; - final IconThemeData themeInactive; - final IconData icon; - final String tooltip; - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding, - child: CupertinoButton( - onPressed: enabled ? onPressed : null, - minSize: 0, - padding: EdgeInsets.zero, - child: IconTheme( - data: active ? themeActive : themeInactive, - child: Tooltip( - message: tooltip, - waitDuration: const Duration(milliseconds: 500), - child: Icon(icon), - ), - ), - ), - ); - } -} diff --git a/studio/lib/utils/build_mode.dart b/studio/lib/utils/build_mode.dart deleted file mode 100644 index 413ba1e..0000000 --- a/studio/lib/utils/build_mode.dart +++ /dev/null @@ -1,25 +0,0 @@ -/// See: https://github.com/flutter/flutter/issues/11392 -/// -enum _BuildMode { - release, - debug, - profile, -} - -_BuildMode _buildMode = (() { - if (const bool.fromEnvironment('dart.vm.product')) { - return _BuildMode.release; - } - var result = _BuildMode.profile; - assert(() { - result = _BuildMode.debug; - return true; - }()); - return result; -}()); - -class BuildMode { - static bool isDebug = (_buildMode == _BuildMode.debug); - static bool isProfile = (_buildMode == _BuildMode.profile); - static bool isRelease = (_buildMode == _BuildMode.release); -} diff --git a/studio/lib/utils/pty_terminal_backend.dart b/studio/lib/utils/pty_terminal_backend.dart deleted file mode 100644 index ebd8114..0000000 --- a/studio/lib/utils/pty_terminal_backend.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:pty/pty.dart'; -import 'package:xterm/xterm.dart'; - -class PtyTerminalBackend implements TerminalBackend { - final PseudoTerminal pty; - - PtyTerminalBackend(this.pty); - - @override - void init() { - pty.init(); - } - - @override - Future get exitCode => pty.exitCode; - - @override - Stream get out => pty.out; - - @override - void resize(int width, int height, int pixelWidth, int pixelHeight) { - pty.resize(width, height); - } - - @override - void write(String input) { - pty.write(input); - } - - @override - void terminate() { - pty.kill(); - } - - @override - void ackProcessed() { - pty.ackProcessed(); - } -} diff --git a/studio/linux/flutter/generated_plugin_registrant.cc b/studio/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index d38195a..0000000 --- a/studio/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,9 +0,0 @@ -// -// Generated file. Do not edit. -// - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/studio/linux/my_application.cc b/studio/linux/my_application.cc deleted file mode 100644 index a1af26b..0000000 --- a/studio/linux/my_application.cc +++ /dev/null @@ -1,68 +0,0 @@ -#include "my_application.h" - -#include - -#include -#include - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication -{ - GtkApplication parent_instance; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -static void on_close() -{ - // kill(0, SIGTERM); - exit(0); -} - -// Implements GApplication::activate. -static void my_application_activate(GApplication *application) -{ - g_object_set(gtk_settings_get_default(), - "gtk-application-prefer-dark-theme", TRUE, - NULL); - - GtkWindow *window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - g_signal_connect(window, "delete_event", G_CALLBACK(on_close), NULL); - - // GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - // gtk_widget_show(GTK_WIDGET(header_bar)); - // gtk_header_bar_set_title(header_bar, "studio"); - // gtk_header_bar_set_show_close_button(header_bar, TRUE); - // gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - gtk_window_set_title(window, "Terminal Studio"); - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); - // gtk_widget_set_opacity (GTK_WIDGET(window), 0.5); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - - FlView *view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -static void my_application_class_init(MyApplicationClass *klass) -{ - G_APPLICATION_CLASS(klass)->activate = my_application_activate; -} - -static void my_application_init(MyApplication *self) {} - -MyApplication *my_application_new() -{ - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - nullptr)); -} diff --git a/studio/macos/Flutter/Flutter-Debug.xcconfig b/studio/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/studio/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/studio/macos/Flutter/Flutter-Release.xcconfig b/studio/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/studio/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/studio/macos/Flutter/GeneratedPluginRegistrant.swift b/studio/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index cccf817..0000000 --- a/studio/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { -} diff --git a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f..0000000 --- a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 3c4935a..0000000 Binary files a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index ed4cc16..0000000 Binary files a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 483be61..0000000 Binary files a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bcbf36d..0000000 Binary files a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 9c0a652..0000000 Binary files a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index e71a726..0000000 Binary files a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 8a31fe2..0000000 Binary files a/studio/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/studio/macos/Runner/MainFlutterWindow.swift b/studio/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 2d95594..0000000 --- a/studio/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - -// self.contentView?.window?.styleMask = NSWindow.NSwinds - // self.backgroundColor = .black -// self.titlebarAppearsTransparent = true - - // self.titleVisibility = NSWindow.TitleVisibility.hidden; - // self.titlebarAppearsTransparent = true; - // self.isMovableByWindowBackground = true; - // self.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isEnabled = false; - - // Transparent view - self.isOpaque = false - self.backgroundColor = .clear - - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/studio/pubspec.lock b/studio/pubspec.lock deleted file mode 100644 index 4a155ff..0000000 --- a/studio/pubspec.lock +++ /dev/null @@ -1,259 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - context_menu_macos: - dependency: "direct main" - description: - path: "../context_menu_macos" - relative: true - source: path - version: "0.0.1" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - equatable: - dependency: transitive - description: - name: equatable - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - ffi: - dependency: transitive - description: - name: ffi - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - http: - dependency: "direct main" - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.2" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.4" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.11" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - platform_info: - dependency: transitive - description: - name: platform_info - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0-nullsafety.1" - pty: - dependency: "direct main" - description: - path: "../pty" - relative: true - source: path - version: "0.2.2-pre" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - tabs: - dependency: "direct main" - description: - path: "../tabs" - relative: true - source: path - version: "0.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.3" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - web_socket_channel: - dependency: "direct main" - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - win32: - dependency: transitive - description: - name: win32 - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.8" - xterm: - dependency: "direct main" - description: - path: "../xterm" - relative: true - source: path - version: "2.5.0-pre" -sdks: - dart: ">=2.13.0 <3.0.0" - flutter: ">=2.0.0" diff --git a/studio/web/index.html b/studio/web/index.html deleted file mode 100644 index 56215b6..0000000 --- a/studio/web/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - studio - - - - - - - - diff --git a/studio/windows/flutter/generated_plugin_registrant.cc b/studio/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 4bfa0f3..0000000 --- a/studio/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,9 +0,0 @@ -// -// Generated file. Do not edit. -// - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/studio/windows/runner/CMakeLists.txt b/studio/windows/runner/CMakeLists.txt deleted file mode 100644 index 977e38b..0000000 --- a/studio/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -cmake_minimum_required(VERSION 3.15) -project(runner LANGUAGES CXX) - -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "run_loop.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) -apply_standard_settings(${BINARY_NAME}) -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/studio/windows/runner/resources/app_icon.ico b/studio/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20c..0000000 Binary files a/studio/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/studio/windows/runner/run_loop.cpp b/studio/windows/runner/run_loop.cpp deleted file mode 100644 index 2d6636a..0000000 --- a/studio/windows/runner/run_loop.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "run_loop.h" - -#include - -#include - -RunLoop::RunLoop() {} - -RunLoop::~RunLoop() {} - -void RunLoop::Run() { - bool keep_running = true; - TimePoint next_flutter_event_time = TimePoint::clock::now(); - while (keep_running) { - std::chrono::nanoseconds wait_duration = - std::max(std::chrono::nanoseconds(0), - next_flutter_event_time - TimePoint::clock::now()); - ::MsgWaitForMultipleObjects( - 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), - QS_ALLINPUT); - bool processed_events = false; - MSG message; - // All pending Windows messages must be processed; MsgWaitForMultipleObjects - // won't return again for items left in the queue after PeekMessage. - while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { - processed_events = true; - if (message.message == WM_QUIT) { - keep_running = false; - break; - } - ::TranslateMessage(&message); - ::DispatchMessage(&message); - // Allow Flutter to process messages each time a Windows message is - // processed, to prevent starvation. - next_flutter_event_time = - std::min(next_flutter_event_time, ProcessFlutterMessages()); - } - // If the PeekMessage loop didn't run, process Flutter messages. - if (!processed_events) { - next_flutter_event_time = - std::min(next_flutter_event_time, ProcessFlutterMessages()); - } - } -} - -void RunLoop::RegisterFlutterInstance( - flutter::FlutterEngine* flutter_instance) { - flutter_instances_.insert(flutter_instance); -} - -void RunLoop::UnregisterFlutterInstance( - flutter::FlutterEngine* flutter_instance) { - flutter_instances_.erase(flutter_instance); -} - -RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { - TimePoint next_event_time = TimePoint::max(); - for (auto instance : flutter_instances_) { - std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); - if (wait_duration != std::chrono::nanoseconds::max()) { - next_event_time = - std::min(next_event_time, TimePoint::clock::now() + wait_duration); - } - } - return next_event_time; -} diff --git a/studio/windows/runner/run_loop.h b/studio/windows/runner/run_loop.h deleted file mode 100644 index 000d362..0000000 --- a/studio/windows/runner/run_loop.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef RUNNER_RUN_LOOP_H_ -#define RUNNER_RUN_LOOP_H_ - -#include - -#include -#include - -// A runloop that will service events for Flutter instances as well -// as native messages. -class RunLoop { - public: - RunLoop(); - ~RunLoop(); - - // Prevent copying - RunLoop(RunLoop const&) = delete; - RunLoop& operator=(RunLoop const&) = delete; - - // Runs the run loop until the application quits. - void Run(); - - // Registers the given Flutter instance for event servicing. - void RegisterFlutterInstance( - flutter::FlutterEngine* flutter_instance); - - // Unregisters the given Flutter instance from event servicing. - void UnregisterFlutterInstance( - flutter::FlutterEngine* flutter_instance); - - private: - using TimePoint = std::chrono::steady_clock::time_point; - - // Processes all currently pending messages for registered Flutter instances. - TimePoint ProcessFlutterMessages(); - - std::set flutter_instances_; -}; - -#endif // RUNNER_RUN_LOOP_H_ diff --git a/tabs b/tabs deleted file mode 160000 index 94cb30e..0000000 --- a/tabs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 94cb30e129ffcb3d4ecb3d4d307cdbb1f0639eab diff --git a/studio/test/widget_test.dart b/test/widget_test.dart similarity index 84% rename from studio/test/widget_test.dart rename to test/widget_test.dart index 268baff..84bf7fa 100644 --- a/studio/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,19 +1,19 @@ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll +// utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:studio/main.dart'; +import 'package:terminal_studio/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/studio/web/favicon.png b/web/favicon.png similarity index 100% rename from studio/web/favicon.png rename to web/favicon.png diff --git a/studio/web/icons/Icon-192.png b/web/icons/Icon-192.png similarity index 100% rename from studio/web/icons/Icon-192.png rename to web/icons/Icon-192.png diff --git a/studio/web/icons/Icon-512.png b/web/icons/Icon-512.png similarity index 100% rename from studio/web/icons/Icon-512.png rename to web/icons/Icon-512.png diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..a00bcc9 --- /dev/null +++ b/web/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + TerminalStudio + + + + + + + + + + diff --git a/studio/web/manifest.json b/web/manifest.json similarity index 62% rename from studio/web/manifest.json rename to web/manifest.json index 8b912c3..0e75630 100644 --- a/studio/web/manifest.json +++ b/web/manifest.json @@ -18,6 +18,18 @@ "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/studio/windows/.gitignore b/windows/.gitignore similarity index 100% rename from studio/windows/.gitignore rename to windows/.gitignore diff --git a/studio/windows/CMakeLists.txt b/windows/CMakeLists.txt similarity index 84% rename from studio/windows/CMakeLists.txt rename to windows/CMakeLists.txt index ce34a2a..8f88d7a 100644 --- a/studio/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -1,13 +1,16 @@ -cmake_minimum_required(VERSION 3.15) +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) project(studio LANGUAGES CXX) +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. set(BINARY_NAME "studio") +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. cmake_policy(SET CMP0063 NEW) -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. +# Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" @@ -20,7 +23,7 @@ else() "Debug" "Profile" "Release") endif() endif() - +# Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") @@ -30,6 +33,10 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") @@ -38,12 +45,11 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) -# Application build +# Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding diff --git a/studio/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt similarity index 96% rename from studio/windows/flutter/CMakeLists.txt rename to windows/flutter/CMakeLists.txt index b02c548..930d207 100644 --- a/studio/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -1,4 +1,5 @@ -cmake_minimum_required(VERSION 3.15) +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..1f60638 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterAcrylicPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/studio/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h similarity index 93% rename from studio/windows/flutter/generated_plugin_registrant.h rename to windows/flutter/generated_plugin_registrant.h index 9846246..dc139d8 100644 --- a/studio/windows/flutter/generated_plugin_registrant.h +++ b/windows/flutter/generated_plugin_registrant.h @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/studio/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake similarity index 55% rename from studio/windows/flutter/generated_plugins.cmake rename to windows/flutter/generated_plugins.cmake index 4d10c25..8524440 100644 --- a/studio/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,14 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_acrylic + screen_retriever + url_launcher_windows + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_pty ) set(PLUGIN_BUNDLED_LIBRARIES) @@ -13,3 +21,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/packaging/msix/make_config.yaml b/windows/packaging/msix/make_config.yaml new file mode 100644 index 0000000..e1cdad7 --- /dev/null +++ b/windows/packaging/msix/make_config.yaml @@ -0,0 +1,3 @@ +display_name: TerminalStudio +# sign_msix: "false" +install_certificate: "false" diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..b9e550f --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/studio/windows/runner/Runner.rc b/windows/runner/Runner.rc similarity index 91% rename from studio/windows/runner/Runner.rc rename to windows/runner/Runner.rc index 1bbed0e..110a46c 100644 --- a/studio/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -1,121 +1,121 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER -#else -#define VERSION_AS_NUMBER 1,0,0 -#endif - -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "A new Flutter project." "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "studio" "\0" - VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "studio.exe" "\0" - VALUE "ProductName", "studio" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "studio" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "studio" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "studio.exe" "\0" + VALUE "ProductName", "studio" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/studio/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp similarity index 80% rename from studio/windows/runner/flutter_window.cpp rename to windows/runner/flutter_window.cpp index c422723..b43b909 100644 --- a/studio/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -4,9 +4,8 @@ #include "flutter/generated_plugin_registrant.h" -FlutterWindow::FlutterWindow(RunLoop* run_loop, - const flutter::DartProject& project) - : run_loop_(run_loop), project_(project) {} +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} FlutterWindow::~FlutterWindow() {} @@ -26,14 +25,12 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); - run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { - run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); flutter_controller_ = nullptr; } @@ -44,7 +41,7 @@ LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opporutunity to handle window messages. + // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, diff --git a/studio/windows/runner/flutter_window.h b/windows/runner/flutter_window.h similarity index 71% rename from studio/windows/runner/flutter_window.h rename to windows/runner/flutter_window.h index b663ddd..6da0652 100644 --- a/studio/windows/runner/flutter_window.h +++ b/windows/runner/flutter_window.h @@ -6,16 +6,13 @@ #include -#include "run_loop.h" #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: - // Creates a new FlutterWindow driven by the |run_loop|, hosting a - // Flutter view running |project|. - explicit FlutterWindow(RunLoop* run_loop, - const flutter::DartProject& project); + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: @@ -26,9 +23,6 @@ class FlutterWindow : public Win32Window { LPARAM const lparam) noexcept override; private: - // The run loop driving events for this window. - RunLoop* run_loop_; - // The project to run. flutter::DartProject project_; diff --git a/studio/windows/runner/main.cpp b/windows/runner/main.cpp similarity index 87% rename from studio/windows/runner/main.cpp rename to windows/runner/main.cpp index bca924b..5edc61b 100644 --- a/studio/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -3,7 +3,6 @@ #include #include "flutter_window.h" -#include "run_loop.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, @@ -18,8 +17,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - RunLoop run_loop; - flutter::DartProject project(L"data"); std::vector command_line_arguments = @@ -27,7 +24,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - FlutterWindow window(&run_loop, project); + FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.CreateAndShow(L"studio", origin, size)) { @@ -35,7 +32,11 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, } window.SetQuitOnClose(true); - run_loop.Run(); + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } ::CoUninitialize(); return EXIT_SUCCESS; diff --git a/studio/windows/runner/resource.h b/windows/runner/resource.h similarity index 100% rename from studio/windows/runner/resource.h rename to windows/runner/resource.h diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..4054796 Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/studio/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest similarity index 100% rename from studio/windows/runner/runner.exe.manifest rename to windows/runner/runner.exe.manifest diff --git a/studio/windows/runner/utils.cpp b/windows/runner/utils.cpp similarity index 94% rename from studio/windows/runner/utils.cpp rename to windows/runner/utils.cpp index d19bdbb..f5bf9fa 100644 --- a/studio/windows/runner/utils.cpp +++ b/windows/runner/utils.cpp @@ -48,10 +48,10 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, diff --git a/studio/windows/runner/utils.h b/windows/runner/utils.h similarity index 100% rename from studio/windows/runner/utils.h rename to windows/runner/utils.h diff --git a/studio/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp similarity index 100% rename from studio/windows/runner/win32_window.cpp rename to windows/runner/win32_window.cpp diff --git a/studio/windows/runner/win32_window.h b/windows/runner/win32_window.h similarity index 100% rename from studio/windows/runner/win32_window.h rename to windows/runner/win32_window.h diff --git a/xterm b/xterm deleted file mode 160000 index ef295ec..0000000 --- a/xterm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ef295eca8a08ccada58ede399d9b860a08ff1389