Ver código fonte

Merge 'v3' to 'master'

xuty 3 anos atrás
pai
commit
b0ad0c00b8
100 arquivos alterados com 4859 adições e 3827 exclusões
  1. 3 0
      .gitignore
  2. 29 0
      CHANGELOG.md
  3. 29 0
      analysis_options.yaml
  4. 57 0
      bin/xterm_bench.dart
  5. 116 0
      bin/xterm_dump.dart
  6. 38 3
      example/.metadata
  7. 23 25
      example/analysis_options.yaml
  8. 2 0
      example/android/.gitignore
  9. 12 7
      example/android/app/build.gradle
  10. 3 16
      example/android/app/src/main/AndroidManifest.xml
  11. 1 1
      example/android/app/src/main/res/values-night/styles.xml
  12. 5 5
      example/android/app/src/main/res/values/styles.xml
  13. 4 4
      example/android/build.gradle
  14. 0 1
      example/android/gradle.properties
  15. 1 1
      example/android/gradle/wrapper/gradle-wrapper.properties
  16. 1 1
      example/ios/Flutter/AppFrameworkInfo.plist
  17. 1 0
      example/ios/Flutter/Debug.xcconfig
  18. 1 0
      example/ios/Flutter/Release.xcconfig
  19. 41 0
      example/ios/Podfile
  20. 22 9
      example/ios/Runner.xcodeproj/project.pbxproj
  21. 1 1
      example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  22. 1 1
      example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  23. 0 150
      example/lib/isolate.dart
  24. 0 100
      example/lib/local.dart
  25. 75 62
      example/lib/main.dart
  26. 107 0
      example/lib/mock.dart
  27. 53 83
      example/lib/ssh.dart
  28. 4 0
      example/linux/flutter/generated_plugin_registrant.cc
  29. 2 0
      example/linux/flutter/generated_plugins.cmake
  30. 1 0
      example/macos/.gitignore
  31. 1 0
      example/macos/Flutter/Flutter-Debug.xcconfig
  32. 1 0
      example/macos/Flutter/Flutter-Release.xcconfig
  33. 2 0
      example/macos/Flutter/GeneratedPluginRegistrant.swift
  34. 40 0
      example/macos/Podfile
  35. 28 0
      example/macos/Podfile.lock
  36. 65 17
      example/macos/Runner.xcodeproj/project.pbxproj
  37. 4 18
      example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  38. 3 0
      example/macos/Runner.xcworkspace/contents.xcworkspacedata
  39. 4 0
      example/macos/Runner/Base.lproj/MainMenu.xib
  40. 1 1
      example/macos/Runner/Configs/AppInfo.xcconfig
  41. 0 16
      example/macos/Runner/DebugProfile.entitlements
  42. 10 6
      example/macos/Runner/MainFlutterWindow.swift
  43. 1 18
      example/macos/Runner/Release.entitlements
  44. 146 31
      example/pubspec.lock
  45. 14 7
      example/pubspec.yaml
  46. 0 30
      example/test/widget_test.dart
  47. 3 0
      example/windows/flutter/generated_plugin_registrant.cc
  48. 2 0
      example/windows/flutter/generated_plugins.cmake
  49. 0 0
      fixture/htop_80x25_3s.txt
  50. 0 555
      lib/buffer/buffer.dart
  51. 0 266
      lib/buffer/line/line.dart
  52. 0 293
      lib/buffer/line/line_bytedata_data.dart
  53. 0 267
      lib/buffer/line/line_list_data.dart
  54. 0 11
      lib/buffer/reflow_strategy.dart
  55. 0 91
      lib/buffer/reflow_strategy_narrower.dart
  56. 0 76
      lib/buffer/reflow_strategy_wider.dart
  57. 2 0
      lib/core.dart
  58. 534 0
      lib/core/buffer/buffer.dart
  59. 0 0
      lib/core/buffer/cell_flags.dart
  60. 295 0
      lib/core/buffer/line.dart
  61. 55 0
      lib/core/buffer/position.dart
  62. 71 0
      lib/core/buffer/range.dart
  63. 51 0
      lib/core/buffer/segment.dart
  64. 66 0
      lib/core/cell.dart
  65. 0 0
      lib/core/charset.dart
  66. 19 0
      lib/core/color.dart
  67. 137 0
      lib/core/cursor.dart
  68. 29 0
      lib/core/escape/emitter.dart
  69. 211 0
      lib/core/escape/handler.dart
  70. 1095 0
      lib/core/escape/parser.dart
  71. 131 0
      lib/core/input/handler.dart
  72. 0 0
      lib/core/input/keys.dart
  73. 110 0
      lib/core/input/keytab/keytab.dart
  74. 2 2
      lib/core/input/keytab/keytab_default.dart
  75. 1 1
      lib/core/input/keytab/keytab_escape.dart
  76. 5 5
      lib/core/input/keytab/keytab_parse.dart
  77. 34 26
      lib/core/input/keytab/keytab_record.dart
  78. 0 0
      lib/core/input/keytab/keytab_token.dart
  79. 1 1
      lib/core/input/keytab/qt_keyname.dart
  80. 23 0
      lib/core/mouse.dart
  81. 157 0
      lib/core/reflow.dart
  82. 3 0
      lib/core/snapshot.dart
  83. 40 0
      lib/core/state.dart
  84. 46 0
      lib/core/tabs.dart
  85. 783 0
      lib/core/terminal.dart
  86. 0 1
      lib/flutter.dart
  87. 0 30
      lib/frontend/char_size.dart
  88. 0 23
      lib/frontend/helpers.dart
  89. 0 17
      lib/frontend/input_behavior.dart
  90. 0 84
      lib/frontend/input_behavior_default.dart
  91. 0 5
      lib/frontend/input_behavior_desktop.dart
  92. 0 43
      lib/frontend/input_behavior_mobile.dart
  93. 0 18
      lib/frontend/input_behaviors.dart
  94. 0 268
      lib/frontend/input_listener.dart
  95. 0 28
      lib/frontend/mouse_listener.dart
  96. 0 78
      lib/frontend/oscillator.dart
  97. 0 423
      lib/frontend/terminal_painters.dart
  98. 0 563
      lib/frontend/terminal_view.dart
  99. 0 37
      lib/input/keytab/keytab.dart
  100. 0 1
      lib/input/shortcut.dart

+ 3 - 0
.gitignore

@@ -73,3 +73,6 @@ build/
 !**/ios/**/default.pbxuser
 !**/ios/**/default.perspectivev3
 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+
+.vscode/
+example/lib/debug.dart

+ 29 - 0
CHANGELOG.md

@@ -1,3 +1,32 @@
+## [3.0.6-alpha] - 2022-4-4
+* Export `TerminalViewState`
+* Added `onTap` callback to `TerminalView`
+
+## [3.0.5-alpha] - 2022-4-4
+* Avoid resize when `RenderBox.size` is zero.
+* Added `charInput` and `textInput`method.
+* Added `requestKeyboard`, `closeKeyboard` and `hasInputConnection`method.
+* Export `KeyboardVisibilty`
+
+## [3.0.4-alpha] - 2022-4-1
+* Improved text editing
+* Added composing state painting
+* Adapt to `MediaQuery.padding`
+
+## [3.0.3-alpha] - 2022-3-28
+* Improved scroll handing
+* Improved resize handing
+* Fix focus repaint
+* Fix OSC title update
+
+## [3.0.2-alpha] - 2022-3-28
+* Re-design `KeyboardVisibilty`
+
+## [3.0.1-alpha] - 2022-3-27
+* Add `KeyboardVisibilty`
+
+## [3.0.0-alpha] - 2022-3-26
+* Initial release of v3.
 
 ## [2.6.0] - 2021-12-28
 * Add scrollBehavior field to the TerminalView class [#55].

+ 29 - 0
analysis_options.yaml

@@ -0,0 +1,29 @@
+include: package:lints/recommended.yaml
+
+linter:
+  rules:
+    prefer_function_declarations_over_variables: false
+    prefer_conditional_assignment: false
+
+analyzer:
+  plugins:
+    - dart_code_metrics
+
+dart_code_metrics:
+  anti-patterns:
+  # - long-method
+  # - long-parameter-list
+  metrics:
+    cyclomatic-complexity: 20
+    maximum-nesting-level: 5
+    number-of-parameters: 4
+    source-lines-of-code: 50
+  metrics-exclude:
+    - test/**
+  rules:
+    # - no-boolean-literal-compare
+    # - no-empty-block
+    - prefer-trailing-comma
+    - no-equal-then-else
+  rules-exclude:
+    - prefer-conditional-expressions

+ 57 - 0
bin/xterm_bench.dart

@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+import 'package:xterm/core/terminal.dart';
+
+class Test extends StatelessWidget {
+  const Test({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return TextSelectionToolbar(
+      anchorAbove: Offset(50, 50),
+      anchorBelow: Offset(50, 50),
+      children: [
+        TextSelectionToolbarTextButton(
+          child: Text('Copy'),
+          onPressed: () {},
+          padding: TextSelectionToolbarTextButton.getPadding(0, 1),
+        ),
+        TextSelectionToolbarTextButton(
+          child: Text('Paste'),
+          onPressed: () {},
+          padding: TextSelectionToolbarTextButton.getPadding(1, 1),
+        ),
+      ],
+    );
+  }
+}
+
+void main(List<String> args) async {
+  final lines = 1000;
+
+  final terminal = Terminal(maxLines: lines);
+
+  bench('write $lines lines', () {
+    for (var i = 0; i < lines; i++) {
+      terminal.write('https://github.com/TerminalStudio/dartssh2\r\n');
+    }
+  });
+
+  final regexp = RegExp(
+    r'[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)',
+  );
+
+  bench('search $lines line', () {
+    var count = 0;
+    for (var line in terminal.lines.toList()) {
+      final matches = regexp.allMatches(line.toString());
+      count += matches.length;
+    }
+    print('count: $count');
+  });
+}
+
+void bench(String description, void Function() f) {
+  final sw = Stopwatch()..start();
+  f();
+  print('$description took ${sw.elapsedMilliseconds}ms');
+}

+ 116 - 0
bin/xterm_dump.dart

@@ -0,0 +1,116 @@
+// import 'dart:convert';
+// import 'dart:io';
+// import 'dart:typed_data';
+
+// import 'package:xterm/core/escape/handler.dart';
+// import 'package:xterm/core/escape/parser.dart';
+
+// final handler = DebugTerminalHandler();
+// final protocol = EscapeParser(handler);
+// final input = BytesBuilder(copy: true);
+
+// void main(List<String> args) async {
+//   final inputStream = args.isNotEmpty ? File(args.first).openRead() : stdin;
+
+//   await for (var chunk in inputStream.transform(Utf8Decoder())) {
+//     input.add(chunk);
+//     protocol.write(chunk);
+//   }
+
+//   handler.flush();
+// }
+
+// extension StringEscape on String {
+//   String escapeInvisible() {
+//     return this.replaceAllMapped(RegExp('[\x00-\x1F]'), (match) {
+//       return '\\x${match.group(0)!.codeUnitAt(0).toRadixString(16).padLeft(2, '0')}';
+//     });
+//   }
+// }
+
+// class DebugTerminalHandler implements EscapeHandler {
+//   final stringBuffer = StringBuffer();
+
+//   void flush() {
+//     if (stringBuffer.isEmpty) return;
+//     print(Color.green('TXT') + "'$stringBuffer'");
+//     stringBuffer.clear();
+//   }
+
+//   void recordCommand(String description) {
+//     flush();
+//     final raw = input.toBytes().sublist(protocol.tokenBegin, protocol.tokenEnd);
+//     final token = utf8.decode(raw).replaceAll('\x1b', 'ESC').escapeInvisible();
+//     print(Color.magenta('CMD ') + token.padRight(40) + '$description');
+//   }
+
+//   @override
+//   void writeChar(int char) {
+//     stringBuffer.writeCharCode(char);
+//   }
+
+//   @override
+//   void setCursor(int x, int y) {
+//     recordCommand('setCursor $x, $y');
+//   }
+
+//   @override
+//   void designateCharset(int charset) {
+//     recordCommand('designateCharset $charset');
+//   }
+
+//   @override
+//   void unkownEscape(int char) {
+//     recordCommand('unkownEscape ${String.fromCharCode(char)}');
+//   }
+
+//   @override
+//   void backspaceReturn() {
+//     recordCommand('backspaceReturn');
+//   }
+
+//   @override
+//   void carriageReturn() {
+//     recordCommand('carriageReturn');
+//   }
+
+//   @override
+//   void setCursorX(int x) {
+//     recordCommand('setCursorX $x');
+//   }
+
+//   @override
+//   void setCursorY(int y) {
+//     recordCommand('setCursorY $y');
+//   }
+
+//   @override
+//   void unkownCSI(int finalByte) {
+//     recordCommand('unkownCSI ${String.fromCharCode(finalByte)}');
+//   }
+
+//   @override
+//   void unkownSBC(int char) {
+//     recordCommand('unkownSBC ${String.fromCharCode(char)}');
+//   }
+
+//   @override
+//   noSuchMethod(Invocation invocation) {
+//     final name = invocation.memberName;
+//     final args = invocation.positionalArguments;
+//     recordCommand('noSuchMethod: $name $args');
+//   }
+// }
+
+// abstract class Color {
+//   static String red(String s) => '\u001b[31m$s\u001b[0m';
+//   static String green(String s) => '\u001b[32m$s\u001b[0m';
+//   static String yellow(String s) => '\u001b[33m$s\u001b[0m';
+//   static String blue(String s) => '\u001b[34m$s\u001b[0m';
+//   static String magenta(String s) => '\u001b[35m$s\u001b[0m';
+//   static String cyan(String s) => '\u001b[36m$s\u001b[0m';
+// }
+
+// abstract class Labels {
+//   static final txt = Color.green('TXT');
+// }

+ 38 - 3
example/.metadata

@@ -1,10 +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 and should not be manually edited.
+# This file should be version controlled.
 
 version:
-  revision: a5fa083906fcaf88b039a717c6e78b9814f3a77c
-  channel: master
+  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'

+ 23 - 25
example/analysis_options.yaml

@@ -1,29 +1,27 @@
-# 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
+include: package:lints/recommended.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
+    prefer_function_declarations_over_variables: false
+
+analyzer:
+  plugins:
+    - dart_code_metrics
 
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
+dart_code_metrics:
+  anti-patterns:
+  # - long-method
+  # - long-parameter-list
+  metrics:
+    cyclomatic-complexity: 20
+    maximum-nesting-level: 5
+    number-of-parameters: 4
+    source-lines-of-code: 50
+  metrics-exclude:
+    - test/**
+  rules:
+    # - no-boolean-literal-compare
+    # - no-empty-block
+    - prefer-trailing-comma
+    - prefer-conditional-expressions
+    - no-equal-then-else

+ 2 - 0
example/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

+ 12 - 7
example/android/app/build.gradle

@@ -26,21 +26,26 @@ apply plugin: 'kotlin-android'
 apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 
 android {
-    compileSdkVersion 28
+    compileSdkVersion flutter.compileSdkVersion
 
-    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.example"
-        minSdkVersion 16
-        targetSdkVersion 28
+        minSdkVersion flutter.minSdkVersion
+        targetSdkVersion flutter.targetSdkVersion
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
     }

+ 3 - 16
example/android/app/src/main/AndroidManifest.xml

@@ -1,16 +1,12 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.example.example">
-    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
-         calls FlutterMain.startInitialization(this); in its onCreate method.
-         In most cases you can leave this as-is, but you if you want to provide
-         additional functionality it is fine to subclass or reimplement
-         FlutterApplication and put your custom class here. -->
-    <application
-        android:name="${applicationName}"
+   <application
         android:label="example"
+        android:name="${applicationName}"
         android:icon="@mipmap/ic_launcher">
         <activity
             android:name=".MainActivity"
+            android:exported="true"
             android:launchMode="singleTop"
             android:theme="@style/LaunchTheme"
             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -24,15 +20,6 @@
               android:name="io.flutter.embedding.android.NormalTheme"
               android:resource="@style/NormalTheme"
               />
-            <!-- Displays an Android View that continues showing the launch screen
-                 Drawable until Flutter paints its first frame, then this splash
-                 screen fades out. A splash screen is useful to avoid any visual
-                 gap between the end of Android's launch screen and the painting of
-                 Flutter's first frame. -->
-            <meta-data
-              android:name="io.flutter.embedding.android.SplashScreenDrawable"
-              android:resource="@drawable/launch_background"
-              />
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>

+ 1 - 1
example/android/app/src/main/res/values-night/styles.xml

@@ -10,7 +10,7 @@
          This theme determines the color of the Android Window while your
          Flutter UI initializes, as well as behind your Flutter UI while its
          running.
-         
+
          This Theme is only used starting with V2 of Flutter's Android embedding. -->
     <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
         <item name="android:windowBackground">?android:colorBackground</item>

+ 5 - 5
example/android/app/src/main/res/values/styles.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <!-- Theme applied to the Android Window while the process is starting -->
-    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
         <!-- Show a splash screen on the activity. Automatically removed when
              Flutter draws its first frame -->
         <item name="android:windowBackground">@drawable/launch_background</item>
@@ -10,9 +10,9 @@
          This theme determines the color of the Android Window while your
          Flutter UI initializes, as well as behind your Flutter UI while its
          running.
-         
+
          This Theme is only used starting with V2 of Flutter's Android embedding. -->
-    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
-        <item name="android:windowBackground">@android:color/white</item>
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
     </style>
 </resources>

+ 4 - 4
example/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:4.1.0'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
 }
@@ -14,7 +14,7 @@ buildscript {
 allprojects {
     repositories {
         google()
-        jcenter()
+        mavenCentral()
     }
 }
 

+ 0 - 1
example/android/gradle.properties

@@ -1,4 +1,3 @@
 org.gradle.jvmargs=-Xmx1536M
-android.enableR8=true
 android.useAndroidX=true
 android.enableJetifier=true

+ 1 - 1
example/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-6.7-all.zip

+ 1 - 1
example/ios/Flutter/AppFrameworkInfo.plist

@@ -21,6 +21,6 @@
   <key>CFBundleVersion</key>
   <string>1.0</string>
   <key>MinimumOSVersion</key>
-  <string>8.0</string>
+  <string>9.0</string>
 </dict>
 </plist>

+ 1 - 0
example/ios/Flutter/Debug.xcconfig

@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
 #include "Generated.xcconfig"

+ 1 - 0
example/ios/Flutter/Release.xcconfig

@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
 #include "Generated.xcconfig"

+ 41 - 0
example/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

+ 22 - 9
example/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,13 +288,17 @@
 				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";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
@@ -354,7 +358,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 +407,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,13 +425,17 @@
 				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";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
@@ -447,13 +456,17 @@
 				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";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",

+ 1 - 1
example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -2,6 +2,6 @@
 <Workspace
    version = "1.0">
    <FileRef
-      location = "group:Runner.xcodeproj">
+      location = "self:">
    </FileRef>
 </Workspace>

+ 1 - 1
example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1020"
+   LastUpgradeVersion = "1300"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"

+ 0 - 150
example/lib/isolate.dart

@@ -1,150 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:xterm/flutter.dart';
-import 'package:xterm/isolate.dart';
-import 'package:xterm/theme/terminal_theme.dart';
-import 'package:xterm/theme/terminal_themes.dart';
-import 'package:xterm/xterm.dart';
-
-void main() {
-  runApp(MyApp());
-}
-
-class MyApp extends StatelessWidget {
-  @override
-  Widget build(BuildContext context) {
-    return MaterialApp(
-      title: 'xterm.dart demo',
-      theme: ThemeData(
-        primarySwatch: Colors.blue,
-        visualDensity: VisualDensity.adaptivePlatformDensity,
-      ),
-      home: MyHomePage(
-        theme: TerminalThemes.defaultTheme,
-        terminalOpacity: 0.8,
-      ),
-    );
-  }
-}
-
-class MyHomePage extends StatefulWidget {
-  MyHomePage({
-    Key? key,
-    required this.theme,
-    required this.terminalOpacity,
-  }) : super(key: key);
-
-  final TerminalTheme theme;
-  final double terminalOpacity;
-
-  @override
-  _MyHomePageState createState() => _MyHomePageState();
-}
-
-class FakeTerminalBackend extends TerminalBackend {
-  // we do a late initialization of those backend members as the backend gets
-  // transferred into the Isolate.
-  // It is not allowed to e.g. transfer closures which we can not guarantee
-  // to not exist in our member types.
-  // The Isolate will call init() once it starts (from its context) and that is
-  // the place where we initialize those members
-  late final _exitCodeCompleter;
-  // ignore: close_sinks
-  late final _outStream;
-
-  @override
-  bool get isReady => true;
-
-  @override
-  Future<int> get exitCode => _exitCodeCompleter.future;
-
-  @override
-  void init() {
-    _exitCodeCompleter = Completer<int>();
-    _outStream = StreamController<String>();
-    _outStream.sink.add('xterm.dart demo');
-    _outStream.sink.add('\r\n');
-    _outStream.sink.add('\$ ');
-  }
-
-  @override
-  Stream<String> get out => _outStream.stream;
-
-  @override
-  void resize(int width, int height, int pixelWidth, int pixelHeight) {
-    // NOOP
-  }
-
-  @override
-  void write(String input) {
-    if (input.length <= 0) {
-      return;
-    }
-    // in a "real" terminal emulation you would connect onInput to the backend
-    // (like a pty or ssh connection) that then handles the changes in the
-    // terminal.
-    // As we don't have a connected backend here we simulate the changes by
-    // directly writing to the terminal.
-    if (input == '\r') {
-      _outStream.sink.add('\r\n');
-      _outStream.sink.add('\$ ');
-    } else if (input.codeUnitAt(0) == 127) {
-      // Backspace handling
-      _outStream.sink.add('\b \b');
-    } else {
-      _outStream.sink.add(input);
-    }
-  }
-
-  @override
-  void terminate() {
-    //NOOP
-  }
-
-  @override
-  void ackProcessed() {
-    //NOOP
-  }
-}
-
-class _MyHomePageState extends State<MyHomePage> {
-  TerminalIsolate? terminal;
-
-  Future<TerminalIsolate> _ensureTerminalStarted() async {
-    if (terminal == null) {
-      terminal = TerminalIsolate(
-        backend: FakeTerminalBackend(),
-        maxLines: 10000,
-        theme: widget.theme,
-      );
-    }
-
-    if (!terminal!.isReady) {
-      await terminal!.start();
-    }
-    return terminal!;
-  }
-
-  void onInput(String input) {}
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      body: FutureBuilder(
-        future: _ensureTerminalStarted(),
-        builder: (context, snapshot) {
-          return SafeArea(
-            child: snapshot.hasData
-                ? TerminalView(terminal: snapshot.data as TerminalIsolate)
-                : Container(
-                    constraints: const BoxConstraints.expand(),
-                    color: Color(widget.theme.background)
-                        .withOpacity(widget.terminalOpacity),
-                  ),
-          );
-        },
-      ),
-    );
-  }
-}

+ 0 - 100
example/lib/local.dart

@@ -1,100 +0,0 @@
-import 'dart:io';
-
-import 'package:flutter/material.dart';
-import 'package:pty/pty.dart';
-import 'package:xterm/flutter.dart';
-import 'package:xterm/xterm.dart';
-
-void main() {
-  runApp(MyApp());
-}
-
-class MyApp extends StatelessWidget {
-  @override
-  Widget build(BuildContext context) {
-    return MaterialApp(
-      title: 'xterm.dart demo',
-      theme: ThemeData(
-        primarySwatch: Colors.blue,
-        visualDensity: VisualDensity.adaptivePlatformDensity,
-      ),
-      home: LocalTerminal(),
-    );
-  }
-}
-
-class LocalTerminalBackend extends TerminalBackend {
-  LocalTerminalBackend();
-
-  final pty = PseudoTerminal.start(
-    '/system/bin/sh',
-    [],
-    blocking: false,
-    environment: Platform.environment,
-  );
-
-  @override
-  bool get isReady => true;
-
-  @override
-  Future<int> get exitCode => pty.exitCode;
-
-  @override
-  void init() {
-    pty.init();
-  }
-
-  @override
-  Stream<String> 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() {
-    // client.disconnect('terminate');
-  }
-
-  @override
-  void ackProcessed() {
-    // NOOP
-  }
-}
-
-class LocalTerminal extends StatefulWidget {
-  const LocalTerminal({Key? key}) : super(key: key);
-
-  @override
-  _LocalTerminalState createState() => _LocalTerminalState();
-}
-
-class _LocalTerminalState extends State<LocalTerminal> {
-  final terminal = Terminal(maxLines: 10000, backend: LocalTerminalBackend());
-
-  @override
-  void initState() {
-    super.initState();
-  }
-
-  void onInput(String input) {
-    print('input: $input');
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      body: SafeArea(
-        child: TerminalView(
-          terminal: terminal,
-        ),
-      ),
-    );
-  }
-}

+ 75 - 62
example/lib/main.dart

@@ -1,22 +1,44 @@
-import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:xterm/flutter.dart';
+import 'package:flutter_acrylic/flutter_acrylic.dart';
+import 'package:flutter_pty/flutter_pty.dart';
 import 'package:xterm/xterm.dart';
 
 void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+
+  if (isDesktop) {
+    setupAcrylic();
+  }
+
   runApp(MyApp());
 }
 
+bool get isDesktop {
+  if (kIsWeb) return false;
+  return [
+    TargetPlatform.windows,
+    TargetPlatform.linux,
+    TargetPlatform.macOS,
+  ].contains(defaultTargetPlatform);
+}
+
+Future<void> setupAcrylic() async {
+  await Window.initialize();
+  await Window.makeTitlebarTransparent();
+  await Window.setEffect(effect: WindowEffect.aero);
+  await Window.setBlurViewState(MacOSBlurViewState.active);
+}
+
 class MyApp extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       title: 'xterm.dart demo',
-      theme: ThemeData(
-        primarySwatch: Colors.blue,
-        visualDensity: VisualDensity.adaptivePlatformDensity,
-      ),
+      debugShowCheckedModeBanner: false,
       home: MyHomePage(),
     );
   }
@@ -26,84 +48,75 @@ class MyHomePage extends StatefulWidget {
   MyHomePage({Key? key}) : super(key: key);
 
   @override
+  // ignore: library_private_types_in_public_api
   _MyHomePageState createState() => _MyHomePageState();
 }
 
-class FakeTerminalBackend extends TerminalBackend {
-  final _exitCodeCompleter = Completer<int>();
-  // ignore: close_sinks
-  final _outStream = StreamController<String>();
+class _MyHomePageState extends State<MyHomePage> {
+  final terminal = Terminal(
+    maxLines: 10000,
+  );
 
-  @override
-  bool get isReady => true;
+  late final Pty pty;
 
   @override
-  Future<int> get exitCode => _exitCodeCompleter.future;
+  void initState() {
+    super.initState();
 
-  @override
-  void init() {
-    _outStream.sink.add('xterm.dart demo');
-    _outStream.sink.add('\r\n');
-    _outStream.sink.add('\$ ');
+    WidgetsBinding.instance.endOfFrame.then(
+      (_) {
+        if (mounted) _startPty();
+      },
+    );
   }
 
-  @override
-  Stream<String> get out => _outStream.stream;
+  void _startPty() {
+    pty = Pty.start(
+      shell,
+      columns: terminal.viewWidth,
+      rows: terminal.viewHeight,
+    );
 
-  @override
-  void resize(int width, int height, int pixelWidth, int pixelHeight) {
-    // NOOP
-  }
+    pty.output
+        .cast<List<int>>()
+        .transform(Utf8Decoder())
+        .listen(terminal.write);
 
-  @override
-  void write(String input) {
-    if (input.length <= 0) {
-      return;
-    }
-    // in a "real" terminal emulation you would connect onInput to the backend
-    // (like a pty or ssh connection) that then handles the changes in the
-    // terminal.
-    // As we don't have a connected backend here we simulate the changes by
-    // directly writing to the terminal.
-    if (input == '\r') {
-      _outStream.sink.add('\r\n');
-      _outStream.sink.add('\$ ');
-    } else if (input.codeUnitAt(0) == 127) {
-      // Backspace handling
-      _outStream.sink.add('\b \b');
-    } else {
-      _outStream.sink.add(input);
-    }
-  }
+    pty.exitCode.then((code) {
+      terminal.write('the process exited with exit code $code');
+    });
 
-  @override
-  void terminate() {
-    //NOOP
-  }
+    terminal.onOutput = (data) {
+      pty.write(const Utf8Encoder().convert(data));
+    };
 
-  @override
-  void ackProcessed() {
-    //NOOP
+    terminal.onResize = (w, h, pw, ph) {
+      pty.resize(h, w);
+    };
   }
-}
-
-class _MyHomePageState extends State<MyHomePage> {
-  final terminal = Terminal(
-    backend: FakeTerminalBackend(),
-    maxLines: 10000,
-  );
-
-  void onInput(String input) {}
 
   @override
   Widget build(BuildContext context) {
     return Scaffold(
+      backgroundColor: Colors.transparent,
       body: SafeArea(
         child: TerminalView(
-          terminal: terminal,
-          style: TerminalStyle(fontFamily: ['Cascadia Mono']),
+          terminal,
+          backgroundOpacity: 0.8,
         ),
       ),
     );
   }
 }
+
+String get shell {
+  if (Platform.isMacOS || Platform.isLinux) {
+    return Platform.environment['SHELL'] ?? 'bash';
+  }
+
+  if (Platform.isWindows) {
+    return 'cmd.exe';
+  }
+
+  return 'sh';
+}

+ 107 - 0
example/lib/mock.dart

@@ -0,0 +1,107 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_acrylic/flutter_acrylic.dart';
+import 'package:xterm/xterm.dart';
+
+void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+
+  if (isDesktop) {}
+  setupAcrylic();
+
+  runApp(MyApp());
+}
+
+bool get isDesktop {
+  if (kIsWeb) return false;
+  return [
+    TargetPlatform.windows,
+    TargetPlatform.linux,
+    TargetPlatform.macOS,
+  ].contains(defaultTargetPlatform);
+}
+
+Future<void> setupAcrylic() async {
+  await Window.initialize();
+  await Window.makeTitlebarTransparent();
+  await Window.setEffect(effect: WindowEffect.aero);
+  await Window.setBlurViewState(MacOSBlurViewState.active);
+}
+
+class MyApp extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'xterm.dart demo',
+      debugShowCheckedModeBanner: false,
+      home: MyHomePage(),
+    );
+  }
+}
+
+class MyHomePage extends StatefulWidget {
+  MyHomePage({Key? key}) : super(key: key);
+
+  @override
+  // ignore: library_private_types_in_public_api
+  _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  final terminal = Terminal(
+    maxLines: 1000,
+  );
+
+  late final MockRepl pty;
+
+  @override
+  void initState() {
+    super.initState();
+
+    pty = MockRepl(terminal.write);
+
+    terminal.onOutput = pty.write;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      backgroundColor: Colors.transparent,
+      body: SafeArea(
+        child: TerminalView(
+          terminal,
+          backgroundOpacity: 0.7,
+        ),
+      ),
+    );
+  }
+}
+
+class MockRepl {
+  MockRepl(this.onOutput) {
+    onOutput('Welcome to xterm.dart!\r\n');
+    onOutput('Type "help" for more information.\r\n');
+    onOutput('\n');
+    onOutput('\$ ');
+  }
+
+  final void Function(String data) onOutput;
+
+  void write(String input) {
+    for (var char in input.codeUnits) {
+      switch (char) {
+        case 13: // carriage return
+          onOutput.call('\r\n');
+          onOutput.call('\$ ');
+          break;
+        case 127: // backspace
+          onOutput.call('\b \b');
+          break;
+        default:
+          onOutput.call(String.fromCharCode(char));
+      }
+    }
+  }
+}

+ 53 - 83
example/lib/ssh.dart

@@ -3,11 +3,11 @@ import 'dart:convert';
 import 'dart:typed_data';
 
 import 'package:dartssh2/dartssh2.dart';
-import 'package:flutter/material.dart';
-import 'package:xterm/flutter.dart';
+import 'package:flutter/cupertino.dart';
 import 'package:xterm/xterm.dart';
 
-const host = 'ssh://localhost:22';
+const host = 'localhost';
+const port = 22;
 const username = '<your username>';
 const password = '<your password>';
 
@@ -18,12 +18,8 @@ void main() {
 class MyApp extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
-    return MaterialApp(
+    return CupertinoApp(
       title: 'xterm.dart demo',
-      theme: ThemeData(
-        primarySwatch: Colors.blue,
-        visualDensity: VisualDensity.adaptivePlatformDensity,
-      ),
       home: MyHomePage(),
     );
   }
@@ -33,101 +29,75 @@ class MyHomePage extends StatefulWidget {
   MyHomePage({Key? key}) : super(key: key);
 
   @override
+  // ignore: library_private_types_in_public_api
   _MyHomePageState createState() => _MyHomePageState();
 }
 
-class SSHTerminalBackend extends TerminalBackend {
-  late SSHClient client;
-
-  String _host;
-  String _username;
-  String _password;
-
-  final _exitCodeCompleter = Completer<int>();
-  final _outStream = StreamController<String>();
-
-  SSHTerminalBackend(this._host, this._username, this._password);
+class _MyHomePageState extends State<MyHomePage> {
+  final terminal = Terminal();
 
-  void onWrite(String data) {
-    _outStream.sink.add(data);
-  }
+  var title = host;
 
   @override
-  bool get isReady => true;
+  void initState() {
+    super.initState();
+    initTerminal();
+  }
 
-  @override
-  Future<int> get exitCode => _exitCodeCompleter.future;
+  Future<void> initTerminal() async {
+    terminal.write('Connecting...\r\n');
 
-  @override
-  void init() {
-    // Use utf8.decoder to handle broken utf8 chunks
-    final _sshOutput = StreamController<List<int>>();
-    _sshOutput.stream.transform(utf8.decoder).listen(onWrite);
-
-    onWrite('connecting $_host...');
-    client = SSHClient(
-      hostport: Uri.parse(_host),
-      username: _username,
-      print: print,
-      termWidth: 80,
-      termHeight: 25,
-      termvar: 'xterm-256color',
-      onPasswordRequest: () => _password,
-      response: (data) {
-        _sshOutput.add(data);
-      },
-      success: () {
-        onWrite('connected.\n');
-      },
-      disconnected: () {
-        onWrite('disconnected.');
-        _outStream.close();
-      },
+    final client = SSHClient(
+      await SSHSocket.connect(host, port),
+      username: username,
+      onPasswordRequest: () => password,
     );
-  }
 
-  @override
-  Stream<String> get out => _outStream.stream;
+    terminal.write('Connected\r\n');
 
-  @override
-  void resize(int width, int height, int pixelWidth, int pixelHeight) {
-    client.setTerminalWindowSize(width, height);
-  }
+    final session = await client.shell(
+      pty: SSHPtyConfig(
+        width: terminal.viewWidth,
+        height: terminal.viewHeight,
+      ),
+    );
 
-  @override
-  void write(String input) {
-    client.sendChannelData(Uint8List.fromList(utf8.encode(input)));
-  }
+    terminal.buffer.clear();
+    terminal.buffer.setCursor(0, 0);
 
-  @override
-  void terminate() {
-    client.disconnect('terminate');
-  }
+    terminal.onTitleChange = (title) {
+      setState(() => this.title = title);
+    };
 
-  @override
-  void ackProcessed() {
-    // NOOP
-  }
-}
+    terminal.onResize = (width, height, pixelWidth, pixelHeight) {
+      session.resizeTerminal(width, height, pixelWidth, pixelHeight);
+    };
 
-class _MyHomePageState extends State<MyHomePage> {
-  late Terminal terminal;
-  late SSHTerminalBackend backend;
+    terminal.onOutput = (data) {
+      session.write(utf8.encode(data) as Uint8List);
+    };
 
-  @override
-  void initState() {
-    super.initState();
-    backend = SSHTerminalBackend(host, username, password);
-    terminal = Terminal(backend: backend, maxLines: 10000);
+    session.stdout
+        .cast<List<int>>()
+        .transform(Utf8Decoder())
+        .listen(terminal.write);
+
+    session.stderr
+        .cast<List<int>>()
+        .transform(Utf8Decoder())
+        .listen(terminal.write);
   }
 
   @override
   Widget build(BuildContext context) {
-    return Scaffold(
-      body: SafeArea(
-        child: TerminalView(
-          terminal: terminal,
-        ),
+    return CupertinoPageScaffold(
+      navigationBar: CupertinoNavigationBar(
+        middle: Text(title),
+        backgroundColor:
+            CupertinoTheme.of(context).barBackgroundColor.withOpacity(0.5),
+      ),
+      child: TerminalView(
+        terminal,
       ),
     );
   }

+ 4 - 0
example/linux/flutter/generated_plugin_registrant.cc

@@ -6,6 +6,10 @@
 
 #include "generated_plugin_registrant.h"
 
+#include <flutter_acrylic/flutter_acrylic_plugin.h>
 
 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);
 }

+ 2 - 0
example/linux/flutter/generated_plugins.cmake

@@ -3,9 +3,11 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
+  flutter_acrylic
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST
+  flutter_pty
 )
 
 set(PLUGIN_BUNDLED_LIBRARIES)

+ 1 - 0
example/macos/.gitignore

@@ -3,4 +3,5 @@
 **/Pods/
 
 # Xcode-related
+**/dgph
 **/xcuserdata/

+ 1 - 0
example/macos/Flutter/Flutter-Debug.xcconfig

@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
 #include "ephemeral/Flutter-Generated.xcconfig"

+ 1 - 0
example/macos/Flutter/Flutter-Release.xcconfig

@@ -1 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
 #include "ephemeral/Flutter-Generated.xcconfig"

+ 2 - 0
example/macos/Flutter/GeneratedPluginRegistrant.swift

@@ -5,6 +5,8 @@
 import FlutterMacOS
 import Foundation
 
+import flutter_acrylic
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  FlutterAcrylicPlugin.register(with: registry.registrar(forPlugin: "FlutterAcrylicPlugin"))
 }

+ 40 - 0
example/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

+ 28 - 0
example/macos/Podfile.lock

@@ -0,0 +1,28 @@
+PODS:
+  - flutter_acrylic (0.1.0):
+    - FlutterMacOS
+  - flutter_pty (0.0.1):
+    - FlutterMacOS
+  - FlutterMacOS (1.0.0)
+
+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`)
+
+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
+
+SPEC CHECKSUMS:
+  flutter_acrylic: c3df24ae52ab6597197837ce59ef2a8542640c17
+  flutter_pty: 41b6f848ade294be726a6b94cdd4a67c3bc52f59
+  FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+
+PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
+
+COCOAPODS: 1.11.3

+ 65 - 17
example/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 */; };
+		A6151BD419F68182C8FF85D2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 413CC3D3B2FBCF7B69907E26 /* 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 = "<group>"; };
 		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
-		33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
 		33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@@ -66,8 +67,12 @@
 		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
 		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
 		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
+		413CC3D3B2FBCF7B69907E26 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		49AA7380F80473893FD60C2E /* 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 = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
+		E0725F2979814304119369B0 /* 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 = "<group>"; };
+		F2CA369AF483BC5EA72B3581 /* 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 = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -75,6 +80,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				A6151BD419F68182C8FF85D2 /* Pods_Runner.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -99,6 +105,7 @@
 				33CEB47122A05771004F2AC0 /* Flutter */,
 				33CC10EE2044A3C60003C045 /* Products */,
 				D73912EC22F37F3D000D13A0 /* Frameworks */,
+				F3BFAC646F7524479B4C81FB /* Pods */,
 			);
 			sourceTree = "<group>";
 		};
@@ -148,10 +155,22 @@
 		D73912EC22F37F3D000D13A0 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				413CC3D3B2FBCF7B69907E26 /* Pods_Runner.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
 		};
+		F3BFAC646F7524479B4C81FB /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				49AA7380F80473893FD60C2E /* Pods-Runner.debug.xcconfig */,
+				F2CA369AF483BC5EA72B3581 /* Pods-Runner.release.xcconfig */,
+				E0725F2979814304119369B0 /* Pods-Runner.profile.xcconfig */,
+			);
+			name = Pods;
+			path = Pods;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -159,11 +178,13 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
 			buildPhases = (
+				3B927F223EF0FB46C39660FE /* [CP] Check Pods Manifest.lock */,
 				33CC10E92044A3C60003C045 /* Sources */,
 				33CC10EA2044A3C60003C045 /* Frameworks */,
 				33CC10EB2044A3C60003C045 /* Resources */,
 				33CC110E2044A8840003C045 /* Bundle Framework */,
 				3399D490228B24CF009A79C7 /* ShellScript */,
+				9092D76C686A497A89A31B0A /* [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 = (
@@ -268,7 +289,46 @@
 			);
 			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";
+		};
+		3B927F223EF0FB46C39660FE /* [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;
+		};
+		9092D76C686A497A89A31B0A /* [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)",

+ 4 - 18
example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1000"
+   LastUpgradeVersion = "1300"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -27,18 +27,6 @@
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       shouldUseLaunchSchemeArgsEnv = "YES">
-      <Testables>
-         <TestableReference
-            skipped = "NO">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "00380F9121DF178D00097171"
-               BuildableName = "RunnerUITests.xctest"
-               BlueprintName = "RunnerUITests"
-               ReferencedContainer = "container:Runner.xcodeproj">
-            </BuildableReference>
-         </TestableReference>
-      </Testables>
       <MacroExpansion>
          <BuildableReference
             BuildableIdentifier = "primary"
@@ -48,8 +36,8 @@
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>
       </MacroExpansion>
-      <AdditionalOptions>
-      </AdditionalOptions>
+      <Testables>
+      </Testables>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -71,11 +59,9 @@
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>
       </BuildableProductRunnable>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
-      buildConfiguration = "Release"
+      buildConfiguration = "Profile"
       shouldUseLaunchSchemeArgsEnv = "YES"
       savedToolIdentifier = ""
       useCustomWorkingDirectory = "NO"

+ 3 - 0
example/macos/Runner.xcworkspace/contents.xcworkspacedata

@@ -4,4 +4,7 @@
    <FileRef
       location = "group:Runner.xcodeproj">
    </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
 </Workspace>

+ 4 - 0
example/macos/Runner/Base.lproj/MainMenu.xib

@@ -323,6 +323,10 @@
                         </items>
                     </menu>
                 </menuItem>
+                <menuItem title="Help" id="EPT-qC-fAb">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
+                </menuItem>
             </items>
             <point key="canvasLocation" x="142" y="-258"/>
         </menu>

+ 1 - 1
example/macos/Runner/Configs/AppInfo.xcconfig

@@ -11,4 +11,4 @@ PRODUCT_NAME = example
 PRODUCT_BUNDLE_IDENTIFIER = com.example.example
 
 // The copyright displayed in application information
-PRODUCT_COPYRIGHT = Copyright © 2020 com.example. All rights reserved.
+PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved.

+ 0 - 16
example/macos/Runner/DebugProfile.entitlements

@@ -2,23 +2,7 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>com.apple.security.app-sandbox</key>
-	<true/>
-	<key>com.apple.security.assets.movies.read-write</key>
-	<true/>
-	<key>com.apple.security.assets.music.read-write</key>
-	<true/>
-	<key>com.apple.security.assets.pictures.read-write</key>
-	<true/>
 	<key>com.apple.security.cs.allow-jit</key>
 	<true/>
-	<key>com.apple.security.files.downloads.read-write</key>
-	<true/>
-	<key>com.apple.security.files.user-selected.read-write</key>
-	<true/>
-	<key>com.apple.security.network.client</key>
-	<true/>
-	<key>com.apple.security.network.server</key>
-	<true/>
 </dict>
 </plist>

+ 10 - 6
example/macos/Runner/MainFlutterWindow.swift

@@ -1,15 +1,19 @@
 import Cocoa
 import FlutterMacOS
+import flutter_acrylic
 
 class MainFlutterWindow: NSWindow {
   override func awakeFromNib() {
-    let flutterViewController = FlutterViewController.init()
-    let windowFrame = self.frame
-    self.contentViewController = flutterViewController
-    self.setFrame(windowFrame, display: true)
+   let windowFrame = self.frame
+   let blurryContainerViewController = BlurryContainerViewController()
+   self.contentViewController = blurryContainerViewController
+   self.setFrame(windowFrame, display: true)
 
-    RegisterGeneratedPlugins(registry: flutterViewController)
+   /* Initialize the flutter_acrylic plugin */
+   MainFlutterWindowManipulator.start(mainFlutterWindow: self)
+
+   RegisterGeneratedPlugins(registry: blurryContainerViewController.flutterViewController)
 
     super.awakeFromNib()
   }
-}
+}

+ 1 - 18
example/macos/Runner/Release.entitlements

@@ -1,22 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
-<dict>
-	<key>com.apple.security.app-sandbox</key>
-	<true/>
-	<key>com.apple.security.assets.movies.read-write</key>
-	<true/>
-	<key>com.apple.security.assets.music.read-write</key>
-	<true/>
-	<key>com.apple.security.assets.pictures.read-write</key>
-	<true/>
-	<key>com.apple.security.files.downloads.read-write</key>
-	<true/>
-	<key>com.apple.security.files.user-selected.read-write</key>
-	<true/>
-	<key>com.apple.security.network.client</key>
-	<true/>
-	<key>com.apple.security.network.server</key>
-	<true/>
-</dict>
+<dict/>
 </plist>

+ 146 - 31
example/pubspec.lock

@@ -1,6 +1,41 @@
 # 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: "46.0.0"
+  after_layout:
+    dependency: "direct main"
+    description:
+      name: after_layout
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.6.0"
+  analyzer_plugin:
+    dependency: transitive
+    description:
+      name: analyzer_plugin
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.10.0"
+  ansicolor:
+    dependency: transitive
+    description:
+      name: ansicolor
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
   args:
     dependency: transitive
     description:
@@ -64,6 +99,20 @@ packages:
       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.2"
+  csslib:
+    dependency: transitive
+    description:
+      name: csslib
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.17.2"
   cupertino_icons:
     dependency: "direct main"
     description:
@@ -71,6 +120,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.1.3"
+  dart_code_metrics:
+    dependency: "direct dev"
+    description:
+      name: dart_code_metrics
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.17.1"
   dart_console:
     dependency: transitive
     description:
@@ -78,15 +134,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.0"
+  dart_style:
+    dependency: transitive
+    description:
+      name: dart_style
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.3"
   dartssh2:
     dependency: "direct main"
     description:
-      path: "."
-      ref: HEAD
-      resolved-ref: "3316b252ace4948f64812b7e5eca11f466d3f62d"
-      url: "https://github.com/TerminalStudio/dartssh2"
-    source: git
-    version: "1.2.0-pre"
+      name: dartssh2
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.5.0"
   equatable:
     dependency: transitive
     description:
@@ -107,31 +168,52 @@ packages:
       name: ffi
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.2"
+    version: "1.2.1"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.4"
   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_pty:
+    dependency: "direct main"
+    description:
+      name: flutter_pty
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.1"
   flutter_test:
     dependency: "direct dev"
     description: flutter
     source: sdk
     version: "0.0.0"
-  http:
+  glob:
     dependency: transitive
     description:
-      name: http
+      name: glob
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.13.4"
-  http_parser:
+    version: "2.1.0"
+  html:
     dependency: transitive
     description:
-      name: http_parser
+      name: html
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "4.0.0"
+    version: "0.15.0"
   js:
     dependency: transitive
     description:
@@ -139,6 +221,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.6.3"
+  lints:
+    dependency: "direct dev"
+    description:
+      name: lints
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   matcher:
     dependency: transitive
     description:
@@ -160,6 +249,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.7.0"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
   path:
     dependency: transitive
     description:
@@ -167,6 +263,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.8.1"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.0.0"
   pinenacl:
     dependency: transitive
     description:
@@ -180,7 +283,7 @@ packages:
       name: platform_info
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.2.0"
+    version: "3.1.0"
   pointycastle:
     dependency: transitive
     description:
@@ -188,15 +291,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.4.0"
-  pty:
-    dependency: "direct main"
+  pub_semver:
+    dependency: transitive
     description:
-      path: "."
-      ref: HEAD
-      resolved-ref: "5c3798fef3f304316a8c8b8b66d611c944213dac"
-      url: "https://github.com/TerminalStudio/pty"
-    source: git
-    version: "0.2.2-pre"
+      name: pub_semver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
   quiver:
     dependency: transitive
     description:
@@ -258,20 +359,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.3.0"
-  validators:
+  vector_math:
     dependency: transitive
     description:
-      name: validators
+      name: vector_math
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.0"
-  vector_math:
+    version: "2.1.2"
+  watcher:
     dependency: transitive
     description:
-      name: vector_math
+      name: watcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.1.2"
+    version: "1.0.1"
   win32:
     dependency: transitive
     description:
@@ -279,13 +380,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.3.3"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.0"
   xterm:
     dependency: "direct main"
     description:
       path: ".."
       relative: true
     source: path
-    version: "2.6.0"
+    version: "3.0.6-alpha"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.1"
 sdks:
-  dart: ">=2.17.0-0 <3.0.0"
-  flutter: ">=2.0.0"
+  dart: ">=2.17.0 <3.0.0"
+  flutter: ">=3.0.0"

+ 14 - 7
example/pubspec.yaml

@@ -3,7 +3,7 @@ description: A new Flutter project.
 
 # The following line prevents the package from being accidentally published to
 # pub.dev using `pub publish`. This is preferred for private packages.
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+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.
 # A version number is three numbers separated by dots, like 1.2.43
@@ -24,29 +24,36 @@ dependencies:
   xterm:
     path: ../
 
-  dartssh2:
-    git:
-      url: https://github.com/TerminalStudio/dartssh2
-  pty:
-    git: https://github.com/TerminalStudio/pty
+  dartssh2: ^2.5.0
+
+  flutter_pty: ^0.1.1
+
+  flutter_acrylic: ^1.0.0+2
+
   flutter:
     sdk: flutter
 
+  # google_fonts: ^2.3.1
 
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^0.1.3
 
+  after_layout: ^1.1.0
+
 dev_dependencies:
   flutter_test:
     sdk: flutter
 
+  lints: ^2.0.0
+
+  dart_code_metrics: ^4.16.0
+
 # 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.
 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.

+ 0 - 30
example/test/widget_test.dart

@@ -1,30 +0,0 @@
-// // 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
-// // 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:example/main.dart';
-
-// void main() {
-//   testWidgets('Counter increments smoke test', (WidgetTester tester) async {
-//     // Build our app and trigger a frame.
-//     await tester.pumpWidget(MyApp());
-
-//     // Verify that our counter starts at 0.
-//     expect(find.text('0'), findsOneWidget);
-//     expect(find.text('1'), findsNothing);
-
-//     // Tap the '+' icon and trigger a frame.
-//     await tester.tap(find.byIcon(Icons.add));
-//     await tester.pump();
-
-//     // Verify that our counter has incremented.
-//     expect(find.text('0'), findsNothing);
-//     expect(find.text('1'), findsOneWidget);
-//   });
-// }

+ 3 - 0
example/windows/flutter/generated_plugin_registrant.cc

@@ -6,6 +6,9 @@
 
 #include "generated_plugin_registrant.h"
 
+#include <flutter_acrylic/flutter_acrylic_plugin.h>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
+  FlutterAcrylicPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
 }

+ 2 - 0
example/windows/flutter/generated_plugins.cmake

@@ -3,9 +3,11 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
+  flutter_acrylic
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST
+  flutter_pty
 )
 
 set(PLUGIN_BUNDLED_LIBRARIES)

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
fixture/htop_80x25_3s.txt


+ 0 - 555
lib/buffer/buffer.dart

@@ -1,555 +0,0 @@
-import 'dart:math' show max, min;
-
-import 'package:xterm/buffer/line/line.dart';
-import 'package:xterm/buffer/reflow_strategy_narrower.dart';
-import 'package:xterm/buffer/reflow_strategy_wider.dart';
-import 'package:xterm/terminal/charset.dart';
-import 'package:xterm/terminal/terminal.dart';
-import 'package:xterm/util/circular_list.dart';
-import 'package:xterm/util/scroll_range.dart';
-import 'package:xterm/util/unicode_v11.dart';
-
-class Buffer {
-  Buffer({
-    required Terminal terminal,
-    required this.isAltBuffer,
-  }) : _terminal = terminal {
-    resetVerticalMargins();
-
-    lines = CircularList(
-      _terminal.maxLines,
-    );
-    for (int i = 0; i < _terminal.viewHeight; i++) {
-      lines.push(_newEmptyLine());
-    }
-  }
-
-  final Terminal _terminal;
-  final bool isAltBuffer;
-  final charset = Charset();
-
-  int get viewHeight => _terminal.viewHeight;
-  int get viewWidth => _terminal.viewWidth;
-
-  /// lines of the buffer. the length of [lines] should always be equal or
-  /// greater than [Terminal.viewHeight].
-  late final CircularList<BufferLine> lines;
-
-  int? _savedCursorX;
-  int? _savedCursorY;
-  int? _savedCellFgColor;
-  int? _savedCellBgColor;
-  int? _savedCellFlags;
-
-  // Indicates how far the bottom of the viewport is from the bottom of the
-  // entire buffer. 0 if the viewport overlaps the terminal screen.
-  int get scrollOffsetFromBottom => _scrollOffsetFromBottom;
-  int _scrollOffsetFromBottom = 0;
-
-  // Indicates how far the top of the viewport is from the top of the entire
-  // buffer. 0 if the viewport is scrolled to the top.
-  int get scrollOffsetFromTop {
-    return _terminal.invisibleHeight - scrollOffsetFromBottom;
-  }
-
-  /// Indicated whether the terminal should automatically scroll to bottom when
-  /// new lines are added. When user is scrolling, [isUserScrolling] is true and
-  /// the automatical scroll-to-bottom behavior is disabled.
-  bool get isUserScrolling {
-    return _scrollOffsetFromBottom != 0;
-  }
-
-  /// Horizontal position of the cursor relative to the top-left cornor of the
-  /// screen, starting from 0.
-  int get cursorX => _cursorX.clamp(0, _terminal.viewWidth - 1);
-  int _cursorX = 0;
-
-  /// Vertical position of the cursor relative to the top-left cornor of the
-  /// screen, starting from 0.
-  int get cursorY => _cursorY;
-  int _cursorY = 0;
-
-  int get marginTop => _marginTop;
-  late int _marginTop;
-
-  int get marginBottom => _marginBottom;
-  late int _marginBottom;
-
-  /// Writes data to the _terminal. Terminal sequences or special characters are
-  /// not interpreted and directly added to the buffer.
-  ///
-  /// See also: [Terminal.write]
-  void write(String text) {
-    for (var char in text.runes) {
-      writeChar(char);
-    }
-  }
-
-  /// Writes a single character to the _terminal. Special chatacters are not
-  /// interpreted and directly added to the buffer.
-  ///
-  /// See also: [Terminal.writeChar]
-  void writeChar(int codePoint) {
-    codePoint = charset.translate(codePoint);
-
-    final cellWidth = unicodeV11.wcwidth(codePoint);
-    if (_cursorX >= _terminal.viewWidth) {
-      newLine();
-      setCursorX(0);
-      if (_terminal.autoWrapMode) {
-        currentLine.isWrapped = true;
-      }
-    }
-
-    final line = currentLine;
-    line.ensure(_cursorX + 1);
-
-    line.cellInitialize(
-      _cursorX,
-      content: codePoint,
-      width: cellWidth,
-      cursor: _terminal.cursor,
-    );
-
-    if (_cursorX < _terminal.viewWidth) {
-      _cursorX++;
-    }
-
-    if (cellWidth == 2) {
-      writeChar(0);
-    }
-  }
-
-  /// get line in the viewport. [index] starts from 0, must be smaller than
-  /// [Terminal.viewHeight].
-  BufferLine getViewLine(int index) {
-    index = index.clamp(0, _terminal.viewHeight - 1);
-    return lines[convertViewLineToRawLine(index)];
-  }
-
-  BufferLine get currentLine {
-    return getViewLine(_cursorY);
-  }
-
-  int get height {
-    return lines.length;
-  }
-
-  int convertViewLineToRawLine(int viewLine) {
-    if (_terminal.viewHeight > height) {
-      return viewLine;
-    }
-
-    return viewLine + (height - _terminal.viewHeight);
-  }
-
-  int convertRawLineToViewLine(int rawLine) {
-    if (_terminal.viewHeight > height) {
-      return rawLine;
-    }
-
-    return rawLine - (height - _terminal.viewHeight);
-  }
-
-  void newLine() {
-    if (_terminal.newLineMode) {
-      setCursorX(0);
-    }
-
-    index();
-  }
-
-  void carriageReturn() {
-    setCursorX(0);
-  }
-
-  void backspace() {
-    if (_cursorX == 0 && currentLine.isWrapped) {
-      currentLine.isWrapped = false;
-      movePosition(_terminal.viewWidth - 1, -1);
-    } else if (_cursorX == _terminal.viewWidth) {
-      movePosition(-2, 0);
-    } else {
-      movePosition(-1, 0);
-    }
-  }
-
-  List<BufferLine> getVisibleLines() {
-    if (height < _terminal.viewHeight) {
-      return lines.toList();
-    }
-
-    final result = <BufferLine>[];
-
-    for (var i = height - _terminal.viewHeight; i < height; i++) {
-      final y = i - scrollOffsetFromBottom;
-      if (y >= 0 && y < height) {
-        result.add(lines[y]);
-      }
-    }
-
-    return result;
-  }
-
-  void eraseDisplayFromCursor() {
-    eraseLineFromCursor();
-
-    for (var i = _cursorY + 1; i < _terminal.viewHeight; i++) {
-      final line = getViewLine(i);
-      line.isWrapped = false;
-      line.erase(_terminal.cursor, 0, _terminal.viewWidth);
-    }
-  }
-
-  void eraseDisplayToCursor() {
-    eraseLineToCursor();
-
-    for (var i = 0; i < _cursorY; i++) {
-      final line = getViewLine(i);
-      line.isWrapped = false;
-      line.erase(_terminal.cursor, 0, _terminal.viewWidth);
-    }
-  }
-
-  void eraseDisplay() {
-    for (var i = 0; i < _terminal.viewHeight; i++) {
-      final line = getViewLine(i);
-      line.isWrapped = false;
-      line.erase(_terminal.cursor, 0, _terminal.viewWidth);
-    }
-  }
-
-  void eraseLineFromCursor() {
-    currentLine.isWrapped = false;
-    currentLine.erase(_terminal.cursor, _cursorX, _terminal.viewWidth);
-  }
-
-  void eraseLineToCursor() {
-    currentLine.isWrapped = false;
-    currentLine.erase(_terminal.cursor, 0, _cursorX);
-  }
-
-  void eraseLine() {
-    currentLine.isWrapped = false;
-    currentLine.erase(_terminal.cursor, 0, _terminal.viewWidth);
-  }
-
-  void eraseCharacters(int count) {
-    final start = _cursorX;
-    currentLine.erase(_terminal.cursor, start, start + count);
-  }
-
-  ScrollRange getAreaScrollRange() {
-    var top = convertViewLineToRawLine(_marginTop);
-    var bottom = convertViewLineToRawLine(_marginBottom) + 1;
-    if (bottom > lines.length) {
-      bottom = lines.length;
-    }
-    return ScrollRange(top, bottom);
-  }
-
-  void areaScrollDown(int lines) {
-    final scrollRange = getAreaScrollRange();
-
-    for (var i = scrollRange.bottom; i > scrollRange.top;) {
-      i--;
-      if (i >= scrollRange.top + lines) {
-        this.lines[i] = this.lines[i - lines];
-      } else {
-        this.lines[i] = _newEmptyLine();
-      }
-    }
-  }
-
-  void areaScrollUp(int lines) {
-    final scrollRange = getAreaScrollRange();
-
-    for (var i = scrollRange.top; i < scrollRange.bottom; i++) {
-      if (i + lines < scrollRange.bottom) {
-        this.lines[i] = this.lines[i + lines];
-      } else {
-        this.lines[i] = _newEmptyLine();
-      }
-    }
-  }
-
-  /// https://vt100.net/docs/vt100-ug/chapter3.html#IND IND – Index
-  ///
-  /// ESC D
-  ///
-  /// [index] causes the active position to move downward one line without
-  /// changing the column position. If the active position is at the bottom
-  /// margin, a scroll up is performed.
-  void index() {
-    if (isInScrollableRegion) {
-      if (_cursorY < _marginBottom) {
-        moveCursorY(1);
-      } else {
-        areaScrollUp(1);
-      }
-      return;
-    }
-
-    // the cursor is not in the scrollable region
-    if (_cursorY >= _terminal.viewHeight - 1) {
-      // we are at the bottom so a new line is created.
-      lines.push(_newEmptyLine());
-
-      // keep viewport from moving if user is scrolling.
-      if (isUserScrolling) {
-        _scrollOffsetFromBottom++;
-      }
-    } else {
-      // there're still lines so we simply move cursor down.
-      moveCursorY(1);
-    }
-  }
-
-  /// https://vt100.net/docs/vt100-ug/chapter3.html#RI
-  void reverseIndex() {
-    if (_cursorY == _marginTop) {
-      areaScrollDown(1);
-    } else if (_cursorY > 0) {
-      moveCursorY(-1);
-    }
-  }
-
-  void cursorGoForward() {
-    setCursorX(_cursorX + 1);
-  }
-
-  void setCursorX(int cursorX) {
-    _cursorX = cursorX.clamp(0, _terminal.viewWidth - 1);
-  }
-
-  void setCursorY(int cursorY) {
-    _cursorY = cursorY.clamp(0, _terminal.viewHeight - 1);
-  }
-
-  void moveCursorX(int offset) {
-    setCursorX(_cursorX + offset);
-  }
-
-  void moveCursorY(int offset) {
-    setCursorY(_cursorY + offset);
-  }
-
-  void setPosition(int cursorX, int cursorY) {
-    var maxLine = _terminal.viewHeight - 1;
-
-    if (_terminal.originMode) {
-      cursorY += _marginTop;
-      maxLine = _marginBottom;
-    }
-
-    _cursorX = cursorX.clamp(0, _terminal.viewWidth - 1);
-    _cursorY = cursorY.clamp(0, maxLine);
-  }
-
-  void movePosition(int offsetX, int offsetY) {
-    final cursorX = _cursorX + offsetX;
-    final cursorY = _cursorY + offsetY;
-    setPosition(cursorX, cursorY);
-  }
-
-  void setScrollOffsetFromBottom(int offsetFromBottom) {
-    if (height < _terminal.viewHeight) return;
-    final maxOffsetFromBottom = height - _terminal.viewHeight;
-    _scrollOffsetFromBottom = offsetFromBottom.clamp(0, maxOffsetFromBottom);
-  }
-
-  void setScrollOffsetFromTop(int offsetFromTop) {
-    final bottomOffset = _terminal.invisibleHeight - offsetFromTop;
-    setScrollOffsetFromBottom(bottomOffset);
-  }
-
-  void screenScrollUp(int lines) {
-    setScrollOffsetFromBottom(scrollOffsetFromBottom + lines);
-  }
-
-  void screenScrollDown(int lines) {
-    setScrollOffsetFromBottom(scrollOffsetFromBottom - lines);
-  }
-
-  void saveCursor() {
-    _savedCellFlags = _terminal.cursor.flags;
-    _savedCellFgColor = _terminal.cursor.fg;
-    _savedCellBgColor = _terminal.cursor.bg;
-    _savedCursorX = _cursorX;
-    _savedCursorY = _cursorY;
-    charset.save();
-  }
-
-  void restoreCursor() {
-    if (_savedCellFlags != null) {
-      _terminal.cursor.flags = _savedCellFlags!;
-    }
-
-    if (_savedCellFgColor != null) {
-      _terminal.cursor.fg = _savedCellFgColor!;
-    }
-
-    if (_savedCellBgColor != null) {
-      _terminal.cursor.bg = _savedCellBgColor!;
-    }
-
-    if (_savedCursorX != null) {
-      _cursorX = _savedCursorX!;
-    }
-
-    if (_savedCursorY != null) {
-      _cursorY = _savedCursorY!;
-    }
-
-    charset.restore();
-  }
-
-  void setVerticalMargins(int top, int bottom) {
-    _marginTop = top.clamp(0, _terminal.viewHeight - 1);
-    _marginBottom = bottom.clamp(0, _terminal.viewHeight - 1);
-
-    _marginTop = min(_marginTop, _marginBottom);
-    _marginBottom = max(_marginTop, _marginBottom);
-  }
-
-  bool get hasScrollableRegion {
-    return _marginTop > 0 || _marginBottom < (_terminal.viewHeight - 1);
-  }
-
-  bool get isInScrollableRegion {
-    return hasScrollableRegion &&
-        _cursorY >= _marginTop &&
-        _cursorY <= _marginBottom;
-  }
-
-  void resetVerticalMargins() {
-    setVerticalMargins(0, _terminal.viewHeight - 1);
-  }
-
-  void deleteChars(int count) {
-    final start = _cursorX.clamp(0, _terminal.viewWidth);
-    final end = min(_cursorX + count, _terminal.viewWidth);
-    currentLine.removeRange(start, end);
-  }
-
-  void clearScrollback() {
-    if (lines.length <= _terminal.viewHeight) {
-      return;
-    }
-
-    lines.trimStart(lines.length - _terminal.viewHeight);
-  }
-
-  void clear() {
-    lines.clear();
-    for (int i = 0; i < _terminal.viewHeight; i++) {
-      lines.push(_newEmptyLine());
-    }
-  }
-
-  void insertBlankCharacters(int count) {
-    for (var i = 0; i < count; i++) {
-      currentLine.insert(_cursorX + i);
-      currentLine.cellSetFlags(_cursorX + i, _terminal.cursor.flags);
-    }
-  }
-
-  void insertLines(int count) {
-    if (hasScrollableRegion && !isInScrollableRegion) {
-      return;
-    }
-
-    setCursorX(0);
-
-    for (var i = 0; i < count; i++) {
-      insertLine();
-    }
-  }
-
-  void insertLine() {
-    if (!isInScrollableRegion) {
-      final index = convertViewLineToRawLine(_cursorX);
-      final newLine = _newEmptyLine();
-      lines.insert(index, newLine);
-    } else {
-      final newLine = _newEmptyLine();
-      lines.insert(_cursorY, newLine);
-    }
-  }
-
-  void deleteLines(int count) {
-    if (hasScrollableRegion && !isInScrollableRegion) {
-      return;
-    }
-
-    setCursorX(0);
-
-    for (var i = 0; i < count; i++) {
-      deleteLine();
-    }
-  }
-
-  void deleteLine() {
-    final index = convertViewLineToRawLine(_cursorX);
-
-    if (index >= height) {
-      return;
-    }
-
-    lines.remove(index);
-  }
-
-  void resize(int oldWidth, int oldHeight, int newWidth, int newHeight) {
-    if (newWidth > oldWidth) {
-      lines.forEach((item) => item.ensure(newWidth));
-    }
-
-    if (newHeight > oldHeight) {
-      while (lines.length < newHeight) {
-        lines.push(_newEmptyLine());
-      }
-      // Grow larger
-      for (var i = 0; i < newHeight - oldHeight; i++) {
-        if (_cursorY < oldHeight - 1) {
-          lines.push(_newEmptyLine());
-        } else {
-          _cursorY++;
-        }
-      }
-    } else {
-      // Shrink smaller
-      for (var i = 0; i < oldHeight - newHeight; i++) {
-        if (_cursorY < oldHeight - 1) {
-          lines.pop();
-        } else {
-          _cursorY++;
-        }
-      }
-    }
-
-    // Ensure cursor is within the screen.
-    _cursorX = _cursorX.clamp(0, newWidth - 1);
-    _cursorY = _cursorY.clamp(0, newHeight - 1);
-
-    if (!isAltBuffer) {
-      final reflowStrategy = newWidth > oldWidth
-          ? ReflowStrategyWider(this)
-          : ReflowStrategyNarrower(this);
-      reflowStrategy.reflow(newWidth, newHeight, oldWidth, oldHeight);
-    }
-  }
-
-  BufferLine _newEmptyLine() {
-    final line = BufferLine(length: _terminal.viewWidth);
-    return line;
-  }
-
-  adjustSavedCursor(int dx, int dy) {
-    if (_savedCursorX != null) {
-      _savedCursorX = _savedCursorX! + dx;
-    }
-    if (_savedCursorY != null) {
-      _savedCursorY = _savedCursorY! + dy;
-    }
-  }
-}

+ 0 - 266
lib/buffer/line/line.dart

@@ -1,266 +0,0 @@
-import 'dart:math';
-
-import 'package:meta/meta.dart';
-import 'package:xterm/buffer/line/line_bytedata_data.dart';
-import 'package:xterm/buffer/line/line_list_data.dart';
-import 'package:xterm/terminal/cursor.dart';
-import 'package:xterm/util/constants.dart';
-
-@sealed
-class BufferLine {
-  BufferLine({int length = 64, bool isWrapped = false}) {
-    _data = BufferLineData(length: length, isWrapped: isWrapped);
-  }
-
-  BufferLine.withDataFrom(BufferLine other) {
-    _data = other.data;
-  }
-
-  late BufferLineData _data;
-  final _nonDirtyTags = Set<String>();
-
-  void markTagAsNonDirty(String tag) {
-    _nonDirtyTags.add(tag);
-  }
-
-  bool isTagDirty(String tag) {
-    return !_nonDirtyTags.contains(tag);
-  }
-
-  BufferLineData get data => _data;
-
-  bool get isWrapped => _data.isWrapped;
-
-  set isWrapped(bool value) => _data.isWrapped = value;
-
-  void ensure(int length) => _data.ensure(length);
-
-  void insert(int index) {
-    _invalidateCaches();
-    _data.insert(index);
-  }
-
-  void insertN(int index, int count) {
-    _invalidateCaches();
-    _data.insertN(index, count);
-  }
-
-  void removeN(int index, int count) {
-    _invalidateCaches();
-    _data.removeN(index, count);
-  }
-
-  void clear() {
-    _invalidateCaches();
-    _data.clear();
-  }
-
-  void erase(Cursor cursor, int start, int end, [bool resetIsWrapped = false]) {
-    _invalidateCaches();
-    _data.erase(cursor, start, end);
-  }
-
-  void cellClear(int index) {
-    _invalidateCaches();
-    _data.cellClear(index);
-  }
-
-  void cellInitialize(
-    int index, {
-    required int content,
-    required int width,
-    required Cursor cursor,
-  }) {
-    _invalidateCaches();
-    _data.cellInitialize(
-      index,
-      content: content,
-      width: width,
-      cursor: cursor,
-    );
-  }
-
-  bool cellHasContent(int index) => _data.cellHasContent(index);
-
-  int cellGetContent(int index) => _data.cellGetContent(index);
-
-  void cellSetContent(int index, int content) {
-    _invalidateCaches();
-    _data.cellSetContent(index, content);
-  }
-
-  int cellGetFgColor(int index) => _data.cellGetFgColor(index);
-
-  void cellSetFgColor(int index, int color) =>
-      _data.cellSetFgColor(index, color);
-
-  int cellGetBgColor(int index) => _data.cellGetBgColor(index);
-
-  void cellSetBgColor(int index, int color) =>
-      _data.cellSetBgColor(index, color);
-
-  int cellGetFlags(int index) => _data.cellGetFlags(index);
-
-  void cellSetFlags(int index, int flags) => _data.cellSetFlags(index, flags);
-
-  int cellGetWidth(int index) => _data.cellGetWidth(index);
-
-  void cellSetWidth(int index, int width) {
-    _invalidateCaches();
-    _data.cellSetWidth(index, width);
-  }
-
-  void cellClearFlags(int index) => _data.cellClearFlags(index);
-
-  bool cellHasFlag(int index, int flag) => _data.cellHasFlag(index, flag);
-
-  void cellSetFlag(int index, int flag) => _data.cellSetFlag(index, flag);
-
-  void cellErase(int index, Cursor cursor) {
-    _invalidateCaches();
-    _data.cellErase(index, cursor);
-  }
-
-  int getTrimmedLength([int? cols]) => _data.getTrimmedLength(cols);
-
-  void copyCellsFrom(
-      covariant BufferLine src, int srcCol, int dstCol, int len) {
-    _invalidateCaches();
-    _data.copyCellsFrom(src.data, srcCol, dstCol, len);
-  }
-
-  void removeRange(int start, int end) {
-    _invalidateCaches();
-    _data.removeRange(start, end);
-  }
-
-  void clearRange(int start, int end) {
-    _invalidateCaches();
-    _data.clearRange(start, end);
-  }
-
-  String toDebugString(int cols) => _data.toDebugString(cols);
-
-  void _invalidateCaches() {
-    _searchStringCache = null;
-    _nonDirtyTags.clear();
-  }
-
-  String? _searchStringCache;
-  bool get hasCachedSearchString => _searchStringCache != null;
-
-  String toSearchString(int cols) {
-    if (_searchStringCache != null) {
-      return _searchStringCache!;
-    }
-    final searchString = StringBuffer();
-    final length = getTrimmedLength();
-    for (int i = 0; i < max(cols, length); i++) {
-      var code = cellGetContent(i);
-      if (code != 0) {
-        final cellString = String.fromCharCode(code);
-        searchString.write(cellString);
-        final widthDiff = cellGetWidth(i) - cellString.length;
-        if (widthDiff > 0) {
-          searchString.write(''.padRight(widthDiff));
-        }
-      }
-    }
-    _searchStringCache = searchString.toString();
-    return _searchStringCache!;
-  }
-}
-
-abstract class BufferLineData {
-  factory BufferLineData({int length = 64, bool isWrapped = false}) {
-    if (kIsWeb) {
-      return ListBufferLineData(length, isWrapped);
-    }
-
-    return ByteDataBufferLineData(length, isWrapped);
-  }
-
-  bool get isWrapped;
-
-  set isWrapped(bool value);
-
-  void ensure(int length);
-
-  void insert(int index);
-
-  void insertN(int index, int count);
-
-  void removeN(int index, int count);
-
-  void clear();
-
-  void erase(Cursor cursor, int start, int end, [bool resetIsWrapped = false]);
-
-  void cellClear(int index);
-
-  void cellInitialize(
-    int index, {
-    required int content,
-    required int width,
-    required Cursor cursor,
-  });
-
-  bool cellHasContent(int index);
-
-  int cellGetContent(int index);
-
-  void cellSetContent(int index, int content);
-
-  int cellGetFgColor(int index);
-
-  void cellSetFgColor(int index, int color);
-
-  int cellGetBgColor(int index);
-
-  void cellSetBgColor(int index, int color);
-
-  int cellGetFlags(int index);
-
-  void cellSetFlags(int index, int flags);
-
-  int cellGetWidth(int index);
-
-  void cellSetWidth(int index, int width);
-
-  void cellClearFlags(int index);
-
-  bool cellHasFlag(int index, int flag);
-
-  void cellSetFlag(int index, int flag);
-
-  void cellErase(int index, Cursor cursor);
-
-  int getTrimmedLength([int? cols]);
-
-  void copyCellsFrom(
-      covariant BufferLineData src, int srcCol, int dstCol, int len);
-
-  // int cellGetHash(int index);
-
-  void removeRange(int start, int end);
-
-  void clearRange(int start, int end);
-
-  @nonVirtual
-  String toDebugString(int cols) {
-    final result = StringBuffer();
-    final length = getTrimmedLength();
-    for (int i = 0; i < max(cols, length); i++) {
-      var code = cellGetContent(i);
-      if (code == 0) {
-        if (cellGetWidth(i) == 0) {
-          code = '_'.runes.first;
-        } else {
-          code = cellGetWidth(i).toString().runes.first;
-        }
-      }
-      result.writeCharCode(code);
-    }
-    return result.toString();
-  }
-}

+ 0 - 293
lib/buffer/line/line_bytedata_data.dart

@@ -1,293 +0,0 @@
-import 'dart:math';
-import 'dart:typed_data';
-
-import 'package:xterm/buffer/line/line.dart';
-import 'package:xterm/terminal/cursor.dart';
-
-/// Line layout:
-///   |  cell  |  cell  |  cell  |  cell  | ...
-///   (16 bytes per cell)
-///
-/// Cell layout:
-///   | code point |  fg color  |  bg color  | attributes |
-///       4bytes       4bytes       4bytes       4bytes
-///
-/// Attributes layout:
-///   |  width  |  flags  | reserved | reserved |
-///      1byte     1byte     1byte      1byte
-
-const _cellSize = 16;
-const _cellSize64Bit = _cellSize >> 3;
-
-const _cellContent = 0;
-const _cellFgColor = 4;
-const _cellBgColor = 8;
-
-// const _cellAttributes = 12;
-const _cellWidth = 12;
-const _cellFlags = 13;
-
-int _nextLength(int lengthRequirement) {
-  var nextLength = 2;
-  while (nextLength < lengthRequirement) {
-    nextLength *= 2;
-  }
-  return nextLength;
-}
-
-/// [ByteData] based [BufferLineData], used in non-web platforms to minimize memory
-/// footprint,
-class ByteDataBufferLineData with BufferLineData {
-  ByteDataBufferLineData(int length, this.isWrapped) {
-    _maxCols = _nextLength(length);
-    _cells = ByteData(_maxCols * _cellSize);
-  }
-
-  late ByteData _cells;
-
-  bool isWrapped;
-
-  int _maxCols = 64;
-
-  void ensure(int length) {
-    if (length <= _maxCols) {
-      return;
-    }
-
-    final nextLength = _nextLength(length);
-    final newCells = ByteData(nextLength * _cellSize);
-    newCells.buffer.asInt64List().setAll(0, _cells.buffer.asInt64List());
-    _cells = newCells;
-    _maxCols = nextLength;
-  }
-
-  void insert(int index) {
-    insertN(index, 1);
-  }
-
-  void removeN(int index, int count) {
-    final moveStart = index * _cellSize64Bit;
-    final moveOffset = count * _cellSize64Bit;
-    final moveEnd = (_maxCols - count) * _cellSize64Bit;
-    final bufferEnd = _maxCols * _cellSize64Bit;
-
-    // move data backward
-    final cells = _cells.buffer.asInt64List();
-    for (var i = moveStart; i < moveEnd; i++) {
-      cells[i] = cells[i + moveOffset];
-    }
-
-    // set empty cells to 0
-    for (var i = moveEnd; i < bufferEnd; i++) {
-      cells[i] = 0x00;
-    }
-  }
-
-  void insertN(int index, int count) {
-    //                       start
-    // +--------------------------|-----------------------------------+
-    // |                          |                                   |
-    // +--------------------------\--\--------------------------------+ end
-    //                             \  \
-    //                              \  \
-    //                               v  v
-    // +--------------------------|--|--------------------------------+
-    // |                          |  |                                |
-    // +--------------------------|--|--------------------------------+ end
-    //                       start   start+offset
-
-    final moveStart = index * _cellSize64Bit;
-    final moveOffset = count * _cellSize64Bit;
-    final bufferEnd = _maxCols * _cellSize64Bit;
-
-    // move data forward
-    final cells = _cells.buffer.asInt64List();
-    for (var i = bufferEnd - moveOffset - 1; i >= moveStart; i--) {
-      cells[i + moveOffset] = cells[i];
-    }
-
-    // set inserted cells to 0
-    for (var i = moveStart; i < moveStart + moveOffset; i++) {
-      cells[i] = 0x00;
-    }
-  }
-
-  void clear() {
-    clearRange(0, _cells.lengthInBytes ~/ _cellSize);
-  }
-
-  void erase(Cursor cursor, int start, int end, [bool resetIsWrapped = false]) {
-    ensure(end);
-    for (var i = start; i < end; i++) {
-      cellErase(i, cursor);
-    }
-    if (resetIsWrapped) {
-      isWrapped = false;
-    }
-  }
-
-  void cellClear(int index) {
-    _cells.setUint64(index * _cellSize, 0x00);
-    _cells.setUint64(index * _cellSize + 8, 0x00);
-  }
-
-  void cellInitialize(
-    int index, {
-    required int content,
-    required int width,
-    required Cursor cursor,
-  }) {
-    final cell = index * _cellSize;
-    _cells.setUint32(cell + _cellContent, content);
-    _cells.setUint32(cell + _cellFgColor, cursor.fg);
-    _cells.setUint32(cell + _cellBgColor, cursor.bg);
-    _cells.setUint8(cell + _cellWidth, width);
-    _cells.setUint8(cell + _cellFlags, cursor.flags);
-  }
-
-  bool cellHasContent(int index) {
-    return cellGetContent(index) != 0;
-  }
-
-  int cellGetContent(int index) {
-    if (index > _maxCols) {
-      return 0;
-    }
-    return _cells.getUint32(index * _cellSize + _cellContent);
-  }
-
-  void cellSetContent(int index, int content) {
-    _cells.setInt32(index * _cellSize + _cellContent, content);
-  }
-
-  int cellGetFgColor(int index) {
-    if (index >= _maxCols) {
-      return 0;
-    }
-    return _cells.getUint32(index * _cellSize + _cellFgColor);
-  }
-
-  void cellSetFgColor(int index, int color) {
-    _cells.setUint32(index * _cellSize + _cellFgColor, color);
-  }
-
-  int cellGetBgColor(int index) {
-    if (index >= _maxCols) {
-      return 0;
-    }
-    return _cells.getUint32(index * _cellSize + _cellBgColor);
-  }
-
-  void cellSetBgColor(int index, int color) {
-    _cells.setUint32(index * _cellSize + _cellBgColor, color);
-  }
-
-  int cellGetFlags(int index) {
-    if (index >= _maxCols) {
-      return 0;
-    }
-    return _cells.getUint8(index * _cellSize + _cellFlags);
-  }
-
-  void cellSetFlags(int index, int flags) {
-    _cells.setUint8(index * _cellSize + _cellFlags, flags);
-  }
-
-  int cellGetWidth(int index) {
-    if (index >= _maxCols) {
-      return 1;
-    }
-    return _cells.getUint8(index * _cellSize + _cellWidth);
-  }
-
-  void cellSetWidth(int index, int width) {
-    _cells.setUint8(index * _cellSize + _cellWidth, width);
-  }
-
-  void cellClearFlags(int index) {
-    cellSetFlags(index, 0);
-  }
-
-  bool cellHasFlag(int index, int flag) {
-    if (index >= _maxCols) {
-      return false;
-    }
-    return cellGetFlags(index) & flag != 0;
-  }
-
-  void cellSetFlag(int index, int flag) {
-    cellSetFlags(index, cellGetFlags(index) | flag);
-  }
-
-  void cellErase(int index, Cursor cursor) {
-    cellSetContent(index, 0x00);
-    cellSetFgColor(index, cursor.fg);
-    cellSetBgColor(index, cursor.bg);
-    cellSetFlags(index, cursor.flags);
-    cellSetWidth(index, 0);
-  }
-
-  int getTrimmedLength([int? cols]) {
-    if (cols == null) {
-      cols = _maxCols;
-    }
-    for (var i = cols - 1; i >= 0; i--) {
-      if (cellGetContent(i) != 0) {
-        // we are at the last cell in this line that has content.
-        // the length of this line is the index of this cell + 1
-        // the only exception is that if that last cell is wider
-        // than 1 then we have to add the diff
-        final lastCellWidth = cellGetWidth(i);
-        return i + lastCellWidth;
-      }
-    }
-    return 0;
-  }
-
-  void copyCellsFrom(
-      ByteDataBufferLineData src, int srcCol, int dstCol, int len) {
-    ensure(dstCol + len);
-
-    final intsToCopy = len * _cellSize64Bit;
-    final srcStart = srcCol * _cellSize64Bit;
-    final dstStart = dstCol * _cellSize64Bit;
-
-    final cells = _cells.buffer.asInt64List();
-    final srcCells = src._cells.buffer.asInt64List();
-    for (var i = 0; i < intsToCopy; i++) {
-      cells[dstStart + i] = srcCells[srcStart + i];
-    }
-  }
-
-  // int cellGetHash(int index) {
-  //   final cell = index * _cellSize;
-  //   final a = _cells.getInt64(cell);
-  //   final b = _cells.getInt64(cell + 8);
-  //   return a ^ b;
-  // }
-
-  void removeRange(int start, int end) {
-    end = min(end, _maxCols);
-    this.removeN(start, end - start);
-  }
-
-  void clearRange(int start, int end) {
-    end = min(end, _maxCols);
-    for (var index = start; index < end; index++) {
-      cellClear(index);
-    }
-  }
-
-  @override
-  String toString() {
-    final result = StringBuffer();
-    for (int i = 0; i < _maxCols; i++) {
-      final code = cellGetContent(i);
-      if (code == 0) {
-        continue;
-      }
-      result.writeCharCode(code);
-    }
-    return result.toString();
-  }
-}

+ 0 - 267
lib/buffer/line/line_list_data.dart

@@ -1,267 +0,0 @@
-import 'dart:math';
-
-import 'package:xterm/buffer/line/line.dart';
-import 'package:xterm/terminal/cursor.dart';
-
-/// Line layout:
-///   |  cell  |  cell  |  cell  |  cell  | ...
-///   (4 ints per cell)
-///
-/// Cell layout:
-///   | code point |  fg color  |  bg color  | attributes |
-///       1 int        1 int        1 int        1 int
-///
-/// Attributes layout:
-///   |  width  |  flags  | reserved | reserved |
-///      1byte     1byte     1byte      1byte
-
-const _cellSize = 4;
-
-const _cellContent = 0;
-const _cellFgColor = 1;
-const _cellBgColor = 2;
-const _cellAttributes = 3;
-
-const _cellWidth = 0;
-const _cellFlags = 8;
-
-int _nextLength(int lengthRequirement) {
-  var nextLength = 2;
-  while (nextLength < lengthRequirement) {
-    nextLength *= 2;
-  }
-  return nextLength;
-}
-
-/// [List] based [BufferLineData], used in browser where ByteData is not avaliable.
-class ListBufferLineData with BufferLineData {
-  ListBufferLineData(int length, this.isWrapped) {
-    _maxCols = _nextLength(length);
-    _cells = List.filled(_maxCols * _cellSize, 0);
-  }
-
-  late List<int> _cells;
-
-  bool isWrapped;
-
-  int _maxCols = 64;
-
-  void ensure(int length) {
-    if (length <= _maxCols) {
-      return;
-    }
-
-    final nextLength = _nextLength(length);
-    final newCells = List.filled(nextLength * _cellSize, 0);
-    newCells.setAll(0, _cells);
-    _cells = newCells;
-    _maxCols = nextLength;
-  }
-
-  void insert(int index) {
-    insertN(index, 1);
-  }
-
-  void removeN(int index, int count) {
-    final moveStart = index * _cellSize;
-    final moveOffset = count * _cellSize;
-    final moveEnd = (_maxCols - count) * _cellSize;
-    final bufferEnd = _maxCols * _cellSize;
-
-    // move data backward
-    for (var i = moveStart; i < moveEnd; i++) {
-      _cells[i] = _cells[i + moveOffset];
-    }
-
-    // set empty cells to 0
-    for (var i = moveEnd; i < bufferEnd; i++) {
-      _cells[i] = 0x00;
-    }
-  }
-
-  void insertN(int index, int count) {
-    final moveStart = index * _cellSize;
-    final moveOffset = count * _cellSize;
-    final bufferEnd = _maxCols * _cellSize;
-
-    // move data forward
-    for (var i = bufferEnd - moveOffset - 1; i >= moveStart; i--) {
-      _cells[i + moveOffset] = _cells[i];
-    }
-
-    // set inserted cells to 0
-    for (var i = moveStart; i < moveStart + moveOffset; i++) {
-      _cells[i] = 0x00;
-    }
-  }
-
-  void clear() {
-    clearRange(0, _cells.length ~/ _cellSize);
-  }
-
-  void erase(Cursor cursor, int start, int end, [bool resetIsWrapped = false]) {
-    ensure(end);
-    for (var i = start; i < end; i++) {
-      cellErase(i, cursor);
-    }
-    if (resetIsWrapped) {
-      isWrapped = false;
-    }
-  }
-
-  void cellClear(int index) {
-    _cells.fillRange(index * _cellSize, index * _cellSize + _cellSize, 0);
-  }
-
-  void cellInitialize(
-    int index, {
-    required int content,
-    required int width,
-    required Cursor cursor,
-  }) {
-    final cell = index * _cellSize;
-    _cells[cell + _cellContent] = content;
-    _cells[cell + _cellFgColor] = cursor.fg;
-    _cells[cell + _cellBgColor] = cursor.bg;
-    _cells[cell + _cellAttributes] =
-        (width << _cellWidth) + (cursor.flags << _cellFlags);
-  }
-
-  bool cellHasContent(int index) {
-    return cellGetContent(index) != 0;
-  }
-
-  int cellGetContent(int index) {
-    if (index >= _maxCols) return 0;
-    return _cells[index * _cellSize + _cellContent];
-  }
-
-  void cellSetContent(int index, int content) {
-    _cells[index * _cellSize + _cellContent] = content;
-  }
-
-  int cellGetFgColor(int index) {
-    if (index >= _maxCols) return 0;
-    return _cells[index * _cellSize + _cellFgColor];
-  }
-
-  void cellSetFgColor(int index, int color) {
-    _cells[index * _cellSize + _cellFgColor] = color;
-  }
-
-  int cellGetBgColor(int index) {
-    if (index >= _maxCols) return 0;
-    return _cells[index * _cellSize + _cellBgColor];
-  }
-
-  void cellSetBgColor(int index, int color) {
-    _cells[index * _cellSize + _cellBgColor] = color;
-  }
-
-  int cellGetFlags(int index) {
-    if (index >= _maxCols) return 0;
-    final offset = index * _cellSize + _cellAttributes;
-    return (_cells[offset] >> _cellFlags) & 0xFF;
-  }
-
-  void cellSetFlags(int index, int flags) {
-    final offset = index * _cellSize + _cellAttributes;
-    var result = _cells[offset];
-    result |= 0xFF << _cellFlags;
-    result &= flags << _cellFlags;
-    _cells[offset] = result;
-  }
-
-  int cellGetWidth(int index) {
-    if (index >= _maxCols) return 0;
-    final offset = index * _cellSize + _cellAttributes;
-    return (_cells[offset] >> _cellWidth) & 0xFF;
-  }
-
-  void cellSetWidth(int index, int width) {
-    final offset = index * _cellSize + _cellAttributes;
-    var result = _cells[offset];
-    result |= 0xFF << _cellWidth;
-    result &= width << _cellWidth;
-    _cells[offset] = result;
-  }
-
-  void cellClearFlags(int index) {
-    cellSetFlags(index, 0);
-  }
-
-  bool cellHasFlag(int index, int flag) {
-    if (index >= _maxCols) {
-      return false;
-    }
-    return cellGetFlags(index) & flag != 0;
-  }
-
-  void cellSetFlag(int index, int flag) {
-    cellSetFlags(index, cellGetFlags(index) | flag);
-  }
-
-  void cellErase(int index, Cursor cursor) {
-    cellSetContent(index, 0x00);
-    cellSetFgColor(index, cursor.fg);
-    cellSetBgColor(index, cursor.bg);
-    cellSetFlags(index, cursor.flags);
-    cellSetWidth(index, 0);
-  }
-
-  int getTrimmedLength([int? cols]) {
-    if (cols == null) {
-      cols = _maxCols;
-    }
-    for (var i = cols - 1; i >= 0; i--) {
-      if (cellGetContent(i) != 0) {
-        // we are at the last cell in this line that has content.
-        // the length of this line is the index of this cell + 1
-        // the only exception is that if that last cell is wider
-        // than 1 then we have to add the diff
-        final lastCellWidth = cellGetWidth(i);
-        return i + lastCellWidth;
-      }
-    }
-    return 0;
-  }
-
-  void copyCellsFrom(ListBufferLineData src, int srcCol, int dstCol, int len) {
-    ensure(dstCol + len);
-
-    final intsToCopy = len * _cellSize;
-    final srcStart = srcCol * _cellSize;
-    final dstStart = dstCol * _cellSize;
-
-    final cells = _cells;
-    final srcCells = src._cells;
-    for (var i = 0; i < intsToCopy; i++) {
-      cells[dstStart + i] = srcCells[srcStart + i];
-    }
-  }
-
-  void removeRange(int start, int end) {
-    end = min(end, _maxCols);
-    this.removeN(start, end - start);
-  }
-
-  void clearRange(int start, int end) {
-    end = min(end, _maxCols);
-    for (var index = start; index < end; index++) {
-      cellClear(index);
-    }
-  }
-
-  @override
-  String toString() {
-    final result = StringBuffer();
-    for (int i = 0; i < _maxCols; i++) {
-      final code = cellGetContent(i);
-      if (code == 0) {
-        continue;
-      }
-      result.writeCharCode(code);
-    }
-    return result.toString();
-  }
-}

+ 0 - 11
lib/buffer/reflow_strategy.dart

@@ -1,11 +0,0 @@
-import 'package:xterm/buffer/buffer.dart';
-
-abstract class ReflowStrategy {
-  final Buffer _buffer;
-
-  ReflowStrategy(this._buffer);
-
-  Buffer get buffer => _buffer;
-
-  void reflow(int newCols, int newRows, int oldCols, int oldRows);
-}

+ 0 - 91
lib/buffer/reflow_strategy_narrower.dart

@@ -1,91 +0,0 @@
-import 'package:xterm/buffer/buffer.dart';
-import 'package:xterm/buffer/line/line.dart';
-import 'package:xterm/buffer/reflow_strategy.dart';
-
-class ReflowStrategyNarrower extends ReflowStrategy {
-  ReflowStrategyNarrower(Buffer buffer) : super(buffer);
-
-  @override
-  void reflow(int newCols, int newRows, int oldCols, int oldRows) {
-    final linesAfterReflow = <BufferLine>[];
-
-    //print('Reflow narrower $oldCols -> $newCols');
-    for (var i = 0; i < buffer.lines.length; i++) {
-      final line = buffer.lines[i];
-      final lineLength = line.getTrimmedLength();
-      linesAfterReflow.add(line);
-
-      if (lineLength > newCols) {
-        var moveIndexStart = newCols;
-        var cellsToCopy = lineLength - newCols;
-
-        // when we have a double width character and are about to move the "0" placeholder,
-        // then we have to move the double width character as well
-        if (line.cellGetContent(moveIndexStart) == 0 &&
-            line.cellGetWidth(moveIndexStart - 1) == 2) {
-          moveIndexStart -= 1;
-          cellsToCopy += 1;
-        }
-
-        var addZero = false;
-        //when the last cell to copy is a double width cell, then add a "0"
-        if (line.cellGetWidth(moveIndexStart + cellsToCopy - 1) == 2) {
-          addZero = true;
-        }
-
-        // var alreadyInserted = 0;
-
-        //when we have aggregated a whole new line then insert it now
-        while (cellsToCopy > newCols) {
-          final newLine = BufferLine(length: newCols, isWrapped: true);
-          newLine.copyCellsFrom(line, moveIndexStart, 0, newCols);
-          // line.clearRange(moveIndexStart, moveIndexStart + newCols);
-          line.removeN(moveIndexStart, newCols);
-
-          linesAfterReflow.add(newLine);
-
-          cellsToCopy -= newCols;
-          // alreadyInserted++;
-        }
-
-        // we need to move cut cells to the next line
-        // if the next line is wrapped anyway, we can push them onto the beginning of that line
-        // otherwise, we need add a new wrapped line
-        // final nextLineIndex = i + alreadyInserted + 1;
-        final nextLineIndex = i + 1;
-        if (nextLineIndex < buffer.lines.length) {
-          final nextLine = buffer.lines[nextLineIndex];
-          if (nextLine.isWrapped) {
-            final nextLineLength = nextLine.getTrimmedLength();
-            nextLine.ensure(nextLineLength + cellsToCopy + (addZero ? 1 : 0));
-            nextLine.insertN(0, cellsToCopy + (addZero ? 1 : 0));
-            nextLine.copyCellsFrom(line, moveIndexStart, 0, cellsToCopy);
-            // clean the cells that we moved
-            line.removeN(moveIndexStart, cellsToCopy);
-            // line.erase(buffer.terminal.cursor, moveIndexStart,
-            //     moveIndexStart + cellsToCopy);
-            //print('M: ${i < 10 ? '0' : ''}$i: ${line.toDebugString(oldCols)}');
-            //print(
-            //    'N: ${i + 1 < 10 ? '0' : ''}${i + 1}: ${nextLine.toDebugString(oldCols)}');
-            continue;
-          }
-        }
-
-        final newLine = BufferLine(length: newCols, isWrapped: true);
-        newLine.copyCellsFrom(line, moveIndexStart, 0, cellsToCopy);
-        // clean the cells that we moved
-        line.removeN(moveIndexStart, cellsToCopy);
-
-        linesAfterReflow.add(newLine);
-
-        //TODO: scrolling is a bit weird afterwards
-
-        //print('S: ${i < 10 ? '0' : ''}$i: ${line.toDebugString(oldCols)}');
-      } else {
-        //print('N: ${i < 10 ? '0' : ''}$i: ${line.toDebugString(oldCols)}');
-      }
-    }
-
-    buffer.lines.replaceWith(linesAfterReflow);
-  }
-}

+ 0 - 76
lib/buffer/reflow_strategy_wider.dart

@@ -1,76 +0,0 @@
-import 'dart:math';
-
-import 'package:xterm/buffer/buffer.dart';
-import 'package:xterm/buffer/line/line.dart';
-import 'package:xterm/buffer/reflow_strategy.dart';
-
-class ReflowStrategyWider extends ReflowStrategy {
-  ReflowStrategyWider(Buffer buffer) : super(buffer);
-
-  @override
-  void reflow(int newCols, int newRows, int oldCols, int oldRows) {
-    final linesAfterReflow = <BufferLine>[];
-
-    for (var i = 0; i < buffer.lines.length; i++) {
-      final line = buffer.lines[i];
-      line.ensure(newCols);
-      linesAfterReflow.add(line);
-
-      var linesToSkip = 0;
-      for (var offset = 1; i + offset < buffer.lines.length; offset++) {
-        final nextLine = buffer.lines[i + offset];
-        if (!nextLine.isWrapped) {
-          break;
-        }
-        // when we are reflowing wider we can be sure that this line and the next all have equal to or less than
-        // 'newCols' length => we can pass newCols as the upper limit
-        final lineLength = line.getTrimmedLength(newCols);
-
-        var copyDestIndex = lineLength;
-        if (copyDestIndex >= 1 &&
-            line.cellGetWidth(copyDestIndex - 1) == 2 &&
-            line.cellGetContent(copyDestIndex) == 0) {
-          //we would override a wide char placeholder => move index one to the right
-          copyDestIndex += 1;
-        }
-
-        final spaceOnLine = newCols - copyDestIndex;
-        if (spaceOnLine <= 0) {
-          // no more space to unwrap
-          break;
-        }
-        // when we are reflowing wider we can be sure that this line and the next all have equal to or less than
-        // 'newCols' length => we can pass newCols as the upper limit
-        final nextLineLength = nextLine.getTrimmedLength(newCols);
-        var moveCount = min(spaceOnLine, nextLineLength);
-        if (moveCount <= 0) {
-          break;
-        }
-
-        // when we are about to copy a double width character
-        // to the end of the line then we just ignore it as the target width
-        // would be too much
-        if (nextLine.cellGetWidth(moveCount - 1) == 2) {
-          moveCount -= 1;
-        }
-        line.copyCellsFrom(nextLine, 0, copyDestIndex, moveCount);
-        if (moveCount >= nextLineLength) {
-          // if we unwrapped all cells off the next line, skip it
-          linesToSkip++;
-        } else {
-          // otherwise just remove the characters we moved up a line
-          nextLine.removeN(0, moveCount);
-        }
-      }
-
-      // skip empty lines.
-      i += linesToSkip;
-    }
-    //buffer doesn't have enough lines
-    while (linesAfterReflow.length < buffer.viewHeight) {
-      linesAfterReflow.add(BufferLine(length: buffer.viewWidth));
-    }
-
-    buffer.lines.replaceWith(linesAfterReflow);
-  }
-}

+ 2 - 0
lib/core.dart

@@ -0,0 +1,2 @@
+export 'core/terminal.dart';
+export 'ui/terminal_view.dart';

+ 534 - 0
lib/core/buffer/buffer.dart

@@ -0,0 +1,534 @@
+import 'dart:math' show max, min;
+
+import 'package:xterm/core/buffer/position.dart';
+import 'package:xterm/core/buffer/range.dart';
+import 'package:xterm/core/cursor.dart';
+import 'package:xterm/core/buffer/line.dart';
+import 'package:xterm/core/reflow.dart';
+import 'package:xterm/core/state.dart';
+import 'package:xterm/core/charset.dart';
+import 'package:xterm/utils/circular_list.dart';
+import 'package:xterm/utils/unicode_v11.dart';
+
+class Buffer {
+  final TerminalState terminal;
+
+  final int maxLines;
+
+  final bool isAltBuffer;
+
+  Buffer(
+    this.terminal, {
+    required this.maxLines,
+    required this.isAltBuffer,
+  }) {
+    for (int i = 0; i < terminal.viewHeight; i++) {
+      lines.push(_newEmptyLine());
+    }
+
+    resetVerticalMargins();
+  }
+
+  int _cursorX = 0;
+
+  int _cursorY = 0;
+
+  late int _marginTop;
+
+  late int _marginBottom;
+
+  var _savedCursorX = 0;
+
+  var _savedCursorY = 0;
+
+  final _savedCursorStyle = CursorStyle();
+
+  final charset = Charset();
+
+  /// Width of the viewport in columns. Also the index of the last column.
+  int get viewWidth => terminal.viewWidth;
+
+  /// Height of the viewport in rows. Also the index of the last line.
+  int get viewHeight => terminal.viewHeight;
+
+  /// lines of the buffer. the length of [lines] should always be equal or
+  /// greater than [viewHeight].
+  late final lines = CircularList<BufferLine>(maxLines);
+
+  /// Total number of lines in the buffer. Always equal or greater than
+  /// [viewHeight].
+  int get height => lines.length;
+
+  /// Horizontal position of the cursor relative to the top-left cornor of the
+  /// screen, starting from 0.
+  int get cursorX => _cursorX.clamp(0, terminal.viewWidth - 1);
+
+  /// Vertical position of the cursor relative to the top-left cornor of the
+  /// screen, starting from 0.
+  int get cursorY => _cursorY;
+
+  /// Index of the first line in the scroll region.
+  int get marginTop => _marginTop;
+
+  /// Index of the last line in the scroll region.
+  int get marginBottom => _marginBottom;
+
+  /// The number of lines above the viewport.
+  int get scrollBack => height - viewHeight;
+
+  /// Vertical position of the cursor relative to the top of the buffer,
+  /// starting from 0.
+  int get absoluteCursorY => _cursorY + scrollBack;
+
+  /// Absolute index of the first line in the scroll region.
+  int get absoluteMarginTop => _marginTop + scrollBack;
+
+  /// Absolute index of the last line in the scroll region.
+  int get absoluteMarginBottom => _marginBottom + scrollBack;
+
+  /// Writes data to the _terminal. Terminal sequences or special characters are
+  /// not interpreted and directly added to the buffer.
+  ///
+  /// See also: [Terminal.write]
+  void write(String text) {
+    for (var char in text.runes) {
+      writeChar(char);
+    }
+  }
+
+  /// Writes a single character to the _terminal. Escape sequences or special
+  /// characters are not interpreted and directly added to the buffer.
+  ///
+  /// See also: [Terminal.writeChar]
+  void writeChar(int codePoint) {
+    codePoint = charset.translate(codePoint);
+
+    final cellWidth = unicodeV11.wcwidth(codePoint);
+    if (_cursorX >= terminal.viewWidth) {
+      index();
+      setCursorX(0);
+      if (terminal.autoWrapMode) {
+        currentLine.isWrapped = true;
+      }
+    }
+
+    final line = currentLine;
+    line.setCell(_cursorX, codePoint, cellWidth, terminal.cursor);
+
+    if (_cursorX < viewWidth) {
+      _cursorX++;
+    }
+
+    if (cellWidth == 2) {
+      writeChar(0);
+    }
+  }
+
+  /// The line at the current cursor position.
+  BufferLine get currentLine {
+    return lines[absoluteCursorY];
+  }
+
+  void backspace() {
+    if (_cursorX == 0 && currentLine.isWrapped) {
+      currentLine.isWrapped = false;
+      moveCursor(viewWidth - 1, -1);
+    } else if (_cursorX == viewWidth) {
+      moveCursor(-2, 0);
+    } else {
+      moveCursor(-1, 0);
+    }
+  }
+
+  /// Erases the viewport from the cursor position to the end of the buffer,
+  /// including the cursor position.
+  void eraseDisplayFromCursor() {
+    eraseLineFromCursor();
+
+    for (var i = absoluteCursorY; i < height; i++) {
+      final line = lines[i];
+      line.isWrapped = false;
+      line.eraseRange(0, viewWidth, terminal.cursor);
+    }
+  }
+
+  /// Erases the viewport from the top-left corner to the cursor, including the
+  /// cursor.
+  void eraseDisplayToCursor() {
+    eraseLineToCursor();
+
+    for (var i = 0; i < _cursorY; i++) {
+      final line = lines[i + scrollBack];
+      line.isWrapped = false;
+      line.eraseRange(0, viewWidth, terminal.cursor);
+    }
+  }
+
+  /// Erases the whole viewport.
+  void eraseDisplay() {
+    for (var i = 0; i < viewHeight; i++) {
+      final line = lines[i + scrollBack];
+      line.isWrapped = false;
+      line.eraseRange(0, viewWidth, terminal.cursor);
+    }
+  }
+
+  /// Erases the line from the cursor to the end of the line, including the
+  /// cursor position.
+  void eraseLineFromCursor() {
+    currentLine.isWrapped = false;
+    currentLine.eraseRange(_cursorX, viewWidth, terminal.cursor);
+  }
+
+  /// Erases the line from the start of the line to the cursor, including the
+  /// cursor.
+  void eraseLineToCursor() {
+    currentLine.isWrapped = false;
+    currentLine.eraseRange(0, _cursorX, terminal.cursor);
+  }
+
+  /// Erases the line at the current cursor position.
+  void eraseLine() {
+    currentLine.isWrapped = false;
+    currentLine.eraseRange(0, viewWidth, terminal.cursor);
+  }
+
+  /// Erases [count] cells starting at the cursor position.
+  void eraseChars(int count) {
+    final start = _cursorX;
+    currentLine.eraseRange(start, start + count, terminal.cursor);
+  }
+
+  void scrollDown(int lines) {
+    for (var i = absoluteMarginBottom; i >= absoluteMarginTop; i--) {
+      if (i >= absoluteMarginTop + lines) {
+        this.lines[i] = this.lines[i - lines];
+      } else {
+        this.lines[i] = _newEmptyLine();
+      }
+    }
+  }
+
+  void scrollUp(int lines) {
+    for (var i = absoluteMarginTop; i <= absoluteMarginBottom; i++) {
+      if (i <= absoluteMarginBottom - lines) {
+        this.lines[i] = this.lines[i + lines];
+      } else {
+        this.lines[i] = _newEmptyLine();
+      }
+    }
+  }
+
+  /// https://vt100.net/docs/vt100-ug/chapter3.html#IND IND – Index
+  ///
+  /// ESC D
+  ///
+  /// [index] causes the active position to move downward one line without
+  /// changing the column position. If the active position is at the bottom
+  /// margin, a scroll up is performed.
+  void index() {
+    if (isInVerticalMargin) {
+      if (_cursorY == _marginBottom) {
+        if (marginTop == 0 && !isAltBuffer) {
+          lines.insert(absoluteMarginBottom + 1, _newEmptyLine());
+        } else {
+          scrollUp(1);
+        }
+      } else {
+        moveCursorY(1);
+      }
+      return;
+    }
+
+    // the cursor is not in the scrollable region
+    if (_cursorY >= viewHeight - 1) {
+      // we are at the bottom
+      if (isAltBuffer) {
+        scrollUp(1);
+      } else {
+        lines.push(_newEmptyLine());
+      }
+    } else {
+      // there're still lines so we simply move cursor down.
+      moveCursorY(1);
+    }
+  }
+
+  void lineFeed() {
+    index();
+    if (terminal.lineFeedMode) {
+      setCursorX(0);
+    }
+  }
+
+  /// https://terminalguide.namepad.de/seq/a_esc_cm/
+  void reverseIndex() {
+    if (isInVerticalMargin) {
+      if (_cursorY == _marginTop) {
+        scrollDown(1);
+      } else {
+        moveCursorY(-1);
+      }
+    } else {
+      moveCursorY(-1);
+    }
+  }
+
+  void cursorGoForward() {
+    _cursorX = min(_cursorX + 1, viewWidth);
+  }
+
+  void setCursorX(int cursorX) {
+    _cursorX = cursorX.clamp(0, viewWidth - 1);
+  }
+
+  void setCursorY(int cursorY) {
+    _cursorY = cursorY.clamp(0, viewHeight - 1);
+  }
+
+  void moveCursorX(int offset) {
+    setCursorX(_cursorX + offset);
+  }
+
+  void moveCursorY(int offset) {
+    setCursorY(_cursorY + offset);
+  }
+
+  void setCursor(int cursorX, int cursorY) {
+    var maxCursorY = viewHeight - 1;
+
+    if (terminal.originMode) {
+      cursorY += _marginTop;
+      maxCursorY = _marginBottom;
+    }
+
+    _cursorX = cursorX.clamp(0, viewWidth - 1);
+    _cursorY = cursorY.clamp(0, maxCursorY);
+  }
+
+  void moveCursor(int offsetX, int offsetY) {
+    final cursorX = _cursorX + offsetX;
+    final cursorY = _cursorY + offsetY;
+    setCursor(cursorX, cursorY);
+  }
+
+  /// Save cursor position, charmap and text attributes.
+  void saveCursor() {
+    _savedCursorX = _cursorX;
+    _savedCursorY = _cursorY;
+    _savedCursorStyle.foreground = terminal.cursor.foreground;
+    _savedCursorStyle.background = terminal.cursor.background;
+    _savedCursorStyle.attrs = terminal.cursor.attrs;
+    charset.save();
+  }
+
+  /// Restore cursor position, charmap and text attributes.
+  void restoreCursor() {
+    _cursorX = _savedCursorX;
+    _cursorY = _savedCursorY;
+    terminal.cursor.foreground = _savedCursorStyle.foreground;
+    terminal.cursor.background = _savedCursorStyle.background;
+    terminal.cursor.attrs = _savedCursorStyle.attrs;
+    charset.restore();
+  }
+
+  /// Sets the vertical scrolling margin to [top] and [bottom].
+  /// Both values must be between 0 and [viewHeight] - 1.
+  void setVerticalMargins(int top, int bottom) {
+    _marginTop = top.clamp(0, viewHeight - 1);
+    _marginBottom = bottom.clamp(0, viewHeight - 1);
+
+    _marginTop = min(_marginTop, _marginBottom);
+    _marginBottom = max(_marginTop, _marginBottom);
+  }
+
+  bool get isInVerticalMargin {
+    return _cursorY >= _marginTop && _cursorY <= _marginBottom;
+  }
+
+  void resetVerticalMargins() {
+    setVerticalMargins(0, viewHeight - 1);
+  }
+
+  void deleteChars(int count) {
+    final start = _cursorX.clamp(0, viewWidth);
+    count = min(count, viewWidth - start);
+    currentLine.removeCells(start, count, terminal.cursor);
+  }
+
+  /// Remove all lines above the top of the viewport.
+  void clearScrollback() {
+    if (height <= viewHeight) {
+      return;
+    }
+
+    lines.trimStart(scrollBack);
+  }
+
+  /// Clears the viewport and scrollback buffer. Then fill with empty lines.
+  void clear() {
+    lines.clear();
+    for (int i = 0; i < viewHeight; i++) {
+      lines.push(_newEmptyLine());
+    }
+  }
+
+  void insertBlankChars(int count) {
+    currentLine.insertCells(_cursorX, count, terminal.cursor);
+  }
+
+  void insertLines(int count) {
+    if (!isInVerticalMargin) {
+      return;
+    }
+
+    setCursorX(0);
+
+    for (var i = 0; i < count; i++) {
+      final shiftStart = absoluteCursorY;
+      final shiftCount = absoluteMarginBottom - absoluteCursorY;
+      lines.shiftElements(shiftStart, shiftCount, 1);
+      lines[absoluteCursorY] = _newEmptyLine();
+    }
+  }
+
+  void deleteLines(int count) {
+    if (!isInVerticalMargin) {
+      return;
+    }
+
+    setCursorX(0);
+
+    for (var i = 0; i < count; i++) {
+      lines.insert(absoluteMarginBottom, _newEmptyLine());
+      lines.remove(absoluteCursorY);
+    }
+  }
+
+  void resize(int oldWidth, int oldHeight, int newWidth, int newHeight) {
+    if (newWidth > oldWidth) {
+      lines.forEach((item) => item.resize(newWidth));
+    }
+
+    if (newHeight > oldHeight) {
+      // Grow larger
+      for (var i = 0; i < newHeight - oldHeight; i++) {
+        if (newHeight > lines.length) {
+          lines.push(_newEmptyLine());
+        } else {
+          _cursorY++;
+        }
+      }
+    } else {
+      // Shrink smallerclear
+      for (var i = 0; i < oldHeight - newHeight; i++) {
+        if (_cursorY > newHeight - 1) {
+          _cursorY--;
+        } else {
+          lines.pop();
+        }
+      }
+    }
+
+    // Ensure cursor is within the screen.
+    _cursorX = _cursorX.clamp(0, newWidth - 1);
+    _cursorY = _cursorY.clamp(0, newHeight - 1);
+
+    if (!isAltBuffer && newWidth != oldWidth) {
+      final reflowResult = reflow(lines, oldWidth, newWidth);
+
+      while (reflowResult.length < newHeight) {
+        reflowResult.add(_newEmptyLine());
+      }
+
+      lines.replaceWith(reflowResult);
+    }
+  }
+
+  BufferLine _newEmptyLine() {
+    final line = BufferLine(viewWidth);
+    return line;
+  }
+
+  static final _kWordSeparators = <int>{
+    0,
+    r' '.codeUnitAt(0),
+    r'.'.codeUnitAt(0),
+    r':'.codeUnitAt(0),
+    r'-'.codeUnitAt(0),
+    r'\'.codeUnitAt(0),
+    r'"'.codeUnitAt(0),
+    r'*'.codeUnitAt(0),
+    r'+'.codeUnitAt(0),
+    r'/'.codeUnitAt(0),
+    r'\'.codeUnitAt(0),
+  };
+
+  BufferRange? getWordBoundary(BufferPosition position) {
+    if (position.y >= lines.length) {
+      return null;
+    }
+
+    var line = lines[position.y];
+    var start = position.x;
+    var end = position.x;
+
+    do {
+      if (start == 0) {
+        break;
+      }
+      final char = line.getCodePoint(start - 1);
+      if (_kWordSeparators.contains(char)) {
+        break;
+      }
+      start--;
+    } while (true);
+
+    do {
+      if (end >= viewWidth) {
+        break;
+      }
+      final char = line.getCodePoint(end);
+      if (_kWordSeparators.contains(char)) {
+        break;
+      }
+      end++;
+    } while (true);
+
+    return BufferRange(
+      BufferPosition(start, position.y),
+      BufferPosition(end, position.y),
+    );
+  }
+
+  String getText(BufferRange range) {
+    final builder = StringBuffer();
+    for (var i = range.begin.y; i <= range.end.y; i++) {
+      if (i < 0 || i >= lines.length) {
+        break;
+      }
+
+      final line = lines[i];
+      final start = i == range.begin.y ? range.begin.x : 0;
+      final end = i == range.end.y ? range.end.x : line.length;
+
+      if (i != range.begin.y && line.isWrapped) {
+        builder.write('\n');
+      }
+
+      builder.write(line.getText(start, end));
+    }
+
+    return builder.toString();
+  }
+
+  @override
+  String toString() {
+    final builder = StringBuffer();
+    final lineNumberLength = lines.length.toString().length;
+    for (var i = 0; i < lines.length; i++) {
+      builder.writeln('${i.toString().padLeft(lineNumberLength)}: ${lines[i]}');
+    }
+    return builder.toString();
+  }
+}

+ 0 - 0
lib/buffer/cell_flags.dart → lib/core/buffer/cell_flags.dart


+ 295 - 0
lib/core/buffer/line.dart

@@ -0,0 +1,295 @@
+import 'dart:math' show min;
+import 'dart:typed_data';
+
+import 'package:xterm/core/cell.dart';
+import 'package:xterm/core/cursor.dart';
+import 'package:xterm/utils/unicode_v11.dart';
+
+const _cellSize = 4;
+
+const _cellForeground = 0;
+
+const _cellBackground = 1;
+
+const _cellAttributes = 2;
+
+const _cellContent = 3;
+
+class BufferLine {
+  BufferLine(
+    this._length, {
+    this.isWrapped = false,
+  }) : _data = Uint32List(_calcCapacity(_length) * _cellSize);
+
+  int _length;
+
+  Uint32List _data;
+
+  Uint32List get data => _data;
+
+  var isWrapped = false;
+
+  int get length => _length;
+
+  int getForeground(int index) {
+    return _data[index * _cellSize + _cellForeground];
+  }
+
+  int getBackground(int index) {
+    return _data[index * _cellSize + _cellBackground];
+  }
+
+  int getAttributes(int index) {
+    return _data[index * _cellSize + _cellAttributes];
+  }
+
+  int getContent(int index) {
+    return _data[index * _cellSize + _cellContent];
+  }
+
+  int getCodePoint(int index) {
+    return _data[index * _cellSize + _cellContent] & CellContent.codepointMask;
+  }
+
+  int getWidth(int index) {
+    return _data[index * _cellSize + _cellContent] >> CellContent.widthShift;
+  }
+
+  void getCellData(int index, CellData cellData) {
+    final offset = index * _cellSize;
+    cellData.foreground = _data[offset + _cellForeground];
+    cellData.background = _data[offset + _cellBackground];
+    cellData.flags = _data[offset + _cellAttributes];
+    cellData.content = _data[offset + _cellContent];
+  }
+
+  CellData createCellData(int index) {
+    final cellData = CellData.empty();
+    final offset = index * _cellSize;
+    _data[offset + _cellForeground] = cellData.foreground;
+    _data[offset + _cellBackground] = cellData.background;
+    _data[offset + _cellAttributes] = cellData.flags;
+    _data[offset + _cellContent] = cellData.content;
+    return cellData;
+  }
+
+  void setForeground(int index, int value) {
+    _data[index * _cellSize + _cellForeground] = value;
+  }
+
+  void setBackground(int index, int value) {
+    _data[index * _cellSize + _cellBackground] = value;
+  }
+
+  void setAttributes(int index, int value) {
+    _data[index * _cellSize + _cellAttributes] = value;
+  }
+
+  void setContent(int index, int value) {
+    _data[index * _cellSize + _cellContent] = value;
+  }
+
+  void setCodePoint(int index, int char) {
+    final width = unicodeV11.wcwidth(char);
+    setContent(index, char | (width << CellContent.widthShift));
+  }
+
+  void setCell(int index, int char, int witdh, CursorStyle style) {
+    final offset = index * _cellSize;
+    _data[offset + _cellForeground] = style.foreground;
+    _data[offset + _cellBackground] = style.background;
+    _data[offset + _cellAttributes] = style.attrs;
+    _data[offset + _cellContent] = char | (witdh << CellContent.widthShift);
+  }
+
+  void setCellData(int index, CellData cellData) {
+    final offset = index * _cellSize;
+    _data[offset + _cellForeground] = cellData.foreground;
+    _data[offset + _cellBackground] = cellData.background;
+    _data[offset + _cellAttributes] = cellData.flags;
+    _data[offset + _cellContent] = cellData.content;
+  }
+
+  void eraseCell(int index, CursorStyle style) {
+    final offset = index * _cellSize;
+    _data[offset + _cellForeground] = style.foreground;
+    _data[offset + _cellBackground] = style.background;
+    _data[offset + _cellAttributes] = style.attrs;
+    _data[offset + _cellContent] = 0;
+  }
+
+  void resetCell(int index) {
+    final offset = index * _cellSize;
+    _data[offset + _cellForeground] = 0;
+    _data[offset + _cellBackground] = 0;
+    _data[offset + _cellAttributes] = 0;
+    _data[offset + _cellContent] = 0;
+  }
+
+  void eraseRange(int start, int end, CursorStyle style) {
+    // reset cell one to the left if start is second cell of a wide char
+    if (start > 0 && getWidth(start - 1) == 2) {
+      eraseCell(start - 1, style);
+    }
+
+    // reset cell one to the right if end is second cell of a wide char
+    if (end < _length && getWidth(end - 1) == 2) {
+      eraseCell(end - 1, style);
+    }
+
+    end = min(end, _length);
+    for (var i = start; i < end; i++) {
+      eraseCell(i, style);
+    }
+  }
+
+  void removeCells(int start, int count, [CursorStyle? style]) {
+    assert(start >= 0 && start < _length);
+    assert(count >= 0 && start + count <= _length);
+
+    style ??= CursorStyle.empty;
+
+    if (start + count < _length) {
+      final moveStart = start * _cellSize;
+      final moveEnd = (_length - count) * _cellSize;
+      final moveOffset = count * _cellSize;
+      for (var i = moveStart; i < moveEnd; i++) {
+        _data[i] = _data[i + moveOffset];
+      }
+    }
+
+    for (var i = _length - count; i < _length; i++) {
+      eraseCell(i, style);
+    }
+
+    if (start > 0 && getWidth(start - 1) == 2) {
+      eraseCell(start - 1, style);
+    }
+  }
+
+  void insertCells(int start, int count, [CursorStyle? style]) {
+    style ??= CursorStyle.empty;
+
+    if (start > 0 && getWidth(start - 1) == 2) {
+      eraseCell(start - 1, style);
+    }
+
+    if (start + count < _length) {
+      final moveStart = start * _cellSize;
+      final moveEnd = (_length - count) * _cellSize;
+      final moveOffset = count * _cellSize;
+      for (var i = moveEnd - 1; i >= moveStart; i--) {
+        _data[i + moveOffset] = _data[i];
+      }
+    }
+
+    final end = min(start + count, _length);
+    for (var i = start; i < end; i++) {
+      eraseCell(i, style);
+    }
+
+    if (getWidth(_length - 1) == 2) {
+      eraseCell(_length - 1, style);
+    }
+  }
+
+  void resize(int length) {
+    assert(length >= 0);
+
+    if (length == _length) {
+      return;
+    }
+
+    final newBufferSize = _calcCapacity(length) * _cellSize;
+
+    if (newBufferSize > _data.length) {
+      final newBuffer = Uint32List(newBufferSize);
+      newBuffer.setRange(0, _data.length, _data);
+      _data = newBuffer;
+    }
+
+    _length = length;
+  }
+
+  int getTrimmedLength([int? cols]) {
+    if (cols == null) {
+      cols = _data.length ~/ _cellSize;
+    }
+
+    for (var i = cols - 1; i >= 0; i--) {
+      var codePoint = getCodePoint(i);
+
+      if (codePoint != 0) {
+        // we are at the last cell in this line that has content.
+        // the length of this line is the index of this cell + 1
+        // the only exception is that if that last cell is wider
+        // than 1 then we have to add the diff
+        final lastCellWidth = getWidth(i);
+        return i + lastCellWidth;
+      }
+    }
+    return 0;
+  }
+
+  void copyFrom(BufferLine src, int srcCol, int dstCol, int len) {
+    resize(dstCol + len);
+
+    // data.setRange(
+    //   dstCol * _cellSize,
+    //   (dstCol + len) * _cellSize,
+    //   Uint32List.sublistView(src.data, srcCol * _cellSize, len * _cellSize),
+    // );
+
+    var srcOffset = srcCol * _cellSize;
+    var dstOffset = dstCol * _cellSize;
+
+    for (var i = 0; i < len * _cellSize; i++) {
+      _data[dstOffset++] = src._data[srcOffset++];
+    }
+  }
+
+  static int _calcCapacity(int length) {
+    assert(length >= 0);
+
+    var capacity = 64;
+
+    if (length < 256) {
+      while (capacity < length) {
+        capacity *= 2;
+      }
+    } else {
+      capacity = 256;
+      while (capacity < length) {
+        capacity += 32;
+      }
+    }
+
+    return capacity;
+  }
+
+  String getText([int? from, int? to]) {
+    if (from == null) {
+      from = 0;
+    }
+
+    if (to == null) {
+      to = _length;
+    }
+
+    final builder = StringBuffer();
+    for (var i = from; i < to; i++) {
+      final codePoint = getCodePoint(i);
+      final width = getWidth(i);
+      if (codePoint != 0 && i + width <= to) {
+        builder.writeCharCode(codePoint);
+      }
+    }
+
+    return builder.toString();
+  }
+
+  @override
+  String toString() {
+    return getText();
+  }
+}

+ 55 - 0
lib/core/buffer/position.dart

@@ -0,0 +1,55 @@
+import 'package:xterm/core/buffer/range.dart';
+
+class BufferPosition {
+  final int x;
+
+  final int y;
+
+  const BufferPosition(this.x, this.y);
+
+  bool isEqual(BufferPosition other) {
+    return other.x == x && other.y == y;
+  }
+
+  bool isBefore(BufferPosition other) {
+    return y < other.y || (y == other.y && x < other.x);
+  }
+
+  bool isAfter(BufferPosition other) {
+    return y > other.y || (y == other.y && x > other.x);
+  }
+
+  bool isBeforeOrSame(BufferPosition other) {
+    return y < other.y || (y == other.y && x <= other.x);
+  }
+
+  bool isAfterOrSame(BufferPosition other) {
+    return y > other.y || (y == other.y && x >= other.x);
+  }
+
+  bool isAtSameRow(BufferPosition other) {
+    return y == other.y;
+  }
+
+  bool isAtSameColumn(BufferPosition other) {
+    return x == other.x;
+  }
+
+  bool isWithin(BufferRange range) {
+    return range.begin.isBeforeOrSame(this) && range.end.isAfterOrSame(this);
+  }
+
+  @override
+  String toString() => 'Position($x, $y)';
+
+  @override
+  int get hashCode => x.hashCode ^ y.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is BufferPosition &&
+          runtimeType == other.runtimeType &&
+          x == other.x &&
+          y == other.y;
+}

+ 71 - 0
lib/core/buffer/range.dart

@@ -0,0 +1,71 @@
+import 'package:xterm/core/buffer/position.dart';
+import 'package:xterm/core/buffer/segment.dart';
+
+class BufferRange {
+  final BufferPosition begin;
+
+  final BufferPosition end;
+
+  BufferRange(this.begin, this.end);
+
+  BufferRange.collapsed(this.begin) : end = begin;
+
+  bool get isNormalized {
+    return begin.isBefore(end) || begin.isEqual(end);
+  }
+
+  bool get isCollapsed {
+    return begin.isEqual(end);
+  }
+
+  Iterable<BufferSegment> toSegments() sync* {
+    var start = this.begin;
+    var end = this.end;
+
+    if (!isNormalized) {
+      end = this.begin;
+      start = this.end;
+    }
+
+    for (var i = start.y; i <= end.y; i++) {
+      var startX = i == start.y ? start.x : null;
+      var endX = i == end.y ? end.x : null;
+      yield BufferSegment(this, i, startX, endX);
+    }
+  }
+
+  bool isWithin(BufferPosition position) {
+    return begin.isBeforeOrSame(position) && end.isAfterOrSame(position);
+  }
+
+  BufferRange merge(BufferRange range) {
+    final begin = this.begin.isBefore(range.begin) ? this.begin : range.begin;
+    final end = this.end.isAfter(range.end) ? this.end : range.end;
+    return BufferRange(begin, end);
+  }
+
+  BufferRange extend(BufferPosition position) {
+    final begin = this.begin.isBefore(position) ? position : this.begin;
+    final end = this.end.isAfter(position) ? position : this.end;
+    return BufferRange(begin, end);
+  }
+
+  @override
+  operator ==(Object other) {
+    if (identical(this, other)) {
+      return true;
+    }
+
+    if (other is! BufferRange) {
+      return false;
+    }
+
+    return begin == other.begin && end == other.end;
+  }
+
+  @override
+  int get hashCode => begin.hashCode ^ end.hashCode;
+
+  @override
+  String toString() => 'Range($begin, $end)';
+}

+ 51 - 0
lib/core/buffer/segment.dart

@@ -0,0 +1,51 @@
+import 'package:xterm/core/buffer/position.dart';
+import 'package:xterm/core/buffer/range.dart';
+
+class BufferSegment {
+  /// The range that this segment belongs to.
+  final BufferRange range;
+
+  /// The line that this segment resides on.
+  final int line;
+
+  /// The start position of this segment.
+  final int? start;
+
+  /// The end position of this segment. [null] if this segment is not closed.
+  final int? end;
+
+  const BufferSegment(this.range, this.line, this.start, this.end);
+
+  bool isWithin(BufferPosition position) {
+    if (position.y != line) {
+      return false;
+    }
+
+    if (start != null && position.x < start!) {
+      return false;
+    }
+
+    if (end != null && position.x > end!) {
+      return false;
+    }
+
+    return true;
+  }
+
+  @override
+  String toString() => 'Segment($line, $start, $end)';
+
+  @override
+  int get hashCode =>
+      range.hashCode ^ line.hashCode ^ start.hashCode ^ end.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is BufferSegment &&
+          runtimeType == other.runtimeType &&
+          range == other.range &&
+          line == other.line &&
+          start == other.start &&
+          end == other.end;
+}

+ 66 - 0
lib/core/cell.dart

@@ -0,0 +1,66 @@
+import 'package:xterm/utils/hash_values.dart';
+
+class CellData {
+  CellData({
+    required this.foreground,
+    required this.background,
+    required this.flags,
+    required this.content,
+  });
+
+  factory CellData.empty() {
+    return CellData(
+      foreground: 0,
+      background: 0,
+      flags: 0,
+      content: 0,
+    );
+  }
+
+  int foreground;
+
+  int background;
+
+  int flags;
+
+  int content;
+
+  int getHash() {
+    return hashValues(foreground, background, flags, content);
+  }
+
+  @override
+  String toString() {
+    return 'CellData{foreground: $foreground, background: $background, flags: $flags, content: $content}';
+  }
+}
+
+abstract class CellAttr {
+  static const bold = 1 << 0;
+  static const faint = 1 << 1;
+  static const italic = 1 << 2;
+  static const underline = 1 << 3;
+  static const blink = 1 << 4;
+  static const inverse = 1 << 5;
+  static const invisible = 1 << 6;
+  static const strikethrough = 1 << 7;
+}
+
+abstract class CellColor {
+  static const valueMask = 0xFFFFFF;
+
+  static const typeShift = 25;
+  static const typeMask = 3 << typeShift;
+
+  static const normal = 0 << typeShift;
+  static const named = 1 << typeShift;
+  static const palette = 2 << typeShift;
+  static const rgb = 3 << typeShift;
+}
+
+abstract class CellContent {
+  static const codepointMask = 0x1fffff;
+
+  static const widthShift = 22;
+  // static const widthMask = 3 << widthShift;
+}

+ 0 - 0
lib/terminal/charset.dart → lib/core/charset.dart


+ 19 - 0
lib/core/color.dart

@@ -0,0 +1,19 @@
+abstract class NamedColor {
+  static const black = 0;
+  static const red = 1;
+  static const green = 2;
+  static const yellow = 3;
+  static const blue = 4;
+  static const magenta = 5;
+  static const cyan = 6;
+  static const white = 7;
+
+  static const brightBlack = 8;
+  static const brightRed = 9;
+  static const brightGreen = 10;
+  static const brightYellow = 11;
+  static const brightBlue = 12;
+  static const brightMagenta = 13;
+  static const brightCyan = 14;
+  static const brightWhite = 15;
+}

+ 137 - 0
lib/core/cursor.dart

@@ -0,0 +1,137 @@
+import 'package:xterm/core/cell.dart';
+
+class CursorStyle {
+  int foreground;
+
+  int background;
+
+  int attrs;
+
+  CursorStyle({this.foreground = 0, this.background = 0, this.attrs = 0});
+
+  static final empty = CursorStyle();
+
+  void setBold() {
+    attrs |= CellAttr.bold;
+  }
+
+  void setFaint() {
+    attrs |= CellAttr.faint;
+  }
+
+  void setItalic() {
+    attrs |= CellAttr.italic;
+  }
+
+  void setUnderline() {
+    attrs |= CellAttr.underline;
+  }
+
+  void setBlink() {
+    attrs |= CellAttr.blink;
+  }
+
+  void setInverse() {
+    attrs |= CellAttr.inverse;
+  }
+
+  void setInvisible() {
+    attrs |= CellAttr.invisible;
+  }
+
+  void setStrikethrough() {
+    attrs |= CellAttr.strikethrough;
+  }
+
+  void unsetBold() {
+    attrs &= ~CellAttr.bold;
+  }
+
+  void unsetFaint() {
+    attrs &= ~CellAttr.faint;
+  }
+
+  void unsetItalic() {
+    attrs &= ~CellAttr.italic;
+  }
+
+  void unsetUnderline() {
+    attrs &= ~CellAttr.underline;
+  }
+
+  void unsetBlink() {
+    attrs &= ~CellAttr.blink;
+  }
+
+  void unsetInverse() {
+    attrs &= ~CellAttr.inverse;
+  }
+
+  void unsetInvisible() {
+    attrs &= ~CellAttr.invisible;
+  }
+
+  void unsetStrikethrough() {
+    attrs &= ~CellAttr.strikethrough;
+  }
+
+  bool get isBold => (attrs & CellAttr.bold) != 0;
+
+  bool get isFaint => (attrs & CellAttr.faint) != 0;
+
+  bool get isItalis => (attrs & CellAttr.italic) != 0;
+
+  bool get isUnderline => (attrs & CellAttr.underline) != 0;
+
+  bool get isBlink => (attrs & CellAttr.blink) != 0;
+
+  bool get isInverse => (attrs & CellAttr.inverse) != 0;
+
+  bool get isInvisible => (attrs & CellAttr.invisible) != 0;
+
+  void setForegroundColor16(int color) {
+    foreground = color | CellColor.named;
+  }
+
+  void setForegroundColor256(int color) {
+    foreground = color | CellColor.palette;
+  }
+
+  void setForegroundColorRgb(int r, int g, int b) {
+    foreground = (r << 16) | (g << 8) | b | CellColor.rgb;
+  }
+
+  void resetForegroundColor() {
+    foreground = 0; // | CellColor.normal;
+  }
+
+  void setBackgroundColor16(int color) {
+    background = color | CellColor.named;
+  }
+
+  void setBackgroundColor256(int color) {
+    background = color | CellColor.palette;
+  }
+
+  void setBackgroundColorRgb(int r, int g, int b) {
+    background = (r << 16) | (g << 8) | b | CellColor.rgb;
+  }
+
+  void resetBackgroundColor() {
+    background = 0; // | CellColor.normal;
+  }
+
+  void reset() {
+    foreground = 0;
+    background = 0;
+    attrs = 0;
+  }
+}
+
+class CursorPosition {
+  int x;
+
+  int y;
+
+  CursorPosition(this.x, this.y);
+}

+ 29 - 0
lib/core/escape/emitter.dart

@@ -0,0 +1,29 @@
+class EscapeEmitter {
+  const EscapeEmitter();
+
+  String primaryDeviceAttributes() {
+    return '\x1b[?1;2c';
+  }
+
+  String secondaryDeviceAttributes() {
+    const model = 0;
+    const version = 0;
+    return '\x1b[>$model;$version;0c';
+  }
+
+  String tertiaryDeviceAttributes() {
+    return '\x1bP!|00000000\x1b\\';
+  }
+
+  String operatingStatus() {
+    return '\x1b[0n';
+  }
+
+  String cursorPosition(int x, int y) {
+    return '\x1b[$y;${x}R';
+  }
+
+  String bracketedPaste(String text) {
+    return '\x1b[200~$text\x1b[201~';
+  }
+}

+ 211 - 0
lib/core/escape/handler.dart

@@ -0,0 +1,211 @@
+import 'package:xterm/core/mouse.dart';
+
+abstract class EscapeHandler {
+  void writeChar(int char);
+
+  /* SBC */
+
+  void bell();
+
+  void backspaceReturn();
+
+  void tab();
+
+  void lineFeed();
+
+  void carriageReturn();
+
+  void shiftOut();
+
+  void shiftIn();
+
+  void unknownSBC(int char);
+
+  /* ANSI sequence */
+
+  void saveCursor();
+
+  void restoreCursor();
+
+  void index();
+
+  void nextLine();
+
+  void setTapStop();
+
+  void reverseIndex();
+
+  void designateCharset(int charset);
+
+  void unkownEscape(int char);
+
+  /* CSI */
+
+  void repeatPreviousCharacter(int n);
+
+  void setCursor(int x, int y);
+
+  void setCursorX(int x);
+
+  void setCursorY(int y);
+
+  void sendPrimaryDeviceAttributes();
+
+  void clearTabStopUnderCursor();
+
+  void clearAllTabStops();
+
+  void moveCursorX(int offset);
+
+  void moveCursorY(int n);
+
+  void sendSecondaryDeviceAttributes();
+
+  void sendTertiaryDeviceAttributes();
+
+  void sendOperatingStatus();
+
+  void sendCursorPosition();
+
+  void setMargins(int i, [int? bottom]);
+
+  void cursorNextLine(int amount);
+
+  void cursorPrecedingLine(int amount);
+
+  void eraseDisplayBelow();
+
+  void eraseDisplayAbove();
+
+  void eraseDisplay();
+
+  void eraseScrollbackOnly();
+
+  void eraseLineRight();
+
+  void eraseLineLeft();
+
+  void eraseLine();
+
+  void insertLines(int amount);
+
+  void deleteLines(int amount);
+
+  void deleteChars(int amount);
+
+  void scrollUp(int amount);
+
+  void scrollDown(int amount);
+
+  void eraseChars(int amount);
+
+  void insertBlankChars(int amount);
+
+  void unknownCSI(int finalByte);
+
+  /* Modes */
+
+  void setInsertMode(bool enabled);
+
+  void setLineFeedMode(bool enabled);
+
+  void setUnknownMode(int mode, bool enabled);
+
+  /* DEC Private modes */
+
+  void setCursorKeysMode(bool enabled);
+
+  void setReverseDisplayMode(bool enabled);
+
+  void setOriginMode(bool enabled);
+
+  void setColumnMode(bool enabled);
+
+  void setAutoWrapMode(bool enabled);
+
+  void setMouseMode(MouseMode mode);
+
+  void setCursorBlinkMode(bool enabled);
+
+  void setCursorVisibleMode(bool enabled);
+
+  void useAltBuffer();
+
+  void useMainBuffer();
+
+  void clearAltBuffer();
+
+  void setAppKeypadMode(bool enabled);
+
+  void setReportFocusMode(bool enabled);
+
+  void setMouseReportMode(MouseReportMode mode);
+
+  void setAltBufferMouseScrollMode(bool enabled);
+
+  void setBracketedPasteMode(bool enabled);
+
+  void setUnknownDecMode(int mode, bool enabled);
+
+  /* Select Graphic Rendition (SGR) */
+
+  void resetCursorStyle();
+
+  void setCursorBold();
+
+  void setCursorFaint();
+
+  void setCursorItalic();
+
+  void setCursorUnderline();
+
+  void setCursorBlink();
+
+  void setCursorInverse();
+
+  void setCursorInvisible();
+
+  void setCursorStrikethrough();
+
+  void unsetCursorBold();
+
+  void unsetCursorFaint();
+
+  void unsetCursorItalic();
+
+  void unsetCursorUnderline();
+
+  void unsetCursorBlink();
+
+  void unsetCursorInverse();
+
+  void unsetCursorInvisible();
+
+  void unsetCursorStrikethrough();
+
+  void setForegroundColor16(int color);
+
+  void setForegroundColor256(int index);
+
+  void setForegroundColorRgb(int r, int g, int b);
+
+  void resetForeground();
+
+  void setBackgroundColor16(int color);
+
+  void setBackgroundColor256(int index);
+
+  void setBackgroundColorRgb(int r, int g, int b);
+
+  void resetBackground();
+
+  void unsupportedStyle(int param);
+
+  /* OSC */
+
+  void setTitle(String name);
+
+  void setIconName(String name);
+
+  void unknownOSC(String ps);
+}

+ 1095 - 0
lib/core/escape/parser.dart

@@ -0,0 +1,1095 @@
+import 'package:xterm/core/color.dart';
+import 'package:xterm/core/mouse.dart';
+import 'package:xterm/core/escape/handler.dart';
+import 'package:xterm/utils/ascii.dart';
+import 'package:xterm/utils/byte_consumer.dart';
+import 'package:xterm/utils/char_code.dart';
+import 'package:xterm/utils/lookup_table.dart';
+
+/// [EscapeParser] translates control characters and escape sequences into
+/// function calls that the terminal can handle.
+///
+/// Design goals:
+///  * Zero object allocation during processing.
+///  * No internal state. Same input will always produce same output.
+class EscapeParser {
+  final EscapeHandler handler;
+
+  EscapeParser(this.handler);
+
+  final _queue = ByteConsumer();
+
+  /// Start of sequence or character being processed. Useful for debugging.
+  var tokenBegin = 0;
+
+  /// End of sequence or character being processed. Useful for debugging.
+  int get tokenEnd => _queue.totalConsumed;
+
+  void write(String chunk) {
+    _queue.unrefConsumedBlocks();
+    _queue.add(chunk);
+    _process();
+  }
+
+  void _process() {
+    while (_queue.isNotEmpty) {
+      tokenBegin = _queue.totalConsumed;
+      final char = _queue.consume();
+
+      if (char == Ascii.ESC) {
+        final processed = _processEscape();
+        if (!processed) {
+          _queue.rollback(tokenEnd - tokenBegin);
+          return;
+        }
+      } else {
+        _processChar(char);
+      }
+    }
+  }
+
+  void _processChar(int char) {
+    if (char > _sbcHandlers.maxIndex) {
+      handler.writeChar(char);
+      return;
+    }
+
+    final sbcHandler = _sbcHandlers[char];
+    if (sbcHandler == null) {
+      handler.unkownEscape(char);
+      return;
+    }
+
+    sbcHandler();
+  }
+
+  /// Processes a sequence of characters that starts with an escape character.
+  /// Returns [true] if the sequence was processed, [false] if it was not.
+  bool _processEscape() {
+    if (_queue.isEmpty) return false;
+
+    final escapeChar = _queue.consume();
+    final escapeHandler = _escHandlers[escapeChar];
+
+    if (escapeHandler == null) {
+      handler.unkownEscape(escapeChar);
+      return true;
+    }
+
+    return escapeHandler();
+  }
+
+  late final _sbcHandlers = FastLookupTable<_SbcHandler>({
+    0x07: handler.bell,
+    0x08: handler.backspaceReturn,
+    0x09: handler.tab,
+    0x0a: handler.lineFeed,
+    0x0b: handler.lineFeed,
+    0x0c: handler.lineFeed,
+    0x0d: handler.carriageReturn,
+    0x0e: handler.shiftOut,
+    0x0f: handler.shiftIn,
+  });
+
+  late final _escHandlers = FastLookupTable<_EscHandler>({
+    '['.charCode: _escHandleCSI,
+    ']'.charCode: _escHandleOSC,
+    '7'.charCode: _escHandleSaveCursor,
+    '8'.charCode: _escHandleRestoreCursor,
+    'D'.charCode: _escHandleIndex,
+    'E'.charCode: _escHandleNextLine,
+    'H'.charCode: _escHandleTabSet,
+    'M'.charCode: _escHandleReverseIndex,
+    // 'P'.charCode: _unsupportedHandler, // Sixel
+    // 'c'.charCode: _unsupportedHandler,
+    // '#'.charCode: _unsupportedHandler,
+    '('.charCode: _escHandleDesignateCharset0, //  SCS - G0
+    ')'.charCode: _escHandleDesignateCharset1, //  SCS - G1
+    // '*'.charCode: _voidHandler(1), // TODO: G2 (vt220)
+    // '+'.charCode: _voidHandler(1), // TODO: G3 (vt220)
+    '>'.charCode: _escHandleResetAppKeypadMode, // TODO: Normal Keypad
+    '='.charCode: _escHandleSetAppKeypadMode, // TODO: Application Keypad
+  });
+
+  /// `ESC 7` Save Cursor (DECSC)
+  ///
+  /// https://terminalguide.namepad.de/seq/a_esc_a7/
+  bool _escHandleSaveCursor() {
+    handler.saveCursor();
+    return true;
+  }
+
+  /// `ESC 8` Restore Cursor (DECRC)
+  ///
+  /// https://terminalguide.namepad.de/seq/a_esc_a8/
+  bool _escHandleRestoreCursor() {
+    handler.restoreCursor();
+    return true;
+  }
+
+  /// `ESC D` Index (IND)
+  ///
+  /// https://terminalguide.namepad.de/seq/a_esc_cd/
+  bool _escHandleIndex() {
+    handler.index();
+    return true;
+  }
+
+  /// `ESC E` Next Line (NEL)
+  ///
+  /// https://terminalguide.namepad.de/seq/a_esc_ce/
+  bool _escHandleNextLine() {
+    handler.nextLine();
+    return true;
+  }
+
+  /// `ESC H` Horizontal Tab Set (HTS)
+  ///
+  /// https://terminalguide.namepad.de/seq/a_esc_ch/
+  bool _escHandleTabSet() {
+    handler.setTapStop();
+    return true;
+  }
+
+  /// `ESC M` Reverse Index (RI)
+  ///
+  /// https://terminalguide.namepad.de/seq/a_esc_cm/
+  bool _escHandleReverseIndex() {
+    handler.reverseIndex();
+    return true;
+  }
+
+  bool _escHandleDesignateCharset0() {
+    if (_queue.isEmpty) return false;
+    _queue.consume();
+    handler.designateCharset(0);
+    return true;
+  }
+
+  bool _escHandleDesignateCharset1() {
+    if (_queue.isEmpty) return false;
+    _queue.consume();
+    handler.designateCharset(1);
+    return true;
+  }
+
+  /// `ESC >` Reset Application Keypad Mode (DECKPNM)
+  ///
+  /// https://terminalguide.namepad.de/seq/a_esc_x3c_greater_than/
+  bool _escHandleSetAppKeypadMode() {
+    handler.setAppKeypadMode(true);
+    return true;
+  }
+
+  /// `ESC =` Set Application Keypad Mode (DECKPAM)
+  ///
+  /// https://terminalguide.namepad.de/seq/a_esc_x3d_equals/
+  bool _escHandleResetAppKeypadMode() {
+    handler.setAppKeypadMode(false);
+    return true;
+  }
+
+  bool _escHandleCSI() {
+    final consumed = _consumeCsi();
+    if (!consumed) return false;
+
+    final csiHandler = _csiHandlers[_csi.finalByte];
+
+    if (csiHandler == null) {
+      handler.unknownCSI(_csi.finalByte);
+    } else {
+      csiHandler();
+    }
+
+    return true;
+  }
+
+  /// The last parsed [_Csi]. This is a mutable singletion by design to reduce
+  /// object allocations.
+  final _csi = _Csi(finalByte: 0, params: []);
+
+  /// Parse a CSI from the head of the queue. Return false if the CSI isn't
+  /// complete. After a CSI is successfully parsed, [_csi] is updated.
+  bool _consumeCsi() {
+    if (_queue.isEmpty) {
+      return false;
+    }
+
+    _csi.params.clear();
+
+    // test whether the csi is a `CSI ? Ps ...` or `CSI Ps ...`
+    final prefix = _queue.peek();
+    if (prefix >= Ascii.colon && prefix <= Ascii.questionMark) {
+      _csi.prefix = prefix;
+      _queue.consume();
+    } else {
+      _csi.prefix = null;
+    }
+
+    var param = 0;
+    var hasParam = false;
+    while (true) {
+      // The sequence isn't completed, just ignore it.
+      if (_queue.isEmpty) {
+        return false;
+      }
+
+      final char = _queue.consume();
+
+      if (char == Ascii.semicolon) {
+        if (hasParam) {
+          _csi.params.add(param);
+        }
+        param = 0;
+        continue;
+      }
+
+      if (char >= Ascii.num0 && char <= Ascii.num9) {
+        hasParam = true;
+        param *= 10;
+        param += char - Ascii.num0;
+        continue;
+      }
+
+      if (char > Ascii.NULL && char < Ascii.num0) {
+        // intermediates.add(char);
+        continue;
+      }
+
+      if (char >= Ascii.atSign && char <= Ascii.tilde) {
+        if (hasParam) {
+          _csi.params.add(param);
+        }
+
+        _csi.finalByte = char;
+        return true;
+      }
+    }
+  }
+
+  late final _csiHandlers = FastLookupTable<_CsiHandler>({
+    // 'a'.codeUnitAt(0): _csiHandleCursorHorizontalRelative,
+    'b'.codeUnitAt(0): _csiHandleRepeatPreviousCharacter,
+    'c'.codeUnitAt(0): _csiHandleSendDeviceAttributes,
+    'd'.codeUnitAt(0): _csiHandleLinePositionAbsolute,
+    'f'.codeUnitAt(0): _csiHandleCursorPosition,
+    'g'.codeUnitAt(0): _csiHandelClearTabStop,
+    'h'.codeUnitAt(0): _csiHandleMode,
+    'l'.codeUnitAt(0): _csiHandleMode,
+    'm'.codeUnitAt(0): _csiHandleSgr,
+    'n'.codeUnitAt(0): _csiHandleDeviceStatusReport,
+    'r'.codeUnitAt(0): _csiHandleSetMargins,
+    't'.codeUnitAt(0): _csiWindowManipulation,
+    'A'.codeUnitAt(0): _csiHandleCursorUp,
+    'B'.codeUnitAt(0): _csiHandleCursorDown,
+    'C'.codeUnitAt(0): _csiHandleCursorForward,
+    'D'.codeUnitAt(0): _csiHandleCursorBackward,
+    'E'.codeUnitAt(0): _csiHandleCursorNextLine,
+    'F'.codeUnitAt(0): _csiHandleCursorPrecedingLine,
+    'G'.codeUnitAt(0): _csiHandleCursorHorizontalAbsolute,
+    'H'.codeUnitAt(0): _csiHandleCursorPosition,
+    'J'.codeUnitAt(0): _csiHandleEraseDisplay,
+    'K'.codeUnitAt(0): _csiHandleEraseLine,
+    'L'.codeUnitAt(0): _csiHandleInsertLines,
+    'M'.codeUnitAt(0): _csiHandleDeleteLines,
+    'P'.codeUnitAt(0): _csiHandleDelete,
+    'S'.codeUnitAt(0): _csiHandleScrollUp,
+    'T'.codeUnitAt(0): _csiHandleScrollDown,
+    'X'.codeUnitAt(0): _csiHandleEraseCharacters,
+    '@'.codeUnitAt(0): _csiHandleInsertBlankCharacters,
+  });
+
+  /// `ESC [ Ps a` Cursor Horizontal Position Relative (HPR)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sa/
+  // void _csiHandleCursorHorizontalRelative() {
+  //   if (_csi.params.isEmpty) {
+  //     handler.cursorHorizontal(1);
+  //   } else {
+  //     handler.cursorHorizontal(_csi.params[0]);
+  //   }
+  // }
+
+  /// `ESC [ Ps b` Repeat Previous Character (REP)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sb/
+  void _csiHandleRepeatPreviousCharacter() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+      if (amount == 0) amount = 1;
+    }
+
+    handler.repeatPreviousCharacter(amount);
+  }
+
+  /// `ESC [ Ps c` Device Attributes (DA)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sc/
+  void _csiHandleSendDeviceAttributes() {
+    switch (_csi.prefix) {
+      case Ascii.greaterThan:
+        return handler.sendSecondaryDeviceAttributes();
+      case Ascii.equal:
+        return handler.sendTertiaryDeviceAttributes();
+      default:
+        handler.sendPrimaryDeviceAttributes();
+    }
+  }
+
+  /// `ESC [ Ps d` Cursor Vertical Position Absolute (VPA)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sd/
+  void _csiHandleLinePositionAbsolute() {
+    var y = 1;
+
+    if (_csi.params.isNotEmpty) {
+      y = _csi.params[0];
+    }
+
+    handler.setCursorY(y - 1);
+  }
+
+  /// `ESC [ Ps ; Ps f` Alias: Set Cursor Position
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sf/
+  void _csiHandleCursorPosition() {
+    var row = 1;
+    var col = 1;
+
+    if (_csi.params.length == 2) {
+      row = _csi.params[0];
+      col = _csi.params[1];
+    }
+
+    handler.setCursor(col - 1, row - 1);
+  }
+
+  /// `ESC [ Ps g` Tab Clear (TBC)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sg/
+  void _csiHandelClearTabStop() {
+    var cmd = 0;
+
+    if (_csi.params.length == 1) {
+      cmd = _csi.params[0];
+    }
+
+    switch (cmd) {
+      case 0:
+        return handler.clearTabStopUnderCursor();
+      default:
+        return handler.clearAllTabStops();
+    }
+  }
+
+  /// - `ESC [ [ Pm ] h Set Mode (SM)` https://terminalguide.namepad.de/seq/csi_sm/
+  /// - `ESC [ ? [ Pm ] h` Set Mode (?) (SM) https://terminalguide.namepad.de/seq/csi_sh__p/
+  /// - `ESC [ [ Pm ] l` Reset Mode (RM) https://terminalguide.namepad.de/seq/csi_rm/
+  /// - `ESC [ ? [ Pm ] l` Reset Mode (?) (RM) https://terminalguide.namepad.de/seq/csi_sl__p/
+  void _csiHandleMode() {
+    final isEnabled = _csi.finalByte == Ascii.h;
+
+    final isDecModes = _csi.prefix == Ascii.questionMark;
+
+    if (isDecModes) {
+      for (var mode in _csi.params) {
+        _setDecMode(mode, isEnabled);
+      }
+    } else {
+      for (var mode in _csi.params) {
+        _setMode(mode, isEnabled);
+      }
+    }
+  }
+
+  /// `ESC [ [ Ps ] m` Select Graphic Rendition (SGR)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sm/
+  void _csiHandleSgr() {
+    final params = _csi.params;
+
+    if (params.isEmpty) {
+      return handler.resetCursorStyle();
+    }
+
+    for (var i = 0; i < _csi.params.length; i++) {
+      final param = params[i];
+      switch (param) {
+        case 0:
+          handler.resetCursorStyle();
+          continue;
+        case 1:
+          handler.setCursorBold();
+          continue;
+        case 2:
+          handler.setCursorFaint();
+          continue;
+        case 3:
+          handler.setCursorItalic();
+          continue;
+        case 4:
+          handler.setCursorUnderline();
+          continue;
+        case 5:
+          handler.setCursorBlink();
+          continue;
+        case 7:
+          handler.setCursorInverse();
+          continue;
+        case 8:
+          handler.setCursorInvisible();
+          continue;
+        case 9:
+          handler.setCursorStrikethrough();
+          continue;
+
+        case 21:
+          handler.unsetCursorBold();
+          continue;
+        case 22:
+          handler.unsetCursorFaint();
+          continue;
+        case 23:
+          handler.unsetCursorItalic();
+          continue;
+        case 24:
+          handler.unsetCursorUnderline();
+          continue;
+        case 25:
+          handler.unsetCursorBlink();
+          continue;
+        case 27:
+          handler.unsetCursorInverse();
+          continue;
+        case 28:
+          handler.unsetCursorInvisible();
+          continue;
+        case 29:
+          handler.unsetCursorStrikethrough();
+          continue;
+
+        case 30:
+          handler.setForegroundColor16(NamedColor.black);
+          continue;
+        case 31:
+          handler.setForegroundColor16(NamedColor.red);
+          continue;
+        case 32:
+          handler.setForegroundColor16(NamedColor.green);
+          continue;
+        case 33:
+          handler.setForegroundColor16(NamedColor.yellow);
+          continue;
+        case 34:
+          handler.setForegroundColor16(NamedColor.blue);
+          continue;
+        case 35:
+          handler.setForegroundColor16(NamedColor.magenta);
+          continue;
+        case 36:
+          handler.setForegroundColor16(NamedColor.cyan);
+          continue;
+        case 37:
+          handler.setForegroundColor16(NamedColor.white);
+          continue;
+        case 38:
+          final mode = params[i + 1];
+          switch (mode) {
+            case 2:
+              final r = params[i + 2];
+              final g = params[i + 3];
+              final b = params[i + 4];
+              handler.setForegroundColorRgb(r, g, b);
+              i += 4;
+              break;
+            case 5:
+              final index = params[i + 2];
+              handler.setForegroundColor256(index);
+              i += 2;
+              break;
+          }
+          continue;
+        case 39:
+          handler.resetForeground();
+          continue;
+
+        case 40:
+          handler.setBackgroundColor16(NamedColor.black);
+          continue;
+        case 41:
+          handler.setBackgroundColor16(NamedColor.red);
+          continue;
+        case 42:
+          handler.setBackgroundColor16(NamedColor.green);
+          continue;
+        case 43:
+          handler.setBackgroundColor16(NamedColor.yellow);
+          continue;
+        case 44:
+          handler.setBackgroundColor16(NamedColor.blue);
+          continue;
+        case 45:
+          handler.setBackgroundColor16(NamedColor.magenta);
+          continue;
+        case 46:
+          handler.setBackgroundColor16(NamedColor.cyan);
+          continue;
+        case 47:
+          handler.setBackgroundColor16(NamedColor.white);
+          continue;
+        case 48:
+          final mode = params[i + 1];
+          switch (mode) {
+            case 2:
+              final r = params[i + 2];
+              final g = params[i + 3];
+              final b = params[i + 4];
+              handler.setBackgroundColorRgb(r, g, b);
+              i += 4;
+              break;
+            case 5:
+              final index = params[i + 2];
+              handler.setBackgroundColor256(index);
+              i += 2;
+              break;
+          }
+          continue;
+        case 49:
+          handler.resetBackground();
+          continue;
+
+        case 90:
+          handler.setForegroundColor16(NamedColor.brightBlack);
+          continue;
+        case 91:
+          handler.setForegroundColor16(NamedColor.brightRed);
+          continue;
+        case 92:
+          handler.setForegroundColor16(NamedColor.brightGreen);
+          continue;
+        case 93:
+          handler.setForegroundColor16(NamedColor.brightYellow);
+          continue;
+        case 94:
+          handler.setForegroundColor16(NamedColor.brightBlue);
+          continue;
+        case 95:
+          handler.setForegroundColor16(NamedColor.brightMagenta);
+          continue;
+        case 96:
+          handler.setForegroundColor16(NamedColor.brightCyan);
+          continue;
+        case 97:
+          handler.setForegroundColor16(NamedColor.brightWhite);
+          continue;
+
+        case 100:
+          handler.setBackgroundColor16(NamedColor.brightBlack);
+          continue;
+        case 101:
+          handler.setBackgroundColor16(NamedColor.brightRed);
+          continue;
+        case 102:
+          handler.setBackgroundColor16(NamedColor.brightGreen);
+          continue;
+        case 103:
+          handler.setBackgroundColor16(NamedColor.brightYellow);
+          continue;
+        case 104:
+          handler.setBackgroundColor16(NamedColor.brightBlue);
+          continue;
+        case 105:
+          handler.setBackgroundColor16(NamedColor.brightMagenta);
+          continue;
+        case 106:
+          handler.setBackgroundColor16(NamedColor.brightCyan);
+          continue;
+        case 107:
+          handler.setBackgroundColor16(NamedColor.brightWhite);
+          continue;
+
+        default:
+          handler.unsupportedStyle(param);
+          continue;
+      }
+    }
+  }
+
+  /// `ESC [ Ps n` Device Status Report [Dispatch] (DSR)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sn/
+  void _csiHandleDeviceStatusReport() {
+    if (_csi.params.isEmpty) return;
+
+    switch (_csi.params[0]) {
+      case 5:
+        return handler.sendOperatingStatus();
+      case 6:
+        return handler.sendCursorPosition();
+    }
+  }
+
+  /// `ESC [ Ps ; Ps r` Set Top and Bottom Margins (DECSTBM)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_sr/
+  void _csiHandleSetMargins() {
+    var top = 1;
+    int? bottom;
+
+    if (_csi.params.length > 2) return;
+
+    if (_csi.params.isNotEmpty) {
+      top = _csi.params[0];
+
+      if (_csi.params.length == 2) {
+        bottom = _csi.params[1] - 1;
+      }
+    }
+
+    handler.setMargins(top - 1, bottom);
+  }
+
+  /// `ESC [ Ps t` Window operations [DISPATCH]
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_st/
+  void _csiWindowManipulation() {
+    // Not supported.
+  }
+
+  /// `ESC [ Ps A` Cursor Up (CUU)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_ca/
+  void _csiHandleCursorUp() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+      if (amount == 0) amount = 1;
+    }
+
+    handler.moveCursorY(-amount);
+  }
+
+  /// `ESC [ Ps B` Cursor Down (CUD)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cb/
+  void _csiHandleCursorDown() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+      if (amount == 0) amount = 1;
+    }
+
+    handler.moveCursorY(amount);
+  }
+
+  /// `ESC [ Ps C` Cursor Right (CUF)
+  ///
+  /// Cursor Right (CUF)
+  void _csiHandleCursorForward() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+      if (amount == 0) amount = 1;
+    }
+
+    handler.moveCursorX(amount);
+  }
+
+  /// `ESC [ Ps D` Cursor Left (CUB)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cd/
+  void _csiHandleCursorBackward() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+      if (amount == 0) amount = 1;
+    }
+
+    handler.moveCursorX(-amount);
+  }
+
+  /// `ESC [ Ps E` Cursor Next Line (CNL)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_ce/
+  void _csiHandleCursorNextLine() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+      if (amount == 0) amount = 1;
+    }
+
+    handler.cursorNextLine(amount);
+  }
+
+  /// `ESC [ Ps F` Cursor Previous Line (CPL)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cf/
+  void _csiHandleCursorPrecedingLine() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+      if (amount == 0) amount = 1;
+    }
+
+    handler.cursorPrecedingLine(amount);
+  }
+
+  void _csiHandleCursorHorizontalAbsolute() {
+    var x = 1;
+
+    if (_csi.params.isNotEmpty) {
+      x = _csi.params[0];
+      if (x == 0) x = 1;
+    }
+
+    handler.setCursorX(x - 1);
+  }
+
+  /// ESC [ Ps J Erase Display [Dispatch] (ED)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cj/
+  void _csiHandleEraseDisplay() {
+    var cmd = 0;
+
+    if (_csi.params.length == 1) {
+      cmd = _csi.params[0];
+    }
+
+    switch (cmd) {
+      case 0:
+        return handler.eraseDisplayBelow();
+      case 1:
+        return handler.eraseDisplayAbove();
+      case 2:
+        return handler.eraseDisplay();
+      case 3:
+        return handler.eraseScrollbackOnly();
+    }
+  }
+
+  /// `ESC [ Ps K` Erase Line [Dispatch] (EL)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_ck/
+  void _csiHandleEraseLine() {
+    var cmd = 0;
+
+    if (_csi.params.length == 1) {
+      cmd = _csi.params[0];
+    }
+
+    switch (cmd) {
+      case 0:
+        return handler.eraseLineRight();
+      case 1:
+        return handler.eraseLineLeft();
+      case 2:
+        return handler.eraseLine();
+    }
+  }
+
+  /// `ESC [ Ps L` Insert Line (IL)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cl/
+  void _csiHandleInsertLines() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+    }
+
+    handler.insertLines(amount);
+  }
+
+  /// ESC [ Ps M Delete Line (DL)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cm/
+  void _csiHandleDeleteLines() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+    }
+
+    handler.deleteLines(amount);
+  }
+
+  /// ESC [ Ps P Delete Character (DCH)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cp/
+  void _csiHandleDelete() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+    }
+
+    handler.deleteChars(amount);
+  }
+
+  /// `ESC [ Ps S` Scroll Up (SU)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cs/
+  void _csiHandleScrollUp() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+    }
+
+    handler.scrollUp(amount);
+  }
+
+  /// `ESC [ Ps T `Scroll Down (SD)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_ct_1param/
+  void _csiHandleScrollDown() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+    }
+
+    handler.scrollDown(amount);
+  }
+
+  /// `ESC [ Ps X` Erase Character (ECH)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_cx/
+  void _csiHandleEraseCharacters() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+    }
+
+    handler.eraseChars(amount);
+  }
+
+  /// `ESC [ Ps @` Insert Blanks (ICH)
+  ///
+  /// https://terminalguide.namepad.de/seq/csi_x40_at/
+  ///
+  /// Inserts amount spaces at current cursor position moving existing cell
+  /// contents to the right. The contents of the amount right-most columns in
+  /// the scroll region are lost. The cursor position is not changed.
+  void _csiHandleInsertBlankCharacters() {
+    var amount = 1;
+
+    if (_csi.params.isNotEmpty) {
+      amount = _csi.params[0];
+    }
+
+    handler.insertBlankChars(amount);
+  }
+
+  void _setMode(int mode, bool enabled) {
+    switch (mode) {
+      case 4:
+        return handler.setInsertMode(enabled);
+      case 20:
+        return handler.setLineFeedMode(enabled);
+      default:
+        return handler.setUnknownMode(mode, enabled);
+    }
+  }
+
+  void _setDecMode(int mode, bool enabled) {
+    switch (mode) {
+      case 1:
+        return handler.setCursorKeysMode(enabled);
+      case 3:
+        return handler.setColumnMode(enabled);
+      case 5:
+        return handler.setReverseDisplayMode(enabled);
+      case 6:
+        return handler.setOriginMode(enabled);
+      case 7:
+        return handler.setAutoWrapMode(enabled);
+      case 9:
+        return enabled
+            ? handler.setMouseMode(MouseMode.clickOnly)
+            : handler.setMouseMode(MouseMode.none);
+      case 12:
+      case 13:
+        return handler.setCursorBlinkMode(enabled);
+      case 25:
+        return handler.setCursorVisibleMode(enabled);
+      case 47:
+        if (enabled) {
+          return handler.useAltBuffer();
+        } else {
+          return handler.useMainBuffer();
+        }
+      case 66:
+        return handler.setAppKeypadMode(enabled);
+      case 1000:
+      case 10061000:
+        return enabled
+            ? handler.setMouseMode(MouseMode.upDownScroll)
+            : handler.setMouseMode(MouseMode.none);
+      case 1001:
+        return enabled
+            ? handler.setMouseMode(MouseMode.upDownScroll)
+            : handler.setMouseMode(MouseMode.none);
+      case 1002:
+        return enabled
+            ? handler.setMouseMode(MouseMode.upDownScrollDrag)
+            : handler.setMouseMode(MouseMode.none);
+      case 1003:
+        return enabled
+            ? handler.setMouseMode(MouseMode.upDownScrollMove)
+            : handler.setMouseMode(MouseMode.none);
+      case 1004:
+        return handler.setReportFocusMode(enabled);
+      case 1005:
+        return enabled
+            ? handler.setMouseReportMode(MouseReportMode.utf)
+            : handler.setMouseReportMode(MouseReportMode.normal);
+      case 1006:
+        return enabled
+            ? handler.setMouseReportMode(MouseReportMode.sgr)
+            : handler.setMouseReportMode(MouseReportMode.normal);
+      case 1007:
+        return handler.setAltBufferMouseScrollMode(enabled);
+      case 1015:
+        return enabled
+            ? handler.setMouseReportMode(MouseReportMode.urxvt)
+            : handler.setMouseReportMode(MouseReportMode.normal);
+      case 1047:
+        if (enabled) {
+          handler.useAltBuffer();
+        } else {
+          handler.clearAltBuffer();
+          handler.useMainBuffer();
+        }
+        return;
+      case 1048:
+        if (enabled) {
+          return handler.saveCursor();
+        } else {
+          return handler.restoreCursor();
+        }
+      case 1049:
+        if (enabled) {
+          handler.saveCursor();
+          handler.clearAltBuffer();
+          handler.useAltBuffer();
+        } else {
+          handler.useMainBuffer();
+        }
+        return;
+      case 2004:
+        return handler.setBracketedPasteMode(enabled);
+      default:
+        return handler.setUnknownDecMode(mode, enabled);
+    }
+  }
+
+  bool _escHandleOSC() {
+    final consumed = _consumeOsc();
+    if (!consumed) return false;
+
+    if (_osc.length < 2) {
+      return true;
+    }
+
+    final ps = _osc[0];
+    final pt = _osc[1];
+
+    switch (ps) {
+      case '0':
+        handler.setTitle(pt);
+        handler.setIconName(pt);
+        break;
+      case '1':
+        handler.setIconName(pt);
+        break;
+      case '2':
+        handler.setTitle(pt);
+        break;
+      default:
+        handler.unknownOSC(ps);
+    }
+
+    return true;
+  }
+
+  final _osc = <String>[];
+
+  bool _consumeOsc() {
+    _osc.clear();
+    final param = StringBuffer();
+
+    while (true) {
+      if (_queue.isEmpty) {
+        return false;
+      }
+
+      final char = _queue.consume();
+
+      // OSC terminates with BEL
+      if (char == Ascii.BEL) {
+        _osc.add(param.toString());
+        return true;
+      }
+
+      /// OSC terminates with ST
+      if (char == Ascii.ESC) {
+        if (_queue.isEmpty) {
+          return false;
+        }
+
+        if (_queue.consume() == Ascii.backslash) {
+          _osc.add(param.toString());
+        }
+
+        return true;
+      }
+
+      /// Parse next parameter
+      if (char == Ascii.semicolon) {
+        _osc.add(param.toString());
+        param.clear();
+        continue;
+      }
+
+      param.writeCharCode(char);
+    }
+  }
+}
+
+class _Csi {
+  _Csi({
+    required this.params,
+    required this.finalByte,
+    // required this.intermediates,
+  });
+
+  int? prefix;
+
+  List<int> params;
+
+  int finalByte;
+  // final List<int> intermediates;
+
+  @override
+  String toString() {
+    return params.join(';') + String.fromCharCode(finalByte);
+  }
+}
+
+/// Function that handles a sequence of characters that starts with an escape.
+/// Returns [true] if the sequence was processed, [false] if it was not.
+typedef _EscHandler = bool Function();
+
+typedef _SbcHandler = void Function();
+
+typedef _CsiHandler = void Function();

+ 131 - 0
lib/core/input/handler.dart

@@ -0,0 +1,131 @@
+import 'package:xterm/core/input/keys.dart';
+import 'package:xterm/core/input/keytab/keytab.dart';
+import 'package:xterm/utils/platform.dart';
+import 'package:xterm/core/state.dart';
+
+class TerminalInputEvent {
+  final TerminalKey key;
+
+  final bool shift;
+
+  final bool ctrl;
+
+  final bool alt;
+
+  final TerminalState state;
+
+  final bool altBuffer;
+
+  final TerminalTargetPlatform platform;
+
+  TerminalInputEvent({
+    required this.key,
+    required this.shift,
+    required this.ctrl,
+    required this.alt,
+    required this.state,
+    required this.altBuffer,
+    required this.platform,
+  });
+}
+
+abstract class TerminalInputHandler {
+  String? call(TerminalInputEvent event);
+}
+
+class CascadeInputHandler implements TerminalInputHandler {
+  final List<TerminalInputHandler> _handlers;
+
+  const CascadeInputHandler(this._handlers);
+
+  @override
+  String? call(TerminalInputEvent event) {
+    for (var handler in _handlers) {
+      final result = handler(event);
+      if (result != null) {
+        return result;
+      }
+    }
+    return null;
+  }
+}
+
+const defaultInputHandler = CascadeInputHandler([
+  KeytabInputHandler(),
+  CtrlInputHandler(),
+  AltInputHandler(),
+]);
+
+final _keytab = Keytab.defaultKeytab();
+
+class KeytabInputHandler implements TerminalInputHandler {
+  const KeytabInputHandler();
+
+  @override
+  String? call(TerminalInputEvent event) {
+    final action = _keytab.find(
+      event.key,
+      ctrl: event.ctrl,
+      alt: event.alt,
+      shift: event.shift,
+      newLineMode: event.state.lineFeedMode,
+      appCursorKeys: event.state.appKeypadMode,
+      appKeyPad: event.state.appKeypadMode,
+      appScreen: event.altBuffer,
+      macos: event.platform == TerminalTargetPlatform.macos,
+    );
+
+    if (action == null) {
+      return null;
+    }
+
+    return action.action.unescapedValue();
+  }
+}
+
+class CtrlInputHandler implements TerminalInputHandler {
+  const CtrlInputHandler();
+
+  @override
+  String? call(TerminalInputEvent event) {
+    if (!event.ctrl) {
+      return null;
+    }
+
+    final key = event.key;
+
+    if (key.index >= TerminalKey.keyA.index &&
+        key.index <= TerminalKey.keyZ.index) {
+      final input = key.index - TerminalKey.keyA.index + 1;
+      return String.fromCharCode(input);
+    }
+
+    return null;
+  }
+}
+
+class AltInputHandler implements TerminalInputHandler {
+  const AltInputHandler();
+
+  @override
+  String? call(TerminalInputEvent event) {
+    if (!event.alt) {
+      return null;
+    }
+
+    if (event.platform == TerminalTargetPlatform.macos) {
+      return null;
+    }
+
+    final key = event.key;
+
+    if (key.index >= TerminalKey.keyA.index &&
+        key.index <= TerminalKey.keyZ.index) {
+      final charCode = key.index - TerminalKey.keyA.index + 65;
+      final input = [0x1b, charCode];
+      return String.fromCharCodes(input);
+    }
+
+    return null;
+  }
+}

+ 0 - 0
lib/input/keys.dart → lib/core/input/keys.dart


+ 110 - 0
lib/core/input/keytab/keytab.dart

@@ -0,0 +1,110 @@
+import 'package:xterm/core/input/keys.dart';
+import 'package:xterm/core/input/keytab/keytab_default.dart';
+import 'package:xterm/core/input/keytab/keytab_parse.dart';
+import 'package:xterm/core/input/keytab/keytab_record.dart';
+import 'package:xterm/core/input/keytab/keytab_token.dart';
+
+class Keytab {
+  Keytab({
+    required this.name,
+    required this.records,
+  });
+
+  factory Keytab.parse(String source) {
+    final tokens = tokenize(source).toList();
+    final parser = KeytabParser()..addTokens(tokens);
+    return parser.result;
+  }
+
+  factory Keytab.defaultKeytab() {
+    return Keytab.parse(kDefaultKeytab);
+  }
+
+  final String? name;
+
+  final List<KeytabRecord> records;
+
+  KeytabRecord? find(
+    TerminalKey key, {
+    bool ctrl = false,
+    bool alt = false,
+    bool shift = false,
+    bool newLineMode = false,
+    bool appCursorKeys = false,
+    bool appKeyPad = false,
+    bool appScreen = false,
+    bool macos = false,
+    // bool meta,
+  }) {
+    for (var record in records) {
+      if (record.key != key) {
+        continue;
+      }
+
+      if (record.anyModifier == true) {
+        if (ctrl == false && alt == false && shift == false) {
+          continue;
+        }
+      } else if (record.anyModifier == false) {
+        if (ctrl != false || alt != false || shift != false) {
+          continue;
+        }
+      } else {
+        if (record.ctrl != null && record.ctrl != ctrl) {
+          continue;
+        }
+
+        if (record.shift != null && record.shift != shift) {
+          continue;
+        }
+
+        if (record.alt != null && record.alt != alt) {
+          continue;
+        }
+      }
+
+      if (record.newLine != null && record.newLine != newLineMode) {
+        continue;
+      }
+
+      if (record.appCursorKeys != null &&
+          record.appCursorKeys != appCursorKeys) {
+        continue;
+      }
+
+      if (record.appKeyPad != null && record.appKeyPad != appKeyPad) {
+        continue;
+      }
+
+      if (record.appScreen != null && record.appScreen != appScreen) {
+        continue;
+      }
+
+      if (record.macos != null && record.macos != macos) {
+        continue;
+      }
+
+      // TODO: support VT52
+      if (record.ansi == false) {
+        continue;
+      }
+
+      return record;
+    }
+
+    return null;
+  }
+
+  @override
+  String toString() {
+    final buffer = StringBuffer();
+
+    buffer.writeln('keyboard "$name"');
+
+    for (var record in records) {
+      buffer.writeln(record);
+    }
+
+    return buffer.toString();
+  }
+}

+ 2 - 2
lib/input/keytab/keytab_default.dart → lib/core/input/keytab/keytab_default.dart

@@ -1,5 +1,5 @@
-import 'package:xterm/input/keytab/keytab_parse.dart';
-import 'package:xterm/input/keytab/keytab_token.dart';
+import 'package:xterm/core/input/keytab/keytab_parse.dart';
+import 'package:xterm/core/input/keytab/keytab_token.dart';
 
 const kDefaultKeytab = r'''
 # [README.default.Keytab] Default Keyboard Table

+ 1 - 1
lib/input/keytab/keytab_escape.dart → lib/core/input/keytab/keytab_escape.dart

@@ -4,7 +4,7 @@ String keytabUnescape(String str) {
   str = str
       .replaceAll(r'\E', _esc)
       .replaceAll(r'\\', '\\')
-      .replaceAll(r'\"', '\"')
+      .replaceAll(r'\"', '"')
       .replaceAll(r'\t', '\t')
       .replaceAll(r'\r', '\r')
       .replaceAll(r'\n', '\n')

+ 5 - 5
lib/input/keytab/keytab_parse.dart → lib/core/input/keytab/keytab_parse.dart

@@ -1,7 +1,7 @@
-import 'package:xterm/input/keytab/keytab.dart';
-import 'package:xterm/input/keytab/keytab_record.dart';
-import 'package:xterm/input/keytab/keytab_token.dart';
-import 'package:xterm/input/keytab/qt_keyname.dart';
+import 'package:xterm/core/input/keytab/keytab.dart';
+import 'package:xterm/core/input/keytab/keytab_record.dart';
+import 'package:xterm/core/input/keytab/keytab_token.dart';
+import 'package:xterm/core/input/keytab/qt_keyname.dart';
 
 class ParseError {}
 
@@ -178,7 +178,7 @@ class KeytabParser {
       appCursorKeys: appCursorKeys,
       appKeyPad: appKeyPad,
       newLine: newLine,
-      mac: mac,
+      macos: mac,
     );
 
     _records.add(record);

+ 34 - 26
lib/input/keytab/keytab_record.dart → lib/core/input/keytab/keytab_record.dart

@@ -1,4 +1,5 @@
-import 'package:xterm/input/keys.dart';
+import 'package:xterm/core/input/keys.dart';
+import 'package:xterm/core/input/keytab/keytab_escape.dart';
 
 enum KeytabActionType {
   input,
@@ -9,8 +10,17 @@ class KeytabAction {
   KeytabAction(this.type, this.value);
 
   final KeytabActionType type;
+
   final String value;
 
+  String unescapedValue() {
+    if (type == KeytabActionType.input) {
+      return keytabUnescape(value);
+    } else {
+      return value;
+    }
+  }
+
   @override
   String toString() {
     switch (type) {
@@ -18,8 +28,6 @@ class KeytabAction {
         return '"$value"';
       case KeytabActionType.shortcut:
         return value;
-      default:
-        return '(no value)';
     }
   }
 }
@@ -39,7 +47,7 @@ class KeytabRecord {
     required this.appCursorKeys,
     required this.appKeyPad,
     required this.newLine,
-    required this.mac,
+    required this.macos,
   });
 
   String qtKeyName;
@@ -56,7 +64,7 @@ class KeytabRecord {
   bool? appCursorKeys;
   bool? appKeyPad;
   bool? newLine;
-  bool? mac;
+  bool? macos;
 
   @override
   String toString() {
@@ -64,63 +72,63 @@ class KeytabRecord {
     buffer.write('$qtKeyName ');
 
     if (alt != null) {
-      buffer.write(modeStatus(alt!, 'Alt'));
+      buffer.write(_toMode(alt!, 'Alt'));
     }
 
     if (ctrl != null) {
-      buffer.write(modeStatus(ctrl!, 'Control'));
+      buffer.write(_toMode(ctrl!, 'Control'));
     }
 
     if (shift != null) {
-      buffer.write(modeStatus(shift!, 'Shift'));
+      buffer.write(_toMode(shift!, 'Shift'));
     }
 
     if (anyModifier != null) {
-      buffer.write(modeStatus(anyModifier!, 'AnyMod'));
+      buffer.write(_toMode(anyModifier!, 'AnyMod'));
     }
 
     if (ansi != null) {
-      buffer.write(modeStatus(ansi!, 'Ansi'));
+      buffer.write(_toMode(ansi!, 'Ansi'));
     }
 
     if (appScreen != null) {
-      buffer.write(modeStatus(appScreen!, 'AppScreen'));
+      buffer.write(_toMode(appScreen!, 'AppScreen'));
     }
 
     if (keyPad != null) {
-      buffer.write(modeStatus(keyPad!, 'KeyPad'));
+      buffer.write(_toMode(keyPad!, 'KeyPad'));
     }
 
     if (appCursorKeys != null) {
-      buffer.write(modeStatus(appCursorKeys!, 'AppCuKeys'));
+      buffer.write(_toMode(appCursorKeys!, 'AppCuKeys'));
     }
 
     if (appKeyPad != null) {
-      buffer.write(modeStatus(appKeyPad!, 'AppKeyPad'));
+      buffer.write(_toMode(appKeyPad!, 'AppKeyPad'));
     }
 
     if (newLine != null) {
-      buffer.write(modeStatus(newLine!, 'NewLine'));
+      buffer.write(_toMode(newLine!, 'NewLine'));
     }
 
-    if (mac != null) {
-      buffer.write(modeStatus(mac!, 'Mac'));
+    if (macos != null) {
+      buffer.write(_toMode(macos!, 'Mac'));
     }
 
     buffer.write(' : $action');
 
     return buffer.toString();
   }
-}
 
-String modeStatus(bool status, String mode) {
-  if (status == true) {
-    return '+$mode';
-  }
+  static String _toMode(bool status, String mode) {
+    if (status == true) {
+      return '+$mode';
+    }
 
-  if (status == false) {
-    return '-$mode';
-  }
+    if (status == false) {
+      return '-$mode';
+    }
 
-  return '';
+    return '';
+  }
 }

+ 0 - 0
lib/input/keytab/keytab_token.dart → lib/core/input/keytab/keytab_token.dart


+ 1 - 1
lib/input/keytab/qt_keyname.dart → lib/core/input/keytab/qt_keyname.dart

@@ -1,4 +1,4 @@
-import 'package:xterm/input/keys.dart';
+import 'package:xterm/core/input/keys.dart';
 
 /// See: https://doc.qt.io/qt-5/qt.html#Key-enum
 const qtKeynameMap = <String, TerminalKey>{

+ 23 - 0
lib/core/mouse.dart

@@ -0,0 +1,23 @@
+/// https://terminalguide.namepad.de/mouse/
+enum MouseMode {
+  none,
+
+  clickOnly,
+
+  upDownScroll,
+
+  upDownScrollDrag,
+
+  upDownScrollMove,
+}
+
+/// https://terminalguide.namepad.de/mouse/
+enum MouseReportMode {
+  normal,
+
+  utf,
+
+  sgr,
+
+  urxvt,
+}

+ 157 - 0
lib/core/reflow.dart

@@ -0,0 +1,157 @@
+import 'package:xterm/core/buffer/line.dart';
+import 'package:xterm/utils/circular_list.dart';
+
+class _LineBuilder {
+  _LineBuilder([this._capacity = 80]) {
+    _result = BufferLine(_capacity);
+  }
+
+  final int _capacity;
+
+  late BufferLine _result;
+
+  int _length = 0;
+
+  int get length => _length;
+
+  bool get isEmpty => _length == 0;
+
+  bool get isNotEmpty => _length != 0;
+
+  void add(BufferLine src, int start, int length) {
+    _result.copyFrom(src, start, _length, length);
+    _length += length;
+  }
+
+  void setBuffer(BufferLine line, int length) {
+    _result = line;
+    _length = length;
+  }
+
+  BufferLine take({required bool wrapped}) {
+    final result = _result;
+    result.isWrapped = wrapped;
+    result.resize(_length);
+
+    _result = BufferLine(_capacity);
+    _length = 0;
+
+    return result;
+  }
+}
+
+class _LineReflow {
+  final int oldWidth;
+
+  final int newWidth;
+
+  _LineReflow(this.oldWidth, this.newWidth);
+
+  final _lines = <BufferLine>[];
+
+  late final _builder = _LineBuilder(newWidth);
+
+  void add(BufferLine line) {
+    final length = line.getTrimmedLength(oldWidth);
+
+    if (length == 0) {
+      _lines.add(line);
+      return;
+    }
+
+    if (_lines.isNotEmpty || _builder.isNotEmpty) {
+      _addRange(line, 0, length);
+      return;
+    }
+
+    if (newWidth >= oldWidth) {
+      _builder.setBuffer(line, length);
+    } else {
+      _lines.add(line);
+
+      if (line.getWidth(newWidth - 1) == 2) {
+        _addRange(line, newWidth - 1, length);
+      } else {
+        _addRange(line, newWidth, length);
+      }
+    }
+
+    line.resize(newWidth);
+
+    if (line.getWidth(newWidth - 1) == 2) {
+      line.resetCell(newWidth - 1);
+    }
+  }
+
+  void _addRange(BufferLine line, int start, int end) {
+    var cellsLeft = end - start;
+
+    while (cellsLeft > 0) {
+      final spaceLeft = newWidth - _builder.length;
+
+      var lineFilled = false;
+
+      var cellsToCopy = cellsLeft;
+
+      if (cellsToCopy >= spaceLeft) {
+        cellsToCopy = spaceLeft;
+        lineFilled = true;
+      }
+
+      // Avoid breaking wide characters
+      if (cellsToCopy == spaceLeft &&
+          line.getWidth(start + cellsToCopy - 1) == 2) {
+        cellsToCopy--;
+      }
+
+      _builder.add(line, start, cellsToCopy);
+
+      start += cellsToCopy;
+      cellsLeft -= cellsToCopy;
+
+      if (lineFilled) {
+        _lines.add(_builder.take(wrapped: _lines.isNotEmpty));
+      }
+    }
+  }
+
+  List<BufferLine> finish() {
+    if (_builder.isNotEmpty) {
+      _lines.add(_builder.take(wrapped: _lines.isNotEmpty));
+    }
+
+    return _lines;
+  }
+}
+
+List<BufferLine> reflow(
+  CircularList<BufferLine> lines,
+  int oldWidth,
+  int newWidth,
+) {
+  final result = <BufferLine>[];
+
+  for (var i = 0; i < lines.length; i++) {
+    final line = lines[i];
+
+    final reflow = _LineReflow(oldWidth, newWidth);
+
+    reflow.add(line);
+
+    for (var offset = i + 1; offset < lines.length; offset++) {
+      final nextLine = lines[offset];
+
+      if (!nextLine.isWrapped) {
+        break;
+      }
+
+      i++;
+
+      reflow.add(nextLine);
+    }
+
+    result.addAll(reflow.finish());
+  }
+
+  return result;
+}

+ 3 - 0
lib/core/snapshot.dart

@@ -0,0 +1,3 @@
+abstract class TerminalSnapshot {
+  void trimScrollback();
+}

+ 40 - 0
lib/core/state.dart

@@ -0,0 +1,40 @@
+import 'package:xterm/core/cursor.dart';
+import 'package:xterm/core/mouse.dart';
+
+abstract class TerminalState {
+  int get viewWidth;
+
+  int get viewHeight;
+
+  CursorStyle get cursor;
+
+  /* Modes */
+
+  bool get insertMode;
+
+  bool get lineFeedMode;
+
+  /* DEC Private modes */
+
+  bool get cursorKeysMode;
+
+  bool get reverseDisplayMode;
+
+  bool get originMode;
+
+  bool get autoWrapMode;
+
+  MouseMode get mouseMode;
+
+  bool get cursorBlinkMode;
+
+  bool get cursorVisibleMode;
+
+  bool get appKeypadMode;
+
+  bool get reportFocusMode;
+
+  bool get altBufferMouseScrollMode;
+
+  bool get bracketedPasteMode;
+}

+ 46 - 0
lib/core/tabs.dart

@@ -0,0 +1,46 @@
+import 'dart:math' show min;
+
+const _kMaxColumns = 1024;
+
+class TabStops {
+  final _stops = List<bool>.filled(_kMaxColumns, false);
+
+  int? find(int start, int end) {
+    if (start >= end) {
+      return null;
+    }
+    end = min(end, _stops.length);
+    for (var i = start; i < end; i++) {
+      if (_stops[i]) {
+        return i;
+      }
+    }
+    return null;
+  }
+
+  void setAt(int index) {
+    assert(index >= 0 && index < _kMaxColumns);
+    _stops[index] = true;
+  }
+
+  void clearAt(int index) {
+    assert(index >= 0 && index < _kMaxColumns);
+    _stops[index] = false;
+  }
+
+  void clearAll() {
+    _stops.fillRange(0, _kMaxColumns, false);
+  }
+
+  bool isSetAt(int index) {
+    return _stops[index];
+  }
+
+  void reset() {
+    clearAll();
+    const interval = 8;
+    for (var i = 0; i < _kMaxColumns; i += interval) {
+      _stops[i] = true;
+    }
+  }
+}

+ 783 - 0
lib/core/terminal.dart

@@ -0,0 +1,783 @@
+import 'dart:math' show max;
+
+import 'package:xterm/core/input/handler.dart';
+import 'package:xterm/core/input/keys.dart';
+import 'package:xterm/core/buffer/buffer.dart';
+import 'package:xterm/core/cursor.dart';
+import 'package:xterm/core/escape/emitter.dart';
+import 'package:xterm/core/escape/handler.dart';
+import 'package:xterm/core/escape/parser.dart';
+import 'package:xterm/core/buffer/line.dart';
+import 'package:xterm/core/mouse.dart';
+import 'package:xterm/utils/platform.dart';
+import 'package:xterm/core/state.dart';
+import 'package:xterm/core/tabs.dart';
+import 'package:xterm/utils/ascii.dart';
+import 'package:xterm/utils/circular_list.dart';
+import 'package:xterm/utils/observable.dart';
+
+class Terminal with Observable implements TerminalState, EscapeHandler {
+  final int maxLines;
+
+  void Function()? onBell;
+
+  void Function(String)? onTitleChange;
+
+  void Function(String)? onIconChange;
+
+  void Function(String)? onOutput;
+
+  void Function(int width, int height, int pixelWidth, int pixelHeight)?
+      onResize;
+
+  /// Flag to toggle os specific behaviors.
+  final TerminalTargetPlatform platform;
+
+  final TerminalInputHandler inputHandler;
+
+  Terminal({
+    this.maxLines = 1000,
+    this.onBell,
+    this.onTitleChange,
+    this.onIconChange,
+    this.onOutput,
+    this.onResize,
+    this.platform = TerminalTargetPlatform.unknown,
+    this.inputHandler = defaultInputHandler,
+  });
+
+  late final _parser = EscapeParser(this);
+
+  final _emitter = const EscapeEmitter();
+
+  late var _buffer = _mainBuffer;
+
+  late final _mainBuffer = Buffer(this, maxLines: maxLines, isAltBuffer: false);
+
+  late final _altBuffer = Buffer(this, maxLines: maxLines, isAltBuffer: true);
+
+  final tabStops = TabStops();
+
+  var _precedingCodepoint = 0;
+
+  /* TerminalState */
+
+  int _viewWidth = 80;
+
+  int _viewHeight = 24;
+
+  final _cursorStyle = CursorStyle();
+
+  bool _insertMode = false;
+
+  bool _lineFeedMode = false;
+
+  bool _cursorKeysMode = false;
+
+  bool _reverseDisplayMode = false;
+
+  bool _originMode = false;
+
+  bool _autoWrapMode = true;
+
+  MouseMode _mouseMode = MouseMode.none;
+
+  MouseReportMode _mouseReportMode = MouseReportMode.normal;
+
+  bool _cursorBlinkMode = false;
+
+  bool _cursorVisibleMode = true;
+
+  bool _appKeypadMode = false;
+
+  bool _reportFocusMode = false;
+
+  bool _altBufferMouseScrollMode = false;
+
+  bool _bracketedPasteMode = false;
+
+  /* State getters */
+
+  @override
+  int get viewWidth => _viewWidth;
+
+  @override
+  int get viewHeight => _viewHeight;
+
+  @override
+  CursorStyle get cursor => _cursorStyle;
+
+  @override
+  bool get insertMode => _insertMode;
+
+  @override
+  bool get lineFeedMode => _lineFeedMode;
+
+  @override
+  bool get cursorKeysMode => _cursorKeysMode;
+
+  @override
+  bool get reverseDisplayMode => _reverseDisplayMode;
+
+  @override
+  bool get originMode => _originMode;
+
+  @override
+  bool get autoWrapMode => _autoWrapMode;
+
+  @override
+  MouseMode get mouseMode => _mouseMode;
+
+  MouseReportMode get mouseReportMode => _mouseReportMode;
+
+  @override
+  bool get cursorBlinkMode => _cursorBlinkMode;
+
+  @override
+  bool get cursorVisibleMode => _cursorVisibleMode;
+
+  @override
+  bool get appKeypadMode => _appKeypadMode;
+
+  @override
+  bool get reportFocusMode => _reportFocusMode;
+
+  @override
+  bool get altBufferMouseScrollMode => _altBufferMouseScrollMode;
+
+  @override
+  bool get bracketedPasteMode => _bracketedPasteMode;
+
+  Buffer get buffer => _buffer;
+
+  Buffer get mainBuffer => _mainBuffer;
+
+  Buffer get altBuffer => _altBuffer;
+
+  bool get isUsingAltBuffer => _buffer == _altBuffer;
+
+  CircularList<BufferLine> get lines => _buffer.lines;
+
+  void write(String data) {
+    _parser.write(data);
+    notifyListeners();
+  }
+
+  bool keyInput(
+    TerminalKey key, {
+    bool shift = false,
+    bool alt = false,
+    bool ctrl = false,
+  }) {
+    final output = inputHandler(
+      TerminalInputEvent(
+        key: key,
+        shift: shift,
+        alt: alt,
+        ctrl: ctrl,
+        state: this,
+        altBuffer: isUsingAltBuffer,
+        platform: platform,
+      ),
+    );
+
+    if (output != null) {
+      onOutput?.call(output);
+      return true;
+    }
+
+    return false;
+  }
+
+  bool charInput(
+    int charCode, {
+    bool alt = false,
+    bool ctrl = false,
+  }) {
+    if (ctrl) {
+      // a(97) ~ z(122)
+      if (charCode >= Ascii.a && charCode <= Ascii.z) {
+        final output = charCode - Ascii.a + 1;
+        onOutput?.call(String.fromCharCode(output));
+        return true;
+      }
+
+      // [(91) ~ _(95)
+      if (charCode >= Ascii.openBracket && charCode <= Ascii.underscore) {
+        final output = charCode - Ascii.openBracket + 27;
+        onOutput?.call(String.fromCharCode(output));
+        return true;
+      }
+    }
+
+    if (alt && platform != TerminalTargetPlatform.macos) {
+      if (charCode >= Ascii.a && charCode <= Ascii.z) {
+        final code = charCode - Ascii.a + 65;
+        final input = [0x1b, code];
+        onOutput?.call(String.fromCharCodes(input));
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  void textInput(String text) {
+    onOutput?.call(text);
+  }
+
+  void paste(String text) {
+    if (_bracketedPasteMode) {
+      onOutput?.call(_emitter.bracketedPaste(text));
+    } else {
+      textInput(text);
+    }
+  }
+
+  /// Resize the terminal screen. [newWidth] and [newHeight] should be greater
+  /// than 0. Text reflow is currently not implemented and will be avaliable in
+  /// the future.
+  void resize(
+    int newWidth,
+    int newHeight, [
+    int? pixelWidth,
+    int? pixelHeight,
+  ]) {
+    newWidth = max(newWidth, 1);
+    newHeight = max(newHeight, 1);
+
+    onResize?.call(newWidth, newHeight, pixelWidth ?? 0, pixelHeight ?? 0);
+
+    //we need to resize both buffers so that they are ready when we switch between them
+    _altBuffer.resize(_viewWidth, _viewHeight, newWidth, newHeight);
+    _mainBuffer.resize(_viewWidth, _viewHeight, newWidth, newHeight);
+
+    _viewWidth = newWidth;
+    _viewHeight = newHeight;
+
+    if (buffer == _altBuffer) {
+      buffer.clearScrollback();
+    }
+
+    _altBuffer.resetVerticalMargins();
+    _mainBuffer.resetVerticalMargins();
+  }
+
+  /* Handlers */
+
+  @override
+  void writeChar(int char) {
+    _precedingCodepoint = char;
+    _buffer.writeChar(char);
+  }
+
+  /* SBC */
+
+  @override
+  void bell() {
+    onBell?.call();
+  }
+
+  @override
+  void backspaceReturn() {
+    _buffer.moveCursorX(-1);
+  }
+
+  @override
+  void tab() {
+    final nextStop = tabStops.find(_buffer.cursorX, _viewWidth);
+
+    if (nextStop != null) {
+      _buffer.setCursorX(nextStop);
+    } else {
+      _buffer.setCursorX(_viewWidth);
+      _buffer.cursorGoForward(); // Enter pending-wrap state
+    }
+  }
+
+  @override
+  void lineFeed() {
+    _buffer.lineFeed();
+  }
+
+  @override
+  void carriageReturn() {
+    _buffer.setCursorX(0);
+  }
+
+  @override
+  void shiftOut() {
+    _buffer.charset.use(1);
+  }
+
+  @override
+  void shiftIn() {
+    _buffer.charset.use(0);
+  }
+
+  @override
+  void unknownSBC(int char) {
+    // no-op
+  }
+
+  /* ANSI sequence */
+
+  @override
+  void saveCursor() {
+    _buffer.saveCursor();
+  }
+
+  @override
+  void restoreCursor() {
+    _buffer.restoreCursor();
+  }
+
+  @override
+  void index() {
+    _buffer.index();
+  }
+
+  @override
+  void nextLine() {
+    _buffer.index();
+    _buffer.setCursorX(0);
+  }
+
+  @override
+  void setTapStop() {
+    tabStops.isSetAt(_buffer.cursorX);
+  }
+
+  @override
+  void reverseIndex() {
+    _buffer.reverseIndex();
+  }
+
+  @override
+  void designateCharset(int charset) {
+    _buffer.charset.use(charset);
+  }
+
+  @override
+  void unkownEscape(int char) {
+    // no-op
+  }
+
+  /* CSI */
+
+  @override
+  void repeatPreviousCharacter(int count) {
+    if (_precedingCodepoint == 0) {
+      return;
+    }
+
+    for (var i = 0; i < count; i++) {
+      _buffer.writeChar(_precedingCodepoint);
+    }
+  }
+
+  @override
+  void setCursor(int x, int y) {
+    _buffer.setCursor(x, y);
+  }
+
+  @override
+  void setCursorX(int x) {
+    _buffer.setCursorX(x);
+  }
+
+  @override
+  void setCursorY(int y) {
+    _buffer.setCursorY(y);
+  }
+
+  @override
+  void moveCursorX(int offset) {
+    _buffer.moveCursorX(offset);
+  }
+
+  @override
+  void moveCursorY(int n) {
+    _buffer.moveCursorY(n);
+  }
+
+  @override
+  void clearTabStopUnderCursor() {
+    tabStops.clearAt(_buffer.cursorX);
+  }
+
+  @override
+  void clearAllTabStops() {
+    tabStops.clearAll();
+  }
+
+  @override
+  void sendPrimaryDeviceAttributes() {
+    onOutput?.call(_emitter.primaryDeviceAttributes());
+  }
+
+  @override
+  void sendSecondaryDeviceAttributes() {
+    onOutput?.call(_emitter.secondaryDeviceAttributes());
+  }
+
+  @override
+  void sendTertiaryDeviceAttributes() {
+    onOutput?.call(_emitter.tertiaryDeviceAttributes());
+  }
+
+  @override
+  void sendOperatingStatus() {
+    onOutput?.call(_emitter.operatingStatus());
+  }
+
+  @override
+  void sendCursorPosition() {
+    onOutput?.call(_emitter.cursorPosition(_buffer.cursorX, _buffer.cursorY));
+  }
+
+  @override
+  void setMargins(int top, [int? bottom]) {
+    _buffer.setVerticalMargins(top, bottom ?? viewHeight - 1);
+  }
+
+  @override
+  void cursorNextLine(int amount) {
+    _buffer.moveCursorY(amount);
+    _buffer.setCursorX(0);
+  }
+
+  @override
+  void cursorPrecedingLine(int amount) {
+    _buffer.moveCursorY(-amount);
+    _buffer.setCursorX(0);
+  }
+
+  @override
+  void eraseDisplayBelow() {
+    _buffer.eraseDisplayFromCursor();
+  }
+
+  @override
+  void eraseDisplayAbove() {
+    _buffer.eraseDisplayToCursor();
+  }
+
+  @override
+  void eraseDisplay() {
+    _buffer.eraseDisplay();
+  }
+
+  @override
+  void eraseScrollbackOnly() {
+    _buffer.clearScrollback();
+  }
+
+  @override
+  void eraseLineRight() {
+    _buffer.eraseLineFromCursor();
+  }
+
+  @override
+  void eraseLineLeft() {
+    _buffer.eraseLineToCursor();
+  }
+
+  @override
+  void eraseLine() {
+    _buffer.eraseLine();
+  }
+
+  @override
+  void insertLines(int amount) {
+    _buffer.insertLines(amount);
+  }
+
+  @override
+  void deleteLines(int amount) {
+    _buffer.deleteLines(amount);
+  }
+
+  @override
+  void deleteChars(int amount) {
+    _buffer.deleteChars(amount);
+  }
+
+  @override
+  void scrollUp(int amount) {
+    _buffer.scrollUp(amount);
+  }
+
+  @override
+  void scrollDown(int amount) {
+    _buffer.scrollDown(amount);
+  }
+
+  @override
+  void eraseChars(int amount) {
+    _buffer.eraseChars(amount);
+  }
+
+  @override
+  void insertBlankChars(int amount) {
+    _buffer.insertBlankChars(amount);
+  }
+
+  @override
+  void unknownCSI(int finalByte) {
+    // no-op
+  }
+
+  /* Modes */
+
+  @override
+  void setInsertMode(bool enabled) {
+    _insertMode = enabled;
+  }
+
+  @override
+  void setLineFeedMode(bool enabled) {
+    _lineFeedMode = enabled;
+  }
+
+  @override
+  void setUnknownMode(int mode, bool enabled) {
+    // no-op
+  }
+
+  /* DEC Private modes */
+
+  @override
+  void setCursorKeysMode(bool enabled) {
+    _cursorKeysMode = enabled;
+  }
+
+  @override
+  void setReverseDisplayMode(bool enabled) {
+    _reverseDisplayMode = enabled;
+  }
+
+  @override
+  void setOriginMode(bool enabled) {
+    _originMode = enabled;
+  }
+
+  @override
+  void setColumnMode(bool enabled) {
+    // no-op
+  }
+
+  @override
+  void setAutoWrapMode(bool enabled) {
+    _autoWrapMode = enabled;
+  }
+
+  @override
+  void setMouseMode(MouseMode mode) {
+    _mouseMode = mode;
+  }
+
+  @override
+  void setCursorBlinkMode(bool enabled) {
+    _cursorBlinkMode = enabled;
+  }
+
+  @override
+  void setCursorVisibleMode(bool enabled) {
+    _cursorVisibleMode = enabled;
+  }
+
+  @override
+  void useAltBuffer() {
+    _buffer = _altBuffer;
+  }
+
+  @override
+  void useMainBuffer() {
+    _buffer = _mainBuffer;
+  }
+
+  @override
+  void clearAltBuffer() {
+    _altBuffer.clear();
+  }
+
+  @override
+  void setAppKeypadMode(bool enabled) {
+    _appKeypadMode = enabled;
+  }
+
+  @override
+  void setReportFocusMode(bool enabled) {
+    _reportFocusMode = enabled;
+  }
+
+  @override
+  void setMouseReportMode(MouseReportMode mode) {
+    _mouseReportMode = mode;
+  }
+
+  @override
+  void setAltBufferMouseScrollMode(bool enabled) {
+    _altBufferMouseScrollMode = enabled;
+  }
+
+  @override
+  void setBracketedPasteMode(bool enabled) {
+    _bracketedPasteMode = enabled;
+  }
+
+  @override
+  void setUnknownDecMode(int mode, bool enabled) {
+    // no-op
+  }
+
+  /* Select Graphic Rendition (SGR) */
+
+  @override
+  void resetCursorStyle() {
+    _cursorStyle.reset();
+  }
+
+  @override
+  void setCursorBold() {
+    _cursorStyle.setBold();
+  }
+
+  @override
+  void setCursorFaint() {
+    _cursorStyle.setFaint();
+  }
+
+  @override
+  void setCursorItalic() {
+    _cursorStyle.setItalic();
+  }
+
+  @override
+  void setCursorUnderline() {
+    _cursorStyle.setUnderline();
+  }
+
+  @override
+  void setCursorBlink() {
+    _cursorStyle.setBlink();
+  }
+
+  @override
+  void setCursorInverse() {
+    _cursorStyle.setInverse();
+  }
+
+  @override
+  void setCursorInvisible() {
+    _cursorStyle.setInvisible();
+  }
+
+  @override
+  void setCursorStrikethrough() {
+    _cursorStyle.setStrikethrough();
+  }
+
+  @override
+  void unsetCursorBold() {
+    _cursorStyle.unsetBold();
+  }
+
+  @override
+  void unsetCursorFaint() {
+    _cursorStyle.unsetFaint();
+  }
+
+  @override
+  void unsetCursorItalic() {
+    _cursorStyle.unsetItalic();
+  }
+
+  @override
+  void unsetCursorUnderline() {
+    _cursorStyle.unsetUnderline();
+  }
+
+  @override
+  void unsetCursorBlink() {
+    _cursorStyle.unsetBlink();
+  }
+
+  @override
+  void unsetCursorInverse() {
+    _cursorStyle.unsetInverse();
+  }
+
+  @override
+  void unsetCursorInvisible() {
+    _cursorStyle.unsetInvisible();
+  }
+
+  @override
+  void unsetCursorStrikethrough() {
+    _cursorStyle.unsetStrikethrough();
+  }
+
+  @override
+  void setForegroundColor16(int color) {
+    _cursorStyle.setForegroundColor16(color);
+  }
+
+  @override
+  void setForegroundColor256(int index) {
+    _cursorStyle.setForegroundColor256(index);
+  }
+
+  @override
+  void setForegroundColorRgb(int r, int g, int b) {
+    _cursorStyle.setForegroundColorRgb(r, g, b);
+  }
+
+  @override
+  void resetForeground() {
+    _cursorStyle.resetForegroundColor();
+  }
+
+  @override
+  void setBackgroundColor16(int color) {
+    _cursorStyle.setBackgroundColor16(color);
+  }
+
+  @override
+  void setBackgroundColor256(int index) {
+    _cursorStyle.setBackgroundColor256(index);
+  }
+
+  @override
+  void setBackgroundColorRgb(int r, int g, int b) {
+    _cursorStyle.setBackgroundColorRgb(r, g, b);
+  }
+
+  @override
+  void resetBackground() {
+    _cursorStyle.resetBackgroundColor();
+  }
+
+  @override
+  void unsupportedStyle(int param) {
+    // no-op
+  }
+
+  /* OSC */
+
+  @override
+  void setTitle(String name) {
+    onTitleChange?.call(name);
+  }
+
+  @override
+  void setIconName(String name) {
+    onIconChange?.call(name);
+  }
+
+  @override
+  void unknownOSC(String ps) {
+    // no-op
+  }
+}

+ 0 - 1
lib/flutter.dart

@@ -1 +0,0 @@
-export 'frontend/terminal_view.dart';

+ 0 - 30
lib/frontend/char_size.dart

@@ -1,30 +0,0 @@
-class CellSize {
-  CellSize({
-    required this.charWidth,
-    required this.charHeight,
-    required this.letterSpacing,
-    required this.lineSpacing,
-    required this.cellWidth,
-    required this.cellHeight,
-  });
-
-  final double charWidth;
-  final double charHeight;
-  final double cellWidth;
-  final double cellHeight;
-  final double letterSpacing;
-  final double lineSpacing;
-
-  @override
-  String toString() {
-    final data = {
-      'charWidth': charWidth,
-      'charHeight': charHeight,
-      'letterSpacing': letterSpacing,
-      'lineSpacing': lineSpacing,
-      'cellWidth': cellWidth,
-      'cellHeight': cellHeight,
-    };
-    return 'CellSize$data';
-  }
-}

+ 0 - 23
lib/frontend/helpers.dart

@@ -1,23 +0,0 @@
-import 'package:flutter/widgets.dart';
-
-Size textSize(Text text) {
-  var span = text.textSpan ?? TextSpan(text: text.data, style: text.style);
-
-  var tp = TextPainter(
-    text: span,
-    textAlign: text.textAlign ?? TextAlign.start,
-    textDirection: text.textDirection ?? TextDirection.ltr,
-    textScaleFactor: text.textScaleFactor ?? 1,
-    maxLines: text.maxLines,
-    locale: text.locale,
-    strutStyle: text.strutStyle,
-  );
-
-  tp.layout();
-
-  return Size(tp.width, tp.height);
-}
-
-bool isMonospace(List<String> fontFamily) {
-  return true; // TBD
-}

+ 0 - 17
lib/frontend/input_behavior.dart

@@ -1,17 +0,0 @@
-import 'package:flutter/services.dart';
-import 'package:xterm/xterm.dart';
-
-abstract class InputBehavior {
-  const InputBehavior();
-
-  bool get acceptKeyStroke;
-
-  TextEditingValue get initEditingState;
-
-  void onKeyStroke(RawKeyEvent event, TerminalUiInteraction terminal);
-
-  TextEditingValue? onTextEdit(
-      TextEditingValue value, TerminalUiInteraction terminal);
-
-  void onAction(TextInputAction action, TerminalUiInteraction terminal);
-}

+ 0 - 84
lib/frontend/input_behavior_default.dart

@@ -1,84 +0,0 @@
-import 'package:flutter/services.dart';
-import 'package:xterm/frontend/input_behavior.dart';
-import 'package:xterm/frontend/input_map.dart';
-import 'package:xterm/xterm.dart';
-
-class InputBehaviorDefault extends InputBehavior {
-  InputBehaviorDefault();
-
-  @override
-  bool get acceptKeyStroke => true;
-
-  @override
-  TextEditingValue get initEditingState => TextEditingValue.empty;
-
-  @override
-  void onKeyStroke(RawKeyEvent event, TerminalUiInteraction terminal) {
-    if (event is! RawKeyDownEvent) {
-      return;
-    }
-
-    final key = inputMap(event.logicalKey);
-
-    if (key != null) {
-      terminal.keyInput(
-        key,
-        ctrl: event.isControlPressed,
-        alt: event.isAltPressed,
-        shift: event.isShiftPressed,
-        mac: terminal.platform.useMacInputBehavior,
-        character: event.character,
-      );
-    }
-  }
-
-  String? _composingString;
-
-  TextEditingValue? _lastEditingState;
-
-  @override
-  TextEditingValue? onTextEdit(
-      TextEditingValue value, TerminalUiInteraction terminal) {
-    var inputText = value.text;
-    // we just want to detect if a composing is going on and notify the terminal
-    // about it
-    if (value.composing.start != value.composing.end) {
-      _composingString = inputText;
-      terminal.updateComposingString(_composingString!);
-      _lastEditingState = value;
-      return null;
-    }
-    //when we reach this point the composing state is over
-    if (_composingString != null) {
-      _composingString = null;
-      terminal.updateComposingString('');
-    }
-
-    //this is a hack to bypass some race condition in the input system
-    //we just take the last rune if there are more than one as it sometimes
-    //happens that the last value is still part of the new value
-
-    if (_lastEditingState?.text.isNotEmpty == true) {
-      if (inputText.length > _lastEditingState!.text.length) {
-        inputText = inputText.substring(_lastEditingState!.text.length);
-      }
-    }
-
-    if (inputText.isNotEmpty) {
-      terminal.raiseOnInput(inputText);
-    }
-
-    _lastEditingState = value;
-
-    if (value == TextEditingValue.empty || inputText == '') {
-      return null;
-    } else {
-      return TextEditingValue.empty;
-    }
-  }
-
-  @override
-  void onAction(TextInputAction action, TerminalUiInteraction terminal) {
-    //
-  }
-}

+ 0 - 5
lib/frontend/input_behavior_desktop.dart

@@ -1,5 +0,0 @@
-import 'package:xterm/frontend/input_behavior_default.dart';
-
-class InputBehaviorDesktop extends InputBehaviorDefault {
-  InputBehaviorDesktop();
-}

+ 0 - 43
lib/frontend/input_behavior_mobile.dart

@@ -1,43 +0,0 @@
-import 'package:flutter/services.dart';
-import 'package:xterm/frontend/input_behavior_default.dart';
-import 'package:xterm/input/keys.dart';
-import 'package:xterm/xterm.dart';
-
-class InputBehaviorMobile extends InputBehaviorDefault {
-  InputBehaviorMobile();
-
-  final acceptKeyStroke = false;
-
-  final initEditingState = const TextEditingValue(
-    text: '  ',
-    selection: TextSelection.collapsed(offset: 1),
-  );
-
-  TextEditingValue onTextEdit(
-      TextEditingValue value, TerminalUiInteraction terminal) {
-    if (value.text.length > initEditingState.text.length) {
-      terminal.raiseOnInput(value.text.substring(1, value.text.length - 1));
-    } else if (value.text.length < initEditingState.text.length) {
-      terminal.keyInput(TerminalKey.backspace);
-    } else {
-      if (value.selection.baseOffset < 1) {
-        terminal.keyInput(TerminalKey.arrowLeft);
-      } else if (value.selection.baseOffset > 1) {
-        terminal.keyInput(TerminalKey.arrowRight);
-      }
-    }
-
-    return initEditingState;
-  }
-
-  void onAction(TextInputAction action, TerminalUiInteraction terminal) {
-    print('action $action');
-    switch (action) {
-      case TextInputAction.done:
-        terminal.keyInput(TerminalKey.enter);
-        break;
-      default:
-        print('unknown action $action');
-    }
-  }
-}

+ 0 - 18
lib/frontend/input_behaviors.dart

@@ -1,18 +0,0 @@
-import 'package:platform_info/platform_info.dart';
-import 'package:xterm/frontend/input_behavior.dart';
-import 'package:xterm/frontend/input_behavior_desktop.dart';
-import 'package:xterm/frontend/input_behavior_mobile.dart';
-
-class InputBehaviors {
-  static final desktop = InputBehaviorDesktop();
-
-  static final mobile = InputBehaviorMobile();
-
-  static InputBehavior get platform {
-    if (Platform.I.isMobile) {
-      return mobile;
-    }
-
-    return desktop;
-  }
-}

+ 0 - 268
lib/frontend/input_listener.dart

@@ -1,268 +0,0 @@
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/scheduler.dart';
-import 'package:flutter/services.dart';
-
-typedef KeyStrokeHandler = void Function(RawKeyEvent);
-typedef InputHandler = TextEditingValue? Function(TextEditingValue);
-typedef ActionHandler = void Function(TextInputAction);
-typedef FocusHandler = void Function(bool);
-
-abstract class InputListenerController {
-  void requestKeyboard();
-  void setCaretRect(Rect rect);
-}
-
-class InputListener extends StatefulWidget {
-  InputListener({
-    required this.child,
-    required this.onKeyStroke,
-    required this.onTextInput,
-    required this.onAction,
-    required this.focusNode,
-    this.onFocus,
-    this.autofocus = false,
-    this.listenKeyStroke = true,
-    this.readOnly = false,
-    this.initEditingState = TextEditingValue.empty,
-    this.inputType = TextInputType.text,
-    this.enableSuggestions = false,
-    this.inputAction = TextInputAction.done,
-    this.keyboardAppearance = Brightness.light,
-    this.autocorrect = false,
-  });
-
-  final Widget child;
-  final InputHandler onTextInput;
-  final KeyStrokeHandler onKeyStroke;
-  final ActionHandler onAction;
-  final FocusHandler? onFocus;
-  final bool autofocus;
-  final FocusNode focusNode;
-  final bool listenKeyStroke;
-  final bool readOnly;
-  final TextEditingValue initEditingState;
-  final TextInputType inputType;
-  final bool enableSuggestions;
-  final TextInputAction inputAction;
-  final Brightness keyboardAppearance;
-  final bool autocorrect;
-
-  @override
-  InputListenerState createState() => InputListenerState();
-
-  static InputListenerController? of(BuildContext context) {
-    return context.findAncestorStateOfType<InputListenerState>();
-  }
-}
-
-class InputListenerState extends State<InputListener>
-    implements InputListenerController {
-  TextInputConnection? _conn;
-  FocusAttachment? _focusAttachment;
-  bool _didAutoFocus = false;
-
-  @override
-  void initState() {
-    _focusAttachment = widget.focusNode.attach(context);
-    widget.focusNode.addListener(onFocusChange);
-    super.initState();
-  }
-
-  @override
-  void didChangeDependencies() {
-    super.didChangeDependencies();
-
-    if (!_didAutoFocus && widget.autofocus) {
-      _didAutoFocus = true;
-      SchedulerBinding.instance.addPostFrameCallback((_) {
-        if (mounted) {
-          FocusScope.of(context).autofocus(widget.focusNode);
-        }
-      });
-    }
-  }
-
-  bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly;
-
-  bool get _hasInputConnection => _conn != null && _conn!.attached;
-
-  @override
-  void didUpdateWidget(InputListener oldWidget) {
-    super.didUpdateWidget(oldWidget);
-
-    if (widget.focusNode != oldWidget.focusNode) {
-      oldWidget.focusNode.removeListener(onFocusChange);
-      _focusAttachment?.detach();
-      _focusAttachment = widget.focusNode.attach(context);
-      widget.focusNode.addListener(onFocusChange);
-    }
-
-    if (!_shouldCreateInputConnection) {
-      closeInputConnectionIfNeeded();
-    } else {
-      if (oldWidget.readOnly && widget.focusNode.hasFocus) {
-        openInputConnection();
-      }
-    }
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _focusAttachment?.detach();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    _focusAttachment?.reparent();
-
-    if (widget.listenKeyStroke) {
-      return RawKeyboardListener(
-        focusNode: widget.focusNode,
-        onKey: widget.onKeyStroke,
-        autofocus: widget.autofocus,
-        child: widget.child,
-      );
-    }
-
-    return Focus(
-      focusNode: widget.focusNode,
-      autofocus: widget.autofocus,
-      includeSemantics: false,
-      child: widget.child,
-    );
-  }
-
-  @override
-  void requestKeyboard() {
-    if (widget.focusNode.hasFocus) {
-      openInputConnection();
-    } else {
-      widget.focusNode.requestFocus();
-    }
-  }
-
-  @override
-  void setCaretRect(Rect rect) {
-    _conn?.setCaretRect(rect);
-  }
-
-  void onFocusChange() {
-    if (widget.onFocus != null) {
-      widget.onFocus?.call(widget.focusNode.hasFocus);
-    }
-
-    openOrCloseInputConnectionIfNeeded();
-  }
-
-  void openOrCloseInputConnectionIfNeeded() {
-    if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) {
-      openInputConnection();
-    } else if (!widget.focusNode.hasFocus) {
-      closeInputConnectionIfNeeded();
-    }
-  }
-
-  void openInputConnection() {
-    if (!_shouldCreateInputConnection) {
-      return;
-    }
-
-    if (_hasInputConnection) {
-      _conn!.show();
-    } else {
-      final config = TextInputConfiguration(
-        inputType: widget.inputType,
-        enableSuggestions: widget.enableSuggestions,
-        inputAction: widget.inputAction,
-        keyboardAppearance: widget.keyboardAppearance,
-        autocorrect: widget.autocorrect,
-      );
-      final client = TerminalTextInputClient(onInput, onAction);
-      _conn = TextInput.attach(client, config);
-
-      _conn!.show();
-
-      final dx = 0.0;
-      final dy = 0.0;
-      _conn!.setEditableSizeAndTransform(
-        Size(10, 10),
-        Matrix4.translationValues(dx, dy, 0.0),
-      );
-
-      _conn!.setEditingState(widget.initEditingState);
-    }
-  }
-
-  void closeInputConnectionIfNeeded() {
-    if (_conn != null && _conn!.attached) {
-      _conn!.close();
-      _conn = null;
-    }
-  }
-
-  void onInput(TextEditingValue value) {
-    final newValue = widget.onTextInput(value);
-
-    if (newValue != null) {
-      _conn?.setEditingState(newValue);
-    } else {
-      _conn?.setEditingState(TextEditingValue.empty);
-    }
-  }
-
-  void onAction(TextInputAction action) {
-    widget.onAction(action);
-  }
-}
-
-class TerminalTextInputClient extends TextInputClient {
-  TerminalTextInputClient(this.onInput, this.onAction);
-
-  final void Function(TextEditingValue) onInput;
-  final ActionHandler onAction;
-
-  TextEditingValue? _savedValue;
-
-  TextEditingValue? get currentTextEditingValue {
-    return _savedValue;
-  }
-
-  AutofillScope? get currentAutofillScope {
-    return null;
-  }
-
-  void updateEditingValue(TextEditingValue value) {
-    // print('updateEditingValue $value');
-
-    if (value.text != '') {
-      onInput(value);
-    }
-
-    _savedValue = value;
-    // print('updateEditingValue $value');
-  }
-
-  void performAction(TextInputAction action) {
-    // print('performAction $action');
-    onAction(action);
-  }
-
-  void updateFloatingCursor(RawFloatingCursorPoint point) {
-    // print('updateFloatingCursor');
-  }
-
-  void showAutocorrectionPromptRect(int start, int end) {
-    // print('showAutocorrectionPromptRect');
-  }
-
-  void connectionClosed() {
-    // print('connectionClosed');
-  }
-
-  @override
-  void performPrivateCommand(String action, Map<String, dynamic> data) {
-    // print('performPrivateCommand $action');
-  }
-}

+ 0 - 28
lib/frontend/mouse_listener.dart

@@ -1,28 +0,0 @@
-import 'package:flutter/gestures.dart';
-import 'package:flutter/widgets.dart';
-
-typedef ScrollHandler = void Function(Offset);
-
-class MouseListener extends StatelessWidget {
-  MouseListener({
-    required this.child,
-    required this.onScroll,
-  });
-
-  final Widget child;
-  final ScrollHandler onScroll;
-
-  @override
-  Widget build(BuildContext context) {
-    return Listener(
-      child: child,
-      onPointerSignal: onPointerSignal,
-    );
-  }
-
-  void onPointerSignal(PointerSignalEvent event) {
-    if (event is PointerScrollEvent) {
-      onScroll(event.scrollDelta);
-    }
-  }
-}

+ 0 - 78
lib/frontend/oscillator.dart

@@ -1,78 +0,0 @@
-import 'dart:async';
-
-import 'package:xterm/util/observable.dart';
-
-class Oscillator with Observable {
-  Oscillator(this.duration);
-
-  Oscillator.ms(int ms) : duration = Duration(milliseconds: ms);
-
-  final Duration duration;
-
-  var _value = true;
-  Timer? _timer;
-  var _shouldRun = false;
-
-  @override
-  void addListener(listener) {
-    super.addListener(listener);
-    resume();
-  }
-
-  @override
-  void removeListener(listener) {
-    super.removeListener(listener);
-    if (listeners.isEmpty) {
-      pause();
-    }
-  }
-
-  void _onOscillation(_) {
-    _value = !_value;
-    notifyListeners();
-  }
-
-  bool get value {
-    return _value;
-  }
-
-  void restart() {
-    stop();
-    start();
-  }
-
-  void start() {
-    _value = true;
-    _shouldRun = true;
-    // only start right away when anyone is listening.
-    // the moment a listener gets registered the Oscillator will start
-    if (listeners.isNotEmpty) {
-      _startInternal();
-    }
-  }
-
-  void _startInternal() {
-    if (_timer != null) return;
-    _timer = Timer.periodic(duration, _onOscillation);
-  }
-
-  void pause() {
-    _stopInternal();
-  }
-
-  void resume() {
-    if (_shouldRun) {
-      _startInternal();
-    }
-  }
-
-  void stop() {
-    _shouldRun = false;
-    _stopInternal();
-  }
-
-  void _stopInternal() {
-    _timer?.cancel();
-    _timer = null;
-  }
-}

+ 0 - 423
lib/frontend/terminal_painters.dart

@@ -1,423 +0,0 @@
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-import 'package:xterm/buffer/cell_flags.dart';
-import 'package:xterm/buffer/line/line.dart';
-import 'package:xterm/mouse/position.dart';
-import 'package:xterm/terminal/terminal_search.dart';
-import 'package:xterm/terminal/terminal_ui_interaction.dart';
-import 'package:xterm/theme/terminal_style.dart';
-import 'package:xterm/util/bit_flags.dart';
-
-import 'cache.dart';
-import 'char_size.dart';
-
-class TerminalPainter extends CustomPainter {
-  TerminalPainter({
-    required this.terminal,
-    required this.style,
-    required this.charSize,
-    required this.textLayoutCache,
-  });
-
-  final TerminalUiInteraction terminal;
-  final TerminalStyle style;
-  final CellSize charSize;
-  final TextLayoutCache textLayoutCache;
-
-  @override
-  void paint(Canvas canvas, Size size) {
-    _paintBackground(canvas);
-
-    _paintText(canvas);
-
-    _paintUserSearchResult(canvas, size);
-
-    _paintSelection(canvas);
-  }
-
-  void _paintBackground(Canvas canvas) {
-    final lines = terminal.getVisibleLines();
-
-    for (var row = 0; row < lines.length; row++) {
-      final line = lines[row];
-      final offsetY = row * charSize.cellHeight;
-      final cellCount = terminal.terminalWidth;
-
-      for (var col = 0; col < cellCount; col++) {
-        final cellWidth = line.cellGetWidth(col);
-        if (cellWidth == 0) {
-          continue;
-        }
-
-        final cellFgColor = line.cellGetFgColor(col);
-        final cellBgColor = line.cellGetBgColor(col);
-        final effectBgColor = line.cellHasFlag(col, CellFlags.inverse)
-            ? cellFgColor
-            : cellBgColor;
-
-        if (effectBgColor == 0x00) {
-          continue;
-        }
-
-        // when a program reports black as background then it "really" means transparent
-        if (effectBgColor == 0xFF000000) {
-          continue;
-        }
-
-        final offsetX = col * charSize.cellWidth;
-        final effectWidth = charSize.cellWidth * cellWidth + 1;
-        final effectHeight = charSize.cellHeight + 1;
-
-        // background color is already painted with opacity by the Container of
-        // TerminalPainter so wo don't need to fallback to
-        // terminal.theme.background here.
-
-        final paint = Paint()..color = Color(effectBgColor);
-        canvas.drawRect(
-          Rect.fromLTWH(offsetX, offsetY, effectWidth, effectHeight),
-          paint,
-        );
-      }
-    }
-  }
-
-  void _paintUserSearchResult(Canvas canvas, Size size) {
-    final searchResult = terminal.userSearchResult;
-
-    //when there is no ongoing user search then directly return
-    if (!terminal.isUserSearchActive) {
-      return;
-    }
-
-    //make everything dim so that the search result can be seen better
-    final dimPaint = Paint()
-      ..color = Color(terminal.theme.background).withAlpha(128)
-      ..style = PaintingStyle.fill;
-
-    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), dimPaint);
-
-    for (int i = 1; i <= searchResult.allHits.length; i++) {
-      _paintSearchHit(canvas, searchResult.allHits[i - 1], i);
-    }
-  }
-
-  void _paintSearchHit(Canvas canvas, TerminalSearchHit hit, int hitNum) {
-    //check if the hit is visible
-    if (hit.startLineIndex >=
-            terminal.scrollOffsetFromTop + terminal.terminalHeight ||
-        hit.endLineIndex < terminal.scrollOffsetFromTop) {
-      return;
-    }
-
-    final paint = Paint()
-      ..color = Color(terminal.currentSearchHit == hitNum
-          ? terminal.theme.searchHitBackgroundCurrent
-          : terminal.theme.searchHitBackground)
-      ..style = PaintingStyle.fill;
-
-    if (hit.startLineIndex == hit.endLineIndex) {
-      final double y =
-          (hit.startLineIndex.toDouble() - terminal.scrollOffsetFromTop) *
-              charSize.cellHeight;
-      final startX = charSize.cellWidth * hit.startIndex;
-      final endX = charSize.cellWidth * hit.endIndex;
-
-      canvas.drawRect(
-          Rect.fromLTRB(startX, y, endX, y + charSize.cellHeight), paint);
-    } else {
-      //draw first row: start - line end
-      final double yFirstRow =
-          (hit.startLineIndex.toDouble() - terminal.scrollOffsetFromTop) *
-              charSize.cellHeight;
-      final startXFirstRow = charSize.cellWidth * hit.startIndex;
-      final endXFirstRow = charSize.cellWidth * terminal.terminalWidth;
-      canvas.drawRect(
-          Rect.fromLTRB(startXFirstRow, yFirstRow, endXFirstRow,
-              yFirstRow + charSize.cellHeight),
-          paint);
-      //draw middle rows
-      final middleRowCount = hit.endLineIndex - hit.startLineIndex - 1;
-      if (middleRowCount > 0) {
-        final startYMiddleRows =
-            (hit.startLineIndex + 1 - terminal.scrollOffsetFromTop) *
-                charSize.cellHeight;
-        final startXMiddleRows = 0.toDouble();
-        final endYMiddleRows = min(
-                hit.endLineIndex - terminal.scrollOffsetFromTop,
-                terminal.terminalHeight) *
-            charSize.cellHeight;
-        final endXMiddleRows = terminal.terminalWidth * charSize.cellWidth;
-        canvas.drawRect(
-            Rect.fromLTRB(startXMiddleRows, startYMiddleRows, endXMiddleRows,
-                endYMiddleRows),
-            paint);
-      }
-      //draw end row: line start - end
-      if (hit.endLineIndex - terminal.scrollOffsetFromTop <
-          terminal.terminalHeight) {
-        final startXEndRow = 0.toDouble();
-        final startYEndRow = (hit.endLineIndex - terminal.scrollOffsetFromTop) *
-            charSize.cellHeight;
-        final endXEndRow = hit.endIndex * charSize.cellWidth;
-        final endYEndRow = startYEndRow + charSize.cellHeight;
-        canvas.drawRect(
-            Rect.fromLTRB(startXEndRow, startYEndRow, endXEndRow, endYEndRow),
-            paint);
-      }
-    }
-
-    final visibleLines = terminal.getVisibleLines();
-
-    //paint text
-    for (var rawRow = hit.startLineIndex;
-        rawRow <= hit.endLineIndex;
-        rawRow++) {
-      final start = rawRow == hit.startLineIndex ? hit.startIndex : 0;
-      final end =
-          rawRow == hit.endLineIndex ? hit.endIndex : terminal.terminalWidth;
-
-      final row = rawRow - terminal.scrollOffsetFromTop;
-
-      final offsetY = row * charSize.cellHeight;
-
-      if (row >= visibleLines.length || row < 0) {
-        continue;
-      }
-
-      final line = visibleLines[row];
-
-      for (var col = start; col < end; col++) {
-        final offsetX = col * charSize.cellWidth;
-        _paintCell(
-          canvas,
-          line,
-          col,
-          offsetX,
-          offsetY,
-          fgColorOverride: terminal.theme.searchHitForeground,
-          bgColorOverride: terminal.theme.searchHitForeground,
-        );
-      }
-    }
-  }
-
-  void _paintSelection(Canvas canvas) {
-    final paint = Paint()..color = Colors.white.withOpacity(0.3);
-
-    for (var y = 0; y < terminal.terminalHeight; y++) {
-      final offsetY = y * charSize.cellHeight;
-      final absoluteY = terminal.convertViewLineToRawLine(y) -
-          terminal.scrollOffsetFromBottom;
-
-      for (var x = 0; x < terminal.terminalWidth; x++) {
-        var cellCount = 0;
-
-        while (
-            (terminal.selection?.contains(Position(x + cellCount, absoluteY)) ??
-                    false) &&
-                x + cellCount < terminal.terminalWidth) {
-          cellCount++;
-        }
-
-        if (cellCount == 0) {
-          continue;
-        }
-
-        final offsetX = x * charSize.cellWidth;
-        final effectWidth = cellCount * charSize.cellWidth;
-        final effectHeight = charSize.cellHeight;
-
-        canvas.drawRect(
-          Rect.fromLTWH(offsetX, offsetY, effectWidth, effectHeight),
-          paint,
-        );
-
-        x += cellCount;
-      }
-    }
-  }
-
-  void _paintText(Canvas canvas) {
-    final lines = terminal.getVisibleLines();
-
-    for (var row = 0; row < lines.length; row++) {
-      final line = lines[row];
-      final offsetY = row * charSize.cellHeight;
-      // final cellCount = math.min(terminal.viewWidth, line.length);
-      final cellCount = terminal.terminalWidth;
-
-      for (var col = 0; col < cellCount; col++) {
-        final width = line.cellGetWidth(col);
-
-        if (width == 0) {
-          continue;
-        }
-        final offsetX = col * charSize.cellWidth;
-        _paintCell(
-          canvas,
-          line,
-          col,
-          offsetX,
-          offsetY,
-        );
-      }
-    }
-  }
-
-  int _getColor(int colorCode) {
-    return (colorCode == 0) ? 0xFF000000 : colorCode;
-  }
-
-  void _paintCell(
-    Canvas canvas,
-    BufferLine line,
-    int cell,
-    double offsetX,
-    double offsetY, {
-    int? fgColorOverride,
-    int? bgColorOverride,
-  }) {
-    final codePoint = line.cellGetContent(cell);
-    final fgColor = fgColorOverride ?? _getColor(line.cellGetFgColor(cell));
-    final bgColor = bgColorOverride ?? _getColor(line.cellGetBgColor(cell));
-    final flags = line.cellGetFlags(cell);
-
-    if (codePoint == 0 || flags.hasFlag(CellFlags.invisible)) {
-      return;
-    }
-
-    // final cellHash = line.cellGetHash(cell);
-    final cellHash = hashValues(codePoint, fgColor, bgColor, flags);
-
-    var character = textLayoutCache.getLayoutFromCache(cellHash);
-    if (character != null) {
-      canvas.drawParagraph(character, Offset(offsetX, offsetY));
-      return;
-    }
-
-    final cellColor = flags.hasFlag(CellFlags.inverse) ? bgColor : fgColor;
-
-    var color = Color(cellColor);
-
-    if (flags & CellFlags.faint != 0) {
-      color = color.withOpacity(0.5);
-    }
-
-    final styleToUse = PaintHelper.getStyleToUse(
-      style,
-      color,
-      bold: flags.hasFlag(CellFlags.bold),
-      italic: flags.hasFlag(CellFlags.italic),
-      underline: flags.hasFlag(CellFlags.underline),
-    );
-
-    character = textLayoutCache.performAndCacheLayout(
-        String.fromCharCode(codePoint), styleToUse, cellHash);
-
-    canvas.drawParagraph(character, Offset(offsetX, offsetY));
-  }
-
-  @override
-  bool shouldRepaint(CustomPainter oldDelegate) {
-    /// paint only when the terminal has changed since last paint.
-    return terminal.dirty;
-  }
-}
-
-class CursorPainter extends CustomPainter {
-  final bool visible;
-  final CellSize charSize;
-  final bool focused;
-  final bool blinkVisible;
-  final int cursorColor;
-  final int textColor;
-  final String composingString;
-  final TextLayoutCache textLayoutCache;
-  final TerminalStyle style;
-
-  CursorPainter({
-    required this.visible,
-    required this.charSize,
-    required this.focused,
-    required this.blinkVisible,
-    required this.cursorColor,
-    required this.textColor,
-    required this.composingString,
-    required this.textLayoutCache,
-    required this.style,
-  });
-
-  @override
-  void paint(Canvas canvas, Size size) {
-    bool isVisible =
-        visible && (blinkVisible || composingString != '' || !focused);
-    if (isVisible) {
-      _paintCursor(canvas);
-    }
-  }
-
-  @override
-  bool shouldRepaint(covariant CustomPainter oldDelegate) {
-    if (oldDelegate is CursorPainter) {
-      return blinkVisible != oldDelegate.blinkVisible ||
-          focused != oldDelegate.focused ||
-          visible != oldDelegate.visible ||
-          charSize.cellWidth != oldDelegate.charSize.cellWidth ||
-          charSize.cellHeight != oldDelegate.charSize.cellHeight ||
-          composingString != oldDelegate.composingString;
-    }
-    return true;
-  }
-
-  void _paintCursor(Canvas canvas) {
-    final paint = Paint()
-      ..color = Color(cursorColor)
-      ..strokeWidth = focused ? 0.0 : 1.0
-      ..style = focused ? PaintingStyle.fill : PaintingStyle.stroke;
-
-    canvas.drawRect(
-        Rect.fromLTWH(0, 0, charSize.cellWidth, charSize.cellHeight), paint);
-
-    if (composingString != '') {
-      final styleToUse = PaintHelper.getStyleToUse(style, Color(textColor));
-      final character = textLayoutCache.performAndCacheLayout(
-          composingString, styleToUse, null);
-      canvas.drawParagraph(character, Offset(0, 0));
-    }
-  }
-}
-
-class PaintHelper {
-  static TextStyle getStyleToUse(
-    TerminalStyle style,
-    Color color, {
-    bool bold = false,
-    bool italic = false,
-    bool underline = false,
-  }) {
-    return (style.textStyleProvider != null)
-        ? style.textStyleProvider!(
-            color: color,
-            fontSize: style.fontSize,
-            fontWeight: bold && !style.ignoreBoldFlag
-                ? FontWeight.bold
-                : FontWeight.normal,
-            fontStyle: italic ? FontStyle.italic : FontStyle.normal,
-            decoration:
-                underline ? TextDecoration.underline : TextDecoration.none,
-          )
-        : TextStyle(
-            color: color,
-            fontSize: style.fontSize,
-            fontWeight: bold && !style.ignoreBoldFlag
-                ? FontWeight.bold
-                : FontWeight.normal,
-            fontStyle: italic ? FontStyle.italic : FontStyle.normal,
-            decoration:
-                underline ? TextDecoration.underline : TextDecoration.none,
-            fontFamily: 'monospace',
-            fontFamilyFallback: style.fontFamily,
-          );
-  }
-}

+ 0 - 563
lib/frontend/terminal_view.dart

@@ -1,563 +0,0 @@
-import 'dart:math' as math;
-
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/gestures.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/scheduler.dart';
-import 'package:xterm/frontend/cache.dart';
-import 'package:xterm/frontend/char_size.dart';
-import 'package:xterm/frontend/helpers.dart';
-import 'package:xterm/frontend/input_behavior.dart';
-import 'package:xterm/frontend/input_behaviors.dart';
-import 'package:xterm/frontend/input_listener.dart';
-import 'package:xterm/frontend/oscillator.dart';
-import 'package:xterm/frontend/terminal_painters.dart';
-import 'package:xterm/mouse/position.dart';
-import 'package:xterm/terminal/terminal_ui_interaction.dart';
-import 'package:xterm/theme/terminal_style.dart';
-
-class TerminalView extends StatefulWidget {
-  TerminalView({
-    Key? key,
-    required this.terminal,
-    this.style = const TerminalStyle(),
-    this.opacity = 1.0,
-    FocusNode? focusNode,
-    this.autofocus = false,
-    ScrollController? scrollController,
-    this.inputType = TextInputType.text,
-    this.enableSuggestions = false,
-    this.inputAction = TextInputAction.done,
-    this.keyboardAppearance = Brightness.light,
-    this.autocorrect = false,
-    InputBehavior? inputBehavior,
-    this.scrollBehavior,
-    this.padding = 0.0,
-  })  : focusNode = focusNode ?? FocusNode(),
-        scrollController = scrollController ?? ScrollController(),
-        inputBehavior = inputBehavior ?? InputBehaviors.platform,
-        super(key: key ?? ValueKey(terminal));
-
-  final TerminalUiInteraction terminal;
-  final FocusNode focusNode;
-  final bool autofocus;
-  final ScrollController scrollController;
-  final TextInputType inputType;
-  final bool enableSuggestions;
-  final TextInputAction inputAction;
-  final Brightness keyboardAppearance;
-  final bool autocorrect;
-
-  final TerminalStyle style;
-  final double opacity;
-
-  final double padding;
-
-  final InputBehavior inputBehavior;
-
-  final ScrollBehavior? scrollBehavior;
-
-  // get the dimensions of a rendered character
-  CellSize measureCellSize(double fontSize) {
-    final testString = 'xxxxxxxxxx' * 1000;
-
-    final text = Text(
-      testString,
-      maxLines: 1,
-      style: (style.textStyleProvider != null)
-          ? style.textStyleProvider!(
-              fontSize: fontSize,
-            )
-          : TextStyle(
-              fontFamily: 'monospace',
-              fontFamilyFallback: style.fontFamily,
-              fontSize: fontSize,
-            ),
-    );
-
-    final size = textSize(text);
-
-    final charWidth = (size.width / testString.length);
-    final charHeight = size.height;
-
-    final cellWidth = charWidth * style.fontWidthScaleFactor;
-    final cellHeight = size.height * style.fontHeightScaleFactor;
-
-    return CellSize(
-      charWidth: charWidth,
-      charHeight: charHeight,
-      cellWidth: cellWidth,
-      cellHeight: cellHeight,
-      letterSpacing: cellWidth - charWidth,
-      lineSpacing: cellHeight - charHeight,
-    );
-  }
-
-  @override
-  _TerminalViewState createState() => _TerminalViewState();
-}
-
-class _TerminalViewState extends State<TerminalView> {
-  /// blinking cursor and blinking character
-  final blinkOscillator = Oscillator.ms(600);
-
-  final textLayoutCache = TextLayoutCache(TextDirection.ltr, 10240);
-
-  bool get focused {
-    return widget.focusNode.hasFocus;
-  }
-
-  late CellSize _cellSize;
-  Position? _tapPosition;
-
-  /// Scroll position from the terminal. Not null if terminal scroll extent has
-  /// been updated and needs to be syncronized to flutter side.
-  double? _pendingTerminalScrollExtent;
-
-  void onTerminalChange() {
-    _pendingTerminalScrollExtent =
-        _cellSize.cellHeight * widget.terminal.scrollOffsetFromTop;
-
-    if (mounted) {
-      setState(() {});
-    }
-  }
-
-  // listen to oscillator to update mouse blink etc.
-  // void onTick() {
-  //   widget.terminal.refresh();
-  // }
-
-  @override
-  void initState() {
-    blinkOscillator.start();
-    // oscillator.addListener(onTick);
-
-    // measureCellSize is expensive so we cache the result.
-    _cellSize = widget.measureCellSize(widget.style.fontSize);
-
-    widget.terminal.addListener(onTerminalChange);
-
-    super.initState();
-  }
-
-  @override
-  void didUpdateWidget(TerminalView oldWidget) {
-    oldWidget.terminal.removeListener(onTerminalChange);
-    widget.terminal.addListener(onTerminalChange);
-
-    if (oldWidget.style != widget.style) {
-      _cellSize = widget.measureCellSize(widget.style.fontSize);
-      textLayoutCache.clear();
-      updateTerminalSize();
-    }
-
-    super.didUpdateWidget(oldWidget);
-  }
-
-  @override
-  void dispose() {
-    blinkOscillator.stop();
-    // oscillator.removeListener(onTick);
-
-    widget.terminal.removeListener(onTerminalChange);
-    super.dispose();
-  }
-
-  GlobalKey _keyCursor = GlobalKey();
-
-  @override
-  Widget build(BuildContext context) {
-    return InputListener(
-      listenKeyStroke: widget.inputBehavior.acceptKeyStroke,
-      onKeyStroke: onKeyStroke,
-      onTextInput: onInput,
-      onAction: onAction,
-      onFocus: onFocus,
-      focusNode: widget.focusNode,
-      autofocus: widget.autofocus,
-      initEditingState: widget.inputBehavior.initEditingState,
-      inputType: widget.inputType,
-      enableSuggestions: widget.enableSuggestions,
-      inputAction: widget.inputAction,
-      keyboardAppearance: widget.keyboardAppearance,
-      autocorrect: widget.autocorrect,
-      child: MouseRegion(
-        cursor: SystemMouseCursors.text,
-        child: LayoutBuilder(builder: (context, constraints) {
-          onWidgetSize(constraints.maxWidth - widget.padding * 2,
-              constraints.maxHeight - widget.padding * 2);
-
-          if (_keyCursor.currentContext != null) {
-            /// this gets set so that the accent selection menu on MacOS pops up
-            /// at the right spot
-            final RenderBox cursorRenderObj =
-                _keyCursor.currentContext!.findRenderObject() as RenderBox;
-            final offset = cursorRenderObj.localToGlobal(Offset.zero);
-            InputListener.of(context)!.setCaretRect(
-              Rect.fromLTWH(
-                offset.dx,
-                offset.dy,
-                _cellSize.cellWidth,
-                _cellSize.cellHeight,
-              ),
-            );
-          }
-
-          // use flutter's Scrollable to manage scrolling to better integrate
-          // with widgets such as Scrollbar.
-          return NotificationListener<ScrollNotification>(
-            onNotification: (notification) {
-              onScroll(notification.metrics.pixels);
-              return false;
-            },
-            child: ScrollConfiguration(
-              behavior: widget.scrollBehavior ??
-                  ScrollConfiguration.of(context).copyWith(scrollbars: false),
-              child: Scrollable(
-                controller: widget.scrollController,
-                viewportBuilder: (context, offset) {
-                  if (!widget.scrollController.hasClients) {
-                    return buildTerminal(context);
-                  }
-                  final position = widget.scrollController.position;
-
-                  /// use [_EmptyScrollActivity] to suppress unexpected behaviors
-                  /// that come from [applyViewportDimension].
-                  if (InputBehaviors.platform == InputBehaviors.desktop &&
-                      position is ScrollActivityDelegate) {
-                    position.beginActivity(
-                      _EmptyScrollActivity(position as ScrollActivityDelegate),
-                    );
-                  }
-
-                  final viewPortHeight =
-                      constraints.maxHeight - widget.padding * 2;
-
-                  // set viewport height.
-                  offset.applyViewportDimension(viewPortHeight);
-
-                  if (widget.terminal.isReady) {
-                    final minScrollExtent = 0.0;
-
-                    final maxScrollExtent = math.max(
-                        0.0,
-                        _cellSize.cellHeight *
-                            (widget.terminal.bufferHeight -
-                                widget.terminal.terminalHeight));
-
-                    // set how much the terminal can scroll
-                    offset.applyContentDimensions(
-                        minScrollExtent, maxScrollExtent);
-
-                    // synchronize pending terminal scroll extent to ScrollController
-                    if (_pendingTerminalScrollExtent != null) {
-                      position.correctPixels(_pendingTerminalScrollExtent!);
-                      _pendingTerminalScrollExtent = null;
-                    }
-                  }
-
-                  return buildTerminal(context);
-                },
-              ),
-            ),
-          );
-        }),
-      ),
-    );
-  }
-
-  Widget buildTerminal(BuildContext context) {
-    return GestureDetector(
-      behavior: HitTestBehavior.deferToChild,
-      dragStartBehavior: DragStartBehavior.down,
-      onDoubleTapDown: (detail) {
-        final pos = detail.localPosition;
-        _tapPosition = getMouseOffset(pos.dx, pos.dy);
-      },
-      onTapDown: (detail) {
-        final pos = detail.localPosition;
-        _tapPosition = getMouseOffset(pos.dx, pos.dy);
-      },
-      onDoubleTap: () {
-        if (_tapPosition != null) {
-          widget.terminal.onMouseDoubleTap(_tapPosition!);
-          widget.terminal.refresh();
-        }
-      },
-      onTap: () {
-        if (widget.terminal.selection?.isEmpty ?? true) {
-          InputListener.of(context)!.requestKeyboard();
-        } else {
-          widget.terminal.clearSelection();
-        }
-        if (_tapPosition != null) {
-          widget.terminal.onMouseTap(_tapPosition!);
-          widget.terminal.refresh();
-        }
-      },
-      onPanStart: (detail) {
-        final pos = detail.localPosition;
-        final offset = getMouseOffset(pos.dx, pos.dy);
-        widget.terminal.onPanStart(offset);
-        widget.terminal.refresh();
-      },
-      onPanUpdate: (detail) {
-        final pos = detail.localPosition;
-        final offset = getMouseOffset(pos.dx, pos.dy);
-        widget.terminal.onPanUpdate(offset);
-        widget.terminal.refresh();
-      },
-      child: Container(
-        constraints: BoxConstraints.expand(),
-        child: Padding(
-          padding: EdgeInsets.all(widget.padding),
-          child: Stack(
-            children: <Widget>[
-              CustomPaint(
-                painter: TerminalPainter(
-                  terminal: widget.terminal,
-                  style: widget.style,
-                  charSize: _cellSize,
-                  textLayoutCache: textLayoutCache,
-                ),
-                child: Container(), //to get the size
-              ),
-              Positioned(
-                key: _keyCursor,
-                child: CursorView(
-                  terminal: widget.terminal,
-                  cellSize: _cellSize,
-                  focusNode: widget.focusNode,
-                  blinkOscillator: blinkOscillator,
-                  style: widget.style,
-                  textLayoutCache: textLayoutCache,
-                ),
-                width: _cellSize.cellWidth,
-                height: _cellSize.cellHeight,
-                left: _getCursorOffset().dx,
-                top: _getCursorOffset().dy,
-              ),
-            ],
-          ),
-        ),
-        color: Color(widget.terminal.backgroundColor).withOpacity(
-          widget.opacity,
-        ),
-      ),
-    );
-  }
-
-  Offset _getCursorOffset() {
-    final screenCursorY = widget.terminal.cursorY;
-    final offsetX = _cellSize.cellWidth * widget.terminal.cursorX;
-    final offsetY = _cellSize.cellHeight * screenCursorY;
-
-    return Offset(offsetX, offsetY);
-  }
-
-  /// Get global cell position from mouse position.
-  Position getMouseOffset(double px, double py) {
-    final col = ((px - widget.padding) / _cellSize.cellWidth).floor();
-    final row = ((py - widget.padding) / _cellSize.cellHeight).floor();
-
-    final x = col;
-    final y = widget.terminal.convertViewLineToRawLine(row) -
-        widget.terminal.scrollOffsetFromBottom;
-
-    return Position(x, y);
-  }
-
-  double? _width;
-  double? _height;
-
-  void onWidgetSize(double width, double height) {
-    if (!widget.terminal.isReady) {
-      return;
-    }
-
-    _width = width;
-    _height = height;
-
-    updateTerminalSize();
-  }
-
-  int? _lastTerminalWidth;
-  int? _lastTerminalHeight;
-
-  void updateTerminalSize() {
-    assert(_width != null);
-    assert(_height != null);
-
-    final termWidth = (_width! / _cellSize.cellWidth).floor();
-    final termHeight = (_height! / _cellSize.cellHeight).floor();
-
-    if (_lastTerminalWidth == termWidth && _lastTerminalHeight == termHeight) {
-      return;
-    }
-
-    _lastTerminalWidth = termWidth;
-    _lastTerminalHeight = termHeight;
-
-    widget.terminal.resize(
-      termWidth,
-      termHeight,
-      (termWidth * _cellSize.cellWidth).floor(),
-      (termHeight * _cellSize.cellHeight).floor(),
-    );
-  }
-
-  TextEditingValue? onInput(TextEditingValue value) {
-    return widget.inputBehavior.onTextEdit(value, widget.terminal);
-  }
-
-  void onKeyStroke(RawKeyEvent event) {
-    blinkOscillator.restart();
-    // TODO: find a way to stop scrolling immediately after key stroke.
-    widget.inputBehavior.onKeyStroke(event, widget.terminal);
-    if (event.character?.isNotEmpty == true) {
-      widget.terminal.setScrollOffsetFromBottom(0);
-    }
-  }
-
-  void onFocus(bool focused) {
-    SchedulerBinding.instance.addPostFrameCallback((_) {
-      widget.terminal.refresh();
-    });
-  }
-
-  void onAction(TextInputAction action) {
-    widget.inputBehavior.onAction(action, widget.terminal);
-  }
-
-  // synchronize flutter scroll offset to terminal
-  void onScroll(double offset) {
-    final topOffset = (offset / _cellSize.cellHeight).ceil();
-    final bottomOffset = widget.terminal.invisibleHeight - topOffset;
-    widget.terminal.setScrollOffsetFromBottom(bottomOffset);
-  }
-}
-
-class CursorView extends StatefulWidget {
-  final CellSize cellSize;
-  final TerminalUiInteraction terminal;
-  final FocusNode? focusNode;
-  final Oscillator blinkOscillator;
-  final TerminalStyle style;
-  final TextLayoutCache textLayoutCache;
-
-  CursorView({
-    required this.terminal,
-    required this.cellSize,
-    required this.focusNode,
-    required this.blinkOscillator,
-    required this.style,
-    required this.textLayoutCache,
-  });
-
-  @override
-  State<StatefulWidget> createState() => _CursorViewState();
-}
-
-class _CursorViewState extends State<CursorView> {
-  bool get focused {
-    return widget.focusNode?.hasFocus ?? false;
-  }
-
-  var _isOscillatorCallbackRegistered = false;
-
-  @override
-  void initState() {
-    _isOscillatorCallbackRegistered = true;
-    widget.blinkOscillator.addListener(onOscillatorTick);
-
-    widget.terminal.addListener(onTerminalChange);
-
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return CustomPaint(
-      painter: CursorPainter(
-        visible: _isCursorVisible(),
-        focused: focused,
-        charSize: widget.cellSize,
-        blinkVisible: widget.blinkOscillator.value,
-        cursorColor: widget.terminal.cursorColor,
-        textColor: widget.terminal.backgroundColor,
-        style: widget.style,
-        composingString: widget.terminal.composingString,
-        textLayoutCache: widget.textLayoutCache,
-      ),
-    );
-  }
-
-  bool _isCursorVisible() {
-    final screenCursorY =
-        widget.terminal.cursorY + widget.terminal.scrollOffsetFromBottom;
-
-    if (screenCursorY < 0 || screenCursorY >= widget.terminal.terminalHeight) {
-      return false;
-    }
-    return widget.terminal.showCursor;
-  }
-
-  @override
-  void dispose() {
-    widget.terminal.removeListener(onTerminalChange);
-    widget.blinkOscillator.removeListener(onOscillatorTick);
-
-    super.dispose();
-  }
-
-  void onTerminalChange() {
-    if (!mounted) {
-      return;
-    }
-
-    setState(() {
-      if (_isCursorVisible() /*&& widget.terminal.blinkingCursor*/ && focused) {
-        if (!_isOscillatorCallbackRegistered) {
-          _isOscillatorCallbackRegistered = true;
-          widget.blinkOscillator.addListener(onOscillatorTick);
-        }
-      } else {
-        if (_isOscillatorCallbackRegistered) {
-          _isOscillatorCallbackRegistered = false;
-          widget.blinkOscillator.removeListener(onOscillatorTick);
-        }
-      }
-    });
-  }
-
-  void onOscillatorTick() {
-    setState(() {});
-  }
-}
-
-/// A scroll activity that does nothing. Used to suppress unexpected behaviors
-/// from [Scrollable] during viewport building process.
-class _EmptyScrollActivity extends IdleScrollActivity {
-  _EmptyScrollActivity(ScrollActivityDelegate delegate) : super(delegate);
-
-  @override
-  void applyNewDimensions() {}
-
-  /// set [isScrolling] to ture to prevent flutter from calling the old scroll
-  /// activity.
-  @override
-  final isScrolling = true;
-
-  void dispatchScrollStartNotification(
-      ScrollMetrics metrics, BuildContext? context) {}
-
-  void dispatchScrollUpdateNotification(
-      ScrollMetrics metrics, BuildContext context, double scrollDelta) {}
-
-  void dispatchOverscrollNotification(
-      ScrollMetrics metrics, BuildContext context, double overscroll) {}
-
-  void dispatchScrollEndNotification(
-      ScrollMetrics metrics, BuildContext context) {}
-}

+ 0 - 37
lib/input/keytab/keytab.dart

@@ -1,37 +0,0 @@
-import 'package:xterm/input/keytab/keytab_default.dart';
-import 'package:xterm/input/keytab/keytab_parse.dart';
-import 'package:xterm/input/keytab/keytab_record.dart';
-import 'package:xterm/input/keytab/keytab_token.dart';
-
-class Keytab {
-  Keytab({
-    required this.name,
-    required this.records,
-  });
-
-  factory Keytab.parse(String source) {
-    final tokens = tokenize(source).toList();
-    final parser = KeytabParser()..addTokens(tokens);
-    return parser.result;
-  }
-
-  factory Keytab.defaultKeytab() {
-    return Keytab.parse(kDefaultKeytab);
-  }
-
-  final String? name;
-  final List<KeytabRecord> records;
-
-  @override
-  String toString() {
-    final buffer = StringBuffer();
-
-    buffer.writeln('keyboard "$name"');
-
-    for (var record in records) {
-      buffer.writeln(record);
-    }
-
-    return buffer.toString();
-  }
-}

+ 0 - 1
lib/input/shortcut.dart

@@ -1 +0,0 @@
-// TBD

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff