diff --git a/.gitignore b/.gitignore index bb431f0d..a66ca3a8 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,7 @@ build/ !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +example/lib/ssh2.dart + +.vscode \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..0377ff44 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "xterm.dart", + "request": "launch", + "type": "dart" + }, + { + "name": "xterm.dart (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "example", + "cwd": "example", + "request": "launch", + "type": "dart" + }, + { + "name": "example (profile mode)", + "cwd": "example", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc415bb..8d88a721 100644 --- a/CHANGELOG.md +++ b/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]. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..312337b5 --- /dev/null +++ b/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 diff --git a/bin/xterm_bench.dart b/bin/xterm_bench.dart new file mode 100644 index 00000000..bd829971 --- /dev/null +++ b/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 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'); +} diff --git a/bin/xterm_dump.dart b/bin/xterm_dump.dart new file mode 100644 index 00000000..4c900349 --- /dev/null +++ b/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 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'); +// } diff --git a/example/.metadata b/example/.metadata index c937ace8..39f2501e 100644 --- a/example/.metadata +++ b/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' diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 61b6c4de..39158cf7 100644 --- a/example/analysis_options.yaml +++ b/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 diff --git a/example/android/.gitignore b/example/android/.gitignore index 0a741cb4..6f568019 100644 --- a/example/android/.gitignore +++ b/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 diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 320be221..5fe3c929 100644 --- a/example/android/app/build.gradle +++ b/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 } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 9b3997fe..3f41384d 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,16 +1,12 @@ - - - - diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml index 449a9f93..3db14bb5 100644 --- a/example/android/app/src/main/res/values-night/styles.xml +++ b/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. --> diff --git a/example/android/build.gradle b/example/android/build.gradle index 3100ad2d..4256f917 100644 --- a/example/android/build.gradle +++ b/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() } } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 38c8d454..94adc3a3 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b..bc6a58af 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/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 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78..f2872cf4 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 00000000..1e8c3c90 --- /dev/null +++ b/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 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 1b28c0ce..6e267c40 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/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", diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16..919434a6 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..3db53b6e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ _MyHomePageState(); -} - -class FakeTerminalBackend extends TerminalBackend { - final _exitCodeCompleter = Completer(); - // ignore: close_sinks - final _outStream = StreamController(); - - @override - Future get exitCode => _exitCodeCompleter.future; - - @override - void init() { - _outStream.sink.add('xterm.dart demo'); - _outStream.sink.add('\r\n'); - _outStream.sink.add('\$ '); - } - - @override - Stream 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 { - TerminalIsolate? terminal; - - Future _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), - ), - ); - }, - ), - ); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index 70b4519f..adfb6394 100644 --- a/example/lib/main.dart +++ b/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 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,81 +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(); - // ignore: close_sinks - final _outStream = StreamController(); - - @override - Future get exitCode => _exitCodeCompleter.future; +class _MyHomePageState extends State { + final terminal = Terminal( + maxLines: 10000, + ); - @override - void init() { - _outStream.sink.add('xterm.dart demo'); - _outStream.sink.add('\r\n'); - _outStream.sink.add('\$ '); - } + late final Pty pty; @override - Stream get out => _outStream.stream; + void initState() { + super.initState(); - @override - void resize(int width, int height, int pixelWidth, int pixelHeight) { - // NOOP + WidgetsBinding.instance.endOfFrame.then( + (_) { + if (mounted) _startPty(); + }, + ); } - @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); - } - } + void _startPty() { + pty = Pty.start( + shell, + columns: terminal.viewWidth, + rows: terminal.viewHeight, + ); - @override - void terminate() { - //NOOP - } + pty.output + .cast>() + .transform(Utf8Decoder()) + .listen(terminal.write); - @override - void ackProcessed() { - //NOOP - } -} + pty.exitCode.then((code) { + terminal.write('the process exited with exit code $code'); + }); -class _MyHomePageState extends State { - final terminal = Terminal( - backend: FakeTerminalBackend(), - maxLines: 10000, - ); + terminal.onOutput = (data) { + pty.write(const Utf8Encoder().convert(data)); + }; - void onInput(String input) {} + terminal.onResize = (w, h, pw, ph) { + pty.resize(h, w); + }; + } @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'; +} diff --git a/example/lib/mock.dart b/example/lib/mock.dart new file mode 100644 index 00000000..1ba0f554 --- /dev/null +++ b/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 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 { + 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)); + } + } + } +} diff --git a/example/lib/ssh.dart b/example/lib/ssh.dart index 35de3085..4f665467 100644 --- a/example/lib/ssh.dart +++ b/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 = ''; const 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,98 +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(); - final _outStream = StreamController(); +class _MyHomePageState extends State { + final terminal = Terminal(); - SSHTerminalBackend(this._host, this._username, this._password); + var title = host; - void onWrite(String data) { - _outStream.sink.add(data); + @override + void initState() { + super.initState(); + initTerminal(); } - @override - Future get exitCode => _exitCodeCompleter.future; + Future initTerminal() async { + terminal.write('Connecting...\r\n'); - @override - void init() { - // Use utf8.decoder to handle broken utf8 chunks - final _sshOutput = StreamController>(); - _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 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 { - 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>() + .transform(Utf8Decoder()) + .listen(terminal.write); + + session.stderr + .cast>() + .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, ), ); } diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index d38195aa..1b8ab33a 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -2,8 +2,14 @@ // Generated file. Do not edit. // +// clang-format off + #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_acrylic_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAcrylicPlugin"); + flutter_acrylic_plugin_register_with_registrar(flutter_acrylic_registrar); } diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h index 9bf74789..e0f0a47b 100644 --- a/example/linux/flutter/generated_plugin_registrant.h +++ b/example/linux/flutter/generated_plugin_registrant.h @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 51436ae8..1ae84db2 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_acrylic +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_pty ) set(PLUGIN_BUNDLED_LIBRARIES) @@ -13,3 +18,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/macos/.gitignore b/example/macos/.gitignore index d2fd3772..746adbb6 100644 --- a/example/macos/.gitignore +++ b/example/macos/.gitignore @@ -3,4 +3,5 @@ **/Pods/ # Xcode-related +**/dgph **/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b6..4b81f9b2 100644 --- a/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/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" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig index c2efd0b6..5caa9d15 100644 --- a/example/macos/Flutter/Flutter-Release.xcconfig +++ b/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" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817a..0f5d8244 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/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")) } diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 00000000..dade8dfa --- /dev/null +++ b/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 diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 00000000..8b14d0a1 --- /dev/null +++ b/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 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index ef1b05f4..d2dcd3af 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/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 = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 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 = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -66,8 +67,12 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; /* 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 = ""; }; @@ -148,10 +155,22 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 413CC3D3B2FBCF7B69907E26 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; + 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 = ""; + }; /* 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)", diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index df12c333..fb7259e1 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - - - - - + + - - + + diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib index 537341ab..80e867a4 100644 --- a/example/macos/Runner/Base.lproj/MainMenu.xib +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -323,6 +323,10 @@ + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig index 1c34a704..8b42559e 100644 --- a/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/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. diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index bc4f5106..d35e43ae 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -2,23 +2,7 @@ - com.apple.security.app-sandbox - - com.apple.security.assets.movies.read-write - - com.apple.security.assets.music.read-write - - com.apple.security.assets.pictures.read-write - com.apple.security.cs.allow-jit - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift index 2722837e..c977593b 100644 --- a/example/macos/Runner/MainFlutterWindow.swift +++ b/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() } -} +} \ No newline at end of file diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements index 69ea0d90..0c67376e 100644 --- a/example/macos/Runner/Release.entitlements +++ b/example/macos/Runner/Release.entitlements @@ -1,22 +1,5 @@ - - com.apple.security.app-sandbox - - com.apple.security.assets.movies.read-write - - com.apple.security.assets.music.read-write - - com.apple.security.assets.pictures.read-write - - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - + diff --git a/example/pubspec.lock b/example/pubspec.lock index df35f7b6..d7fe21bf 100644 --- a/example/pubspec.lock +++ b/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: @@ -21,7 +56,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -35,28 +70,21 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: @@ -64,6 +92,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 +113,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 +127,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: @@ -100,38 +154,59 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" ffi: dependency: transitive description: 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,34 +214,55 @@ 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: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.2" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" pinenacl: dependency: transitive description: @@ -180,7 +276,7 @@ packages: name: platform_info url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.1" + version: "3.1.0" pointycastle: dependency: transitive description: @@ -188,6 +284,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.4.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" quiver: dependency: transitive description: @@ -206,7 +309,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -227,21 +330,21 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.12" typed_data: dependency: transitive description: @@ -249,20 +352,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.1" + version: "1.0.1" win32: dependency: transitive description: @@ -270,13 +373,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.5.0-pre" + 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.14.0 <3.0.0" - flutter: ">=2.0.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 406603f8..e6e68f11 100644 --- a/example/pubspec.yaml +++ b/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,28 +24,36 @@ dependencies: xterm: path: ../ - dartssh2: - git: - url: https://github.com/TerminalStudio/dartssh2 + 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. @@ -84,4 +92,4 @@ flutter: fonts: - family: Cascadia Mono fonts: - - asset: fonts/CascadiaMonoPL.ttf \ No newline at end of file + - asset: fonts/CascadiaMonoPL.ttf diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 5dc5e542..00000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -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); -// }); -// } diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 8b6d4680..20fbca13 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterAcrylicPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 4d10c251..1144ab67 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_acrylic +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_pty ) set(PLUGIN_BUNDLED_LIBRARIES) @@ -13,3 +18,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/fixture/htop_80x25_3s.txt b/fixture/htop_80x25_3s.txt new file mode 100644 index 00000000..37939769 --- /dev/null +++ b/fixture/htop_80x25_3s.txt @@ -0,0 +1 @@ +[?1049h(B[?7h[?1h=[?25l[?1006;1000h(B  0(B[ 0.0%](B  3(B[ 0.0%](B  6(B[ 0.0%](B  9(B[ 0.0%](B 1(B[ 0.0%](B  4(B[ 0.0%](B  7(B[ 0.0%](B  10(B[ 0.0%](B 2(B[ 0.0%](B  5(B[ 0.0%](B  8(B[ 0.0%](B  11(B[ 0.0%](BMem(B[(B||||||||||(B 15.2G/62.8G](B Tasks: (B83(B, (B78(B thr; (B1(B runningSwp(B[0K/16.0G](B Load average: (B0.52 0.57 (B0.50 Uptime: (B1 day, 18:20:08 (B PID USER PRI NI VIRT RES SHR S CPU% MEM%β–½ TIME+ Command  8129 root 20 0 14.9G 12.3G 11260 S 0.0 19.2 1h35:48 /usr/bin/kvm -i(B8130 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 0:00.01 /usr/bin/kvm -i(B8176 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 40:55.62 /usr/bin/kvm -i(B8177 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 11:28.53 /usr/bin/kvm -i(B8178 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 9:00.44 /usr/bin/kvm -i(B8179 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 7:59.73 /usr/bin/kvm -i(B8180 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 7:29.87 /usr/bin/kvm -i(B8181 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 6:23.66 /usr/bin/kvm -i(B8182 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 5:18.48 /usr/bin/kvm -i(B8183 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 4:53.70 /usr/bin/kvm -i(B8186 root20 0 14.9G 12.3G 11(B260 S 0.0 19.2 0:00.00 /usr/bin/kvm -i(B1240 root20 0 3456M 2075M 11(B444 S 0.0 3.2 (B 5h(B21:38 /usr/bin/kvm -i1241 root20 0 3456M 2075M 11(B444 S 0.0 3.2 0:00.01 /usr/bin/kvm -i(B1274 root20 0 3456M 2075M 11(B444 S 0.0 3.2 (B 1h(B19:14 /usr/bin/kvm -i(B1275 root20 0 3456M 2075M 11(B444 S 0.0 3.2 (B 1h(B13:49 /usr/bin/kvm -i(BF1Help (BF2Setup (BF3Search(BF4Filter(BF5Tree (BF6SortBy(BF7Nice -(BF8Nice +(BF9Kill (BF10Quit(B||(B3.9(B||(B1.3(B|||(B 3.3(B|||(B 2(B||(B1.3(B||(B2(B||(B6.4(B||(B3.910(B4(B1.3478718.692.61.3 \ No newline at end of file diff --git a/lib/buffer/buffer.dart b/lib/buffer/buffer.dart deleted file mode 100644 index 33b30f02..00000000 --- a/lib/buffer/buffer.dart +++ /dev/null @@ -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 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 getVisibleLines() { - if (height < _terminal.viewHeight) { - return lines.toList(); - } - - final result = []; - - 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; - } - } -} diff --git a/lib/buffer/line/line.dart b/lib/buffer/line/line.dart deleted file mode 100644 index 16462cda..00000000 --- a/lib/buffer/line/line.dart +++ /dev/null @@ -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(); - - 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(); - } -} diff --git a/lib/buffer/line/line_bytedata_data.dart b/lib/buffer/line/line_bytedata_data.dart deleted file mode 100644 index 7c83e292..00000000 --- a/lib/buffer/line/line_bytedata_data.dart +++ /dev/null @@ -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(); - } -} diff --git a/lib/buffer/line/line_list_data.dart b/lib/buffer/line/line_list_data.dart deleted file mode 100644 index eff310aa..00000000 --- a/lib/buffer/line/line_list_data.dart +++ /dev/null @@ -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 _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(); - } -} diff --git a/lib/buffer/reflow_strategy.dart b/lib/buffer/reflow_strategy.dart deleted file mode 100644 index a12d9127..00000000 --- a/lib/buffer/reflow_strategy.dart +++ /dev/null @@ -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); -} diff --git a/lib/buffer/reflow_strategy_narrower.dart b/lib/buffer/reflow_strategy_narrower.dart deleted file mode 100644 index 42eba825..00000000 --- a/lib/buffer/reflow_strategy_narrower.dart +++ /dev/null @@ -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 = []; - - //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); - } -} diff --git a/lib/buffer/reflow_strategy_wider.dart b/lib/buffer/reflow_strategy_wider.dart deleted file mode 100644 index 443ac193..00000000 --- a/lib/buffer/reflow_strategy_wider.dart +++ /dev/null @@ -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 = []; - - 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); - } -} diff --git a/lib/core.dart b/lib/core.dart new file mode 100644 index 00000000..491455a4 --- /dev/null +++ b/lib/core.dart @@ -0,0 +1,2 @@ +export 'core/terminal.dart'; +export 'ui/terminal_view.dart'; diff --git a/lib/core/buffer/buffer.dart b/lib/core/buffer/buffer.dart new file mode 100644 index 00000000..65c5df51 --- /dev/null +++ b/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(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 = { + 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(); + } +} diff --git a/lib/buffer/cell_flags.dart b/lib/core/buffer/cell_flags.dart similarity index 100% rename from lib/buffer/cell_flags.dart rename to lib/core/buffer/cell_flags.dart diff --git a/lib/core/buffer/line.dart b/lib/core/buffer/line.dart new file mode 100644 index 00000000..791895c5 --- /dev/null +++ b/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(); + } +} diff --git a/lib/core/buffer/position.dart b/lib/core/buffer/position.dart new file mode 100644 index 00000000..c0efbfb5 --- /dev/null +++ b/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; +} diff --git a/lib/core/buffer/range.dart b/lib/core/buffer/range.dart new file mode 100644 index 00000000..f8ce7923 --- /dev/null +++ b/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 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)'; +} diff --git a/lib/core/buffer/segment.dart b/lib/core/buffer/segment.dart new file mode 100644 index 00000000..513955f8 --- /dev/null +++ b/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; +} diff --git a/lib/core/cell.dart b/lib/core/cell.dart new file mode 100644 index 00000000..dd14d2e4 --- /dev/null +++ b/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; +} diff --git a/lib/terminal/charset.dart b/lib/core/charset.dart similarity index 100% rename from lib/terminal/charset.dart rename to lib/core/charset.dart diff --git a/lib/core/color.dart b/lib/core/color.dart new file mode 100644 index 00000000..716219c3 --- /dev/null +++ b/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; +} diff --git a/lib/core/cursor.dart b/lib/core/cursor.dart new file mode 100644 index 00000000..6b020ad8 --- /dev/null +++ b/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); +} diff --git a/lib/core/escape/emitter.dart b/lib/core/escape/emitter.dart new file mode 100644 index 00000000..23d95f85 --- /dev/null +++ b/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~'; + } +} diff --git a/lib/core/escape/handler.dart b/lib/core/escape/handler.dart new file mode 100644 index 00000000..1e2da74f --- /dev/null +++ b/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); +} diff --git a/lib/core/escape/parser.dart b/lib/core/escape/parser.dart new file mode 100644 index 00000000..7e13d44d --- /dev/null +++ b/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 = []; + + 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 params; + + int finalByte; + // final List 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(); diff --git a/lib/core/input/handler.dart b/lib/core/input/handler.dart new file mode 100644 index 00000000..fe0dae0a --- /dev/null +++ b/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 _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; + } +} diff --git a/lib/input/keys.dart b/lib/core/input/keys.dart similarity index 100% rename from lib/input/keys.dart rename to lib/core/input/keys.dart diff --git a/lib/core/input/keytab/keytab.dart b/lib/core/input/keytab/keytab.dart new file mode 100644 index 00000000..3ddf8f3a --- /dev/null +++ b/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 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(); + } +} diff --git a/lib/input/keytab/keytab_default.dart b/lib/core/input/keytab/keytab_default.dart similarity index 98% rename from lib/input/keytab/keytab_default.dart rename to lib/core/input/keytab/keytab_default.dart index 185fbcd5..0f615d1c 100644 --- a/lib/input/keytab/keytab_default.dart +++ b/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 diff --git a/lib/input/keytab/keytab_escape.dart b/lib/core/input/keytab/keytab_escape.dart similarity index 94% rename from lib/input/keytab/keytab_escape.dart rename to lib/core/input/keytab/keytab_escape.dart index dd64020e..5375bc34 100644 --- a/lib/input/keytab/keytab_escape.dart +++ b/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') diff --git a/lib/input/keytab/keytab_parse.dart b/lib/core/input/keytab/keytab_parse.dart similarity index 93% rename from lib/input/keytab/keytab_parse.dart rename to lib/core/input/keytab/keytab_parse.dart index 05eaefaf..38da7897 100644 --- a/lib/input/keytab/keytab_parse.dart +++ b/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); diff --git a/lib/input/keytab/keytab_record.dart b/lib/core/input/keytab/keytab_record.dart similarity index 59% rename from lib/input/keytab/keytab_record.dart rename to lib/core/input/keytab/keytab_record.dart index d6b84e0e..d08690a1 100644 --- a/lib/input/keytab/keytab_record.dart +++ b/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 ''; + } } diff --git a/lib/input/keytab/keytab_token.dart b/lib/core/input/keytab/keytab_token.dart similarity index 100% rename from lib/input/keytab/keytab_token.dart rename to lib/core/input/keytab/keytab_token.dart diff --git a/lib/input/keytab/qt_keyname.dart b/lib/core/input/keytab/qt_keyname.dart similarity index 99% rename from lib/input/keytab/qt_keyname.dart rename to lib/core/input/keytab/qt_keyname.dart index 9245658d..72db9104 100644 --- a/lib/input/keytab/qt_keyname.dart +++ b/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 = { diff --git a/lib/core/mouse.dart b/lib/core/mouse.dart new file mode 100644 index 00000000..49311b1f --- /dev/null +++ b/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, +} diff --git a/lib/core/reflow.dart b/lib/core/reflow.dart new file mode 100644 index 00000000..8130a4e5 --- /dev/null +++ b/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 = []; + + 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 finish() { + if (_builder.isNotEmpty) { + _lines.add(_builder.take(wrapped: _lines.isNotEmpty)); + } + + return _lines; + } +} + +List reflow( + CircularList lines, + int oldWidth, + int newWidth, +) { + final result = []; + + 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; +} diff --git a/lib/core/snapshot.dart b/lib/core/snapshot.dart new file mode 100644 index 00000000..319fc4a2 --- /dev/null +++ b/lib/core/snapshot.dart @@ -0,0 +1,3 @@ +abstract class TerminalSnapshot { + void trimScrollback(); +} diff --git a/lib/core/state.dart b/lib/core/state.dart new file mode 100644 index 00000000..f0fa8498 --- /dev/null +++ b/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; +} diff --git a/lib/core/tabs.dart b/lib/core/tabs.dart new file mode 100644 index 00000000..226f5be9 --- /dev/null +++ b/lib/core/tabs.dart @@ -0,0 +1,46 @@ +import 'dart:math' show min; + +const _kMaxColumns = 1024; + +class TabStops { + final _stops = List.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; + } + } +} diff --git a/lib/core/terminal.dart b/lib/core/terminal.dart new file mode 100644 index 00000000..1cf1711a --- /dev/null +++ b/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 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 + } +} diff --git a/lib/flutter.dart b/lib/flutter.dart deleted file mode 100644 index 381586c4..00000000 --- a/lib/flutter.dart +++ /dev/null @@ -1 +0,0 @@ -export 'frontend/terminal_view.dart'; diff --git a/lib/frontend/char_size.dart b/lib/frontend/char_size.dart deleted file mode 100644 index 80ead3cb..00000000 --- a/lib/frontend/char_size.dart +++ /dev/null @@ -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'; - } -} diff --git a/lib/frontend/helpers.dart b/lib/frontend/helpers.dart deleted file mode 100644 index d23bc1c1..00000000 --- a/lib/frontend/helpers.dart +++ /dev/null @@ -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 fontFamily) { - return true; // TBD -} diff --git a/lib/frontend/input_behavior.dart b/lib/frontend/input_behavior.dart deleted file mode 100644 index 4d6d30c3..00000000 --- a/lib/frontend/input_behavior.dart +++ /dev/null @@ -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); -} diff --git a/lib/frontend/input_behavior_default.dart b/lib/frontend/input_behavior_default.dart deleted file mode 100644 index 691fe184..00000000 --- a/lib/frontend/input_behavior_default.dart +++ /dev/null @@ -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) { - // - } -} diff --git a/lib/frontend/input_behavior_desktop.dart b/lib/frontend/input_behavior_desktop.dart deleted file mode 100644 index 226d387c..00000000 --- a/lib/frontend/input_behavior_desktop.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:xterm/frontend/input_behavior_default.dart'; - -class InputBehaviorDesktop extends InputBehaviorDefault { - InputBehaviorDesktop(); -} diff --git a/lib/frontend/input_behavior_mobile.dart b/lib/frontend/input_behavior_mobile.dart deleted file mode 100644 index 2c56486c..00000000 --- a/lib/frontend/input_behavior_mobile.dart +++ /dev/null @@ -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'); - } - } -} diff --git a/lib/frontend/input_behaviors.dart b/lib/frontend/input_behaviors.dart deleted file mode 100644 index ff789774..00000000 --- a/lib/frontend/input_behaviors.dart +++ /dev/null @@ -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; - } -} diff --git a/lib/frontend/input_listener.dart b/lib/frontend/input_listener.dart deleted file mode 100644 index ea97d7d0..00000000 --- a/lib/frontend/input_listener.dart +++ /dev/null @@ -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(); - } -} - -class InputListenerState extends State - 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 data) { - // print('performPrivateCommand $action'); - } -} diff --git a/lib/frontend/mouse_listener.dart b/lib/frontend/mouse_listener.dart deleted file mode 100644 index d6efe821..00000000 --- a/lib/frontend/mouse_listener.dart +++ /dev/null @@ -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); - } - } -} diff --git a/lib/frontend/oscillator.dart b/lib/frontend/oscillator.dart deleted file mode 100644 index fbf4005b..00000000 --- a/lib/frontend/oscillator.dart +++ /dev/null @@ -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; - } -} diff --git a/lib/frontend/terminal_painters.dart b/lib/frontend/terminal_painters.dart deleted file mode 100644 index bd49583e..00000000 --- a/lib/frontend/terminal_painters.dart +++ /dev/null @@ -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, - ); - } -} diff --git a/lib/frontend/terminal_view.dart b/lib/frontend/terminal_view.dart deleted file mode 100644 index d69392ba..00000000 --- a/lib/frontend/terminal_view.dart +++ /dev/null @@ -1,561 +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 { - /// 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( - 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: [ - 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); - 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 createState() => _CursorViewState(); -} - -class _CursorViewState extends State { - 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) {} -} diff --git a/lib/input/keytab/keytab.dart b/lib/input/keytab/keytab.dart deleted file mode 100644 index 1540bd73..00000000 --- a/lib/input/keytab/keytab.dart +++ /dev/null @@ -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 records; - - @override - String toString() { - final buffer = StringBuffer(); - - buffer.writeln('keyboard "$name"'); - - for (var record in records) { - buffer.writeln(record); - } - - return buffer.toString(); - } -} diff --git a/lib/input/shortcut.dart b/lib/input/shortcut.dart deleted file mode 100644 index 954ddd23..00000000 --- a/lib/input/shortcut.dart +++ /dev/null @@ -1 +0,0 @@ -// TBD diff --git a/lib/isolate.dart b/lib/isolate.dart deleted file mode 100644 index 5d56be1b..00000000 --- a/lib/isolate.dart +++ /dev/null @@ -1 +0,0 @@ -export 'terminal/terminal_isolate.dart'; diff --git a/lib/mouse/mouse_kind.dart b/lib/mouse/mouse_kind.dart deleted file mode 100644 index 474ca0b3..00000000 --- a/lib/mouse/mouse_kind.dart +++ /dev/null @@ -1,16 +0,0 @@ -enum MouseKind { - /// A touch-based pointer device. - touch, - - /// A mouse-based pointer device. - mouse, - - /// A pointer device with a stylus. - stylus, - - /// A pointer device with a stylus that has been inverted. - invertedStylus, - - /// An unknown pointer device. - unknown -} diff --git a/lib/mouse/mouse_mode.dart b/lib/mouse/mouse_mode.dart deleted file mode 100644 index c811d4f1..00000000 --- a/lib/mouse/mouse_mode.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:xterm/mouse/position.dart'; -import 'package:xterm/terminal/terminal.dart'; - -abstract class MouseMode { - const MouseMode(); - - static const none = MouseModeNone(); - // static const x10 = MouseModeX10(); - // static const vt200 = MouseModeX10(); - // static const buttonEvent = MouseModeX10(); - - void onTap(Terminal terminal, Position offset); - void onDoubleTap(Terminal terminal, Position offset) {} - void onPanStart(Terminal terminal, Position offset) {} - void onPanUpdate(Terminal terminal, Position offset) {} -} - -class MouseModeNone extends MouseMode { - const MouseModeNone(); - - @override - void onTap(Terminal terminal, Position offset) { - terminal.debug.onMsg('tap: $offset'); - } - - @override - void onDoubleTap(Terminal terminal, Position offset) { - terminal.selectWordOrRow(offset); - } - - @override - void onPanStart(Terminal terminal, Position offset) { - terminal.selection!.init(offset); - } - - @override - void onPanUpdate(Terminal terminal, Position offset) { - terminal.selection!.update(offset); - } -} - -class MouseModeX10 extends MouseMode { - const MouseModeX10(); - - @override - void onTap(Terminal terminal, Position offset) { - final btn = 1; - - final px = offset.x + 1; - final py = terminal.buffer.convertRawLineToViewLine(offset.y) + 1; - - final buffer = StringBuffer(); - buffer.writeCharCode(0x1b); - buffer.write('[M'); - buffer.writeCharCode(btn + 32); - buffer.writeCharCode(px + 32); - buffer.writeCharCode(py + 32); - terminal.backend?.write(buffer.toString()); - } -} diff --git a/lib/mouse/position.dart b/lib/mouse/position.dart deleted file mode 100644 index 77f5461e..00000000 --- a/lib/mouse/position.dart +++ /dev/null @@ -1,27 +0,0 @@ -class Position { - const Position(this.x, this.y); - - final int x; - final int y; - - bool isBefore(Position another) { - return another.y > y || (another.y == y && another.x > x); - } - - bool isAfter(Position another) { - return another.y < y || (another.y == y && another.x < x); - } - - bool isBeforeOrSame(Position another) { - return another.y > y || (another.y == y && another.x >= x); - } - - bool isAfterOrSame(Position another) { - return another.y < y || (another.y == y && another.x <= x); - } - - @override - String toString() { - return 'MouseOffset($x, $y)'; - } -} diff --git a/lib/mouse/selection.dart b/lib/mouse/selection.dart deleted file mode 100644 index 3f1460a2..00000000 --- a/lib/mouse/selection.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:xterm/mouse/position.dart'; - -class Selection { - Position? _start; - Position? _end; - var _endFixed = false; - - Position? get start => _start; - Position? get end => _end; - - void init(Position position) { - _start = position; - _end = position; - _endFixed = false; - } - - void update(Position position) { - final start = _start; - if (start == null) { - return; - } - - if (position.isBefore(start) && !_endFixed) { - _endFixed = true; - _end = _start; - } - - if (position.isAfter(start) && _endFixed) { - _endFixed = false; - _start = _end; - } - - if (_endFixed) { - _start = position; - } else { - _end = position; - } - - // print('($_start, $end'); - } - - void clear() { - _start = null; - _end = null; - _endFixed = false; - } - - bool contains(Position position) { - if (isEmpty) { - return false; - } - - return _start!.isBeforeOrSame(position) && _end!.isAfterOrSame(position); - } - - bool get isEmpty { - return _start == null || _end == null; - } -} diff --git a/lib/terminal/ansi.dart b/lib/terminal/ansi.dart deleted file mode 100644 index 882d3099..00000000 --- a/lib/terminal/ansi.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:collection'; - -import 'package:xterm/terminal/csi.dart'; -import 'package:xterm/terminal/osc.dart'; -import 'package:xterm/terminal/terminal.dart'; -import 'package:xterm/util/lookup_table.dart'; - -/// Handler of terminal sequences. Returns true if the sequence is consumed, -/// false to indicate that the sequence is not completed and no charater is -/// consumed from the queue. -typedef AnsiHandler = bool Function(Queue, Terminal); - -bool ansiHandler(Queue queue, Terminal terminal) { - // The sequence isn't completed, just ignore it. - if (queue.isEmpty) { - return false; - } - - final charAfterEsc = queue.removeFirst(); - - final handler = _ansiHandlers[charAfterEsc]; - if (handler != null) { - // if (handler != csiHandler && handler != oscHandler) { - // terminal.debug.onEsc(charAfterEsc); - // } - - final finished = handler(queue, terminal); - if (!finished) { - queue.addFirst(charAfterEsc); - } - return finished; - } - - terminal.debug.onError('unsupported ansi sequence: $charAfterEsc'); - return true; -} - -final _ansiHandlers = FastLookupTable({ - '['.codeUnitAt(0): csiHandler, - ']'.codeUnitAt(0): oscHandler, - '7'.codeUnitAt(0): _ansiSaveCursorHandler, - '8'.codeUnitAt(0): _ansiRestoreCursorHandler, - 'D'.codeUnitAt(0): _ansiIndexHandler, - 'E'.codeUnitAt(0): _ansiNextLineHandler, - 'H'.codeUnitAt(0): _ansiTabSetHandler, - 'M'.codeUnitAt(0): _ansiReverseIndexHandler, - 'P'.codeUnitAt(0): _unsupportedHandler, // Sixel - 'c'.codeUnitAt(0): _unsupportedHandler, - '#'.codeUnitAt(0): _unsupportedHandler, - '('.codeUnitAt(0): _scsHandler(0), // SCS - G0 - ')'.codeUnitAt(0): _scsHandler(1), // SCS - G1 - '*'.codeUnitAt(0): _voidHandler(1), // TODO: G2 (vt220) - '+'.codeUnitAt(0): _voidHandler(1), // TODO: G3 (vt220) - '>'.codeUnitAt(0): _voidHandler(0), // TODO: Normal Keypad - '='.codeUnitAt(0): _voidHandler(0), // TODO: Application Keypad -}); - -AnsiHandler _voidHandler(int sequenceLength) { - return (queue, terminal) { - if (queue.length < sequenceLength) { - return false; - } - - for (var i = 0; i < sequenceLength; i++) { - queue.removeFirst(); - } - return true; - }; -} - -bool _unsupportedHandler(Queue queue, Terminal terminal) { - // print('unimplemented ansi sequence.'); - return true; -} - -bool _ansiSaveCursorHandler(Queue queue, Terminal terminal) { - terminal.buffer.saveCursor(); - return true; -} - -bool _ansiRestoreCursorHandler(Queue queue, Terminal terminal) { - terminal.buffer.restoreCursor(); - return true; -} - -/// https://vt100.net/docs/vt100-ug/chapter3.html#IND IND – Index -/// -/// ESC D -/// -/// This sequence 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. -bool _ansiIndexHandler(Queue queue, Terminal terminal) { - terminal.buffer.index(); - return true; -} - -bool _ansiReverseIndexHandler(Queue queue, Terminal terminal) { - terminal.buffer.reverseIndex(); - return true; -} - -/// SCS – Select Character Set -/// -/// The appropriate G0 and G1 character sets are designated from one of the five -/// possible character sets. The G0 and G1 sets are invoked by the codes SI and -/// SO (shift in and shift out) respectively. -AnsiHandler _scsHandler(int which) { - return (Queue queue, Terminal terminal) { - // The sequence isn't completed, just ignore it. - if (queue.isEmpty) { - return false; - } - - final name = queue.removeFirst(); - terminal.buffer.charset.designate(which, name); - return true; - }; -} - -bool _ansiNextLineHandler(Queue queue, Terminal terminal) { - terminal.buffer.newLine(); - terminal.buffer.setCursorX(0); - return true; -} - -bool _ansiTabSetHandler(Queue queue, Terminal terminal) { - terminal.tabSetAtCursor(); - return true; -} diff --git a/lib/terminal/csi.dart b/lib/terminal/csi.dart deleted file mode 100644 index 3182e7a9..00000000 --- a/lib/terminal/csi.dart +++ /dev/null @@ -1,502 +0,0 @@ -import 'dart:collection'; - -import 'package:xterm/terminal/modes.dart'; -import 'package:xterm/terminal/sgr.dart'; -import 'package:xterm/terminal/terminal.dart'; -import 'package:xterm/util/lookup_table.dart'; - -typedef CsiSequenceHandler = void Function(CSI, Terminal); - -final _csiHandlers = FastLookupTable({ - 'c'.codeUnitAt(0): csiSendDeviceAttributesHandler, - 'd'.codeUnitAt(0): csiLinePositionAbsolute, - 'f'.codeUnitAt(0): csiCursorPositionHandler, - 'g'.codeUnitAt(0): csiTabClearHandler, - 'h'.codeUnitAt(0): csiModeHandler, // SM - Set Mode - 'l'.codeUnitAt(0): csiModeHandler, // RM - Reset Mode - 'm'.codeUnitAt(0): sgrHandler, - 'n'.codeUnitAt(0): csiDeviceStatusReportHandler, - 'r'.codeUnitAt(0): csiSetMarginsHandler, // DECSTBM - 't'.codeUnitAt(0): csiWindowManipulation, - 'A'.codeUnitAt(0): csiCursorUpHandler, - 'B'.codeUnitAt(0): csiCursorDownHandler, - 'C'.codeUnitAt(0): csiCursorForwardHandler, - 'D'.codeUnitAt(0): csiCursorBackwardHandler, - 'E'.codeUnitAt(0): csiCursorNextLineHandler, - 'F'.codeUnitAt(0): csiCursorPrecedingLineHandler, - 'G'.codeUnitAt(0): csiCursorHorizontalAbsoluteHandler, - 'H'.codeUnitAt(0): csiCursorPositionHandler, // CUP - Cursor Position - 'J'.codeUnitAt(0): csiEraseInDisplayHandler, // DECSED - Selective Erase - 'K'.codeUnitAt(0): csiEraseInLineHandler, - 'L'.codeUnitAt(0): csiInsertLinesHandler, - 'M'.codeUnitAt(0): csiDeleteLinesHandler, - 'P'.codeUnitAt(0): csiDeleteHandler, - 'S'.codeUnitAt(0): csiScrollUpHandler, - 'T'.codeUnitAt(0): csiScrollDownHandler, - 'X'.codeUnitAt(0): csiEraseCharactersHandler, - '@'.codeUnitAt(0): csiInsertBlankCharactersHandler, -}); - -class CSI { - CSI({ - required this.params, - required this.finalByte, - // required this.intermediates, - }); - - int? prefix; - List params; - int finalByte; - // final List intermediates; - - @override - String toString() { - return params.join(';') + String.fromCharCode(finalByte); - } -} - -/// Keep a singleton of [CSI] to reduce object allocation. This should only be -/// modified by [_parseCsi]. -final _csi = CSI( - finalByte: 0, - params: [], -); - -final _semicolon = ';'.codeUnitAt(0); - -/// Parse a CSI from the head of the queue. Return null if the CSI isn't -/// complete. -CSI? _parseCsi(Queue queue) { - _csi.params.clear(); - - // Keep track of how many characters should be taken from the queue. - var readOffset = 0; - - if (queue.isEmpty) { - return null; - } - - // ascii char - // 48 '0' - // 49 '1' - // 50 '2' - // 51 '3' - // 52 '4' - // 53 '5' - // 54 '6' - // 55 '7' - // 56 '8' - // 57 '9' - // 58 ':' - // 59 ';' - // 60 '<' - // 61 '=' - // 62 '>' - // 63 '?' - - // test whether the csi is a `CSI ? Ps ...` or `CSI Ps ...` - final firstChar = queue.first; - if (firstChar >= 58 && firstChar <= 63) { - _csi.prefix = firstChar; - readOffset++; - } else { - _csi.prefix = null; - } - - var param = 0; - var hasParam = false; - while (true) { - // The sequence isn't completed, just ignore it. - if (queue.length <= readOffset) { - return null; - } - - // final char = queue.removeFirst(); - final char = queue.elementAt(readOffset++); - - if (char == _semicolon) { - if (hasParam) { - _csi.params.add(param); - } - param = 0; - continue; - } - - // '0' <= char <= '9' - if (char >= 48 && char <= 57) { - hasParam = true; - param *= 10; - param += char - 48; - continue; - } - - if (char > 0 && char <= 0x2F) { - // intermediates.add(char); - continue; - } - - const csiMin = 0x40; - const csiMax = 0x7e; - - if (char >= csiMin && char <= csiMax) { - // The sequence is complete. So we consume it from the queue. - for (var i = 0; i < readOffset; i++) { - queue.removeFirst(); - } - - if (hasParam) { - _csi.params.add(param); - } - - _csi.finalByte = char; - return _csi; - } - } -} - -/// CSI - Control Sequence Introducer: sequence starting with ESC [ (7bit) or -/// CSI (\x9B, 8bit) -bool csiHandler(Queue queue, Terminal terminal) { - final csi = _parseCsi(queue); - - if (csi == null) { - return false; - } - - // terminal.debug.onCsi(csi); - - final handler = _csiHandlers[csi.finalByte]; - - if (handler != null) { - handler(csi, terminal); - } else { - terminal.debug.onError('unknown: $csi'); - } - - return true; -} - -/// DECSED - Selective Erase In Display -/// -/// ```text -/// CSI ? P s J -/// -/// Erase in Display (DECSED) -/// -/// P s = 0 β†’ Selective Erase Below (default) -/// P s = 1 β†’ Selective Erase Above -/// P s = 2 β†’ Selective Erase All -/// ``` -void csiEraseInDisplayHandler(CSI csi, Terminal terminal) { - var ps = 0; - - if (csi.params.isNotEmpty) { - ps = csi.params.first; - } - - switch (ps) { - case 0: - terminal.buffer.eraseDisplayFromCursor(); - break; - case 1: - terminal.buffer.eraseDisplayToCursor(); - break; - case 2: - case 3: - terminal.buffer.eraseDisplay(); - break; - default: - terminal.debug.onError("Unsupported ED: CSI $ps J"); - } -} - -void csiEraseInLineHandler(CSI csi, Terminal terminal) { - var ps = 0; - - if (csi.params.isNotEmpty) { - ps = csi.params.first; - } - - switch (ps) { - case 0: - terminal.buffer.eraseLineFromCursor(); - break; - case 1: - terminal.buffer.eraseLineToCursor(); - break; - case 2: - terminal.buffer.eraseLine(); - break; - default: - terminal.debug.onError("Unsupported EL: CSI $ps K"); - } -} - -/// CUP - Cursor Position -void csiCursorPositionHandler(CSI csi, Terminal terminal) { - var x = 1; - var y = 1; - - if (csi.params.length == 2) { - y = csi.params[0]; - x = csi.params[1]; - } - - terminal.buffer.setPosition(x - 1, y - 1); -} - -void csiLinePositionAbsolute(CSI csi, Terminal terminal) { - var row = 1; - - if (csi.params.isNotEmpty) { - row = csi.params.first; - } - - terminal.buffer.setCursorY(row - 1); -} - -void csiCursorHorizontalAbsoluteHandler(CSI csi, Terminal terminal) { - var x = 1; - - if (csi.params.isNotEmpty) { - x = csi.params.first; - } - - terminal.buffer.setCursorX(x - 1); -} - -void csiCursorForwardHandler(CSI csi, Terminal terminal) { - var offset = 1; - - if (csi.params.isNotEmpty) { - offset = csi.params.first; - } - - terminal.buffer.movePosition(offset, 0); -} - -void csiCursorBackwardHandler(CSI csi, Terminal terminal) { - var offset = 1; - - if (csi.params.isNotEmpty) { - offset = csi.params.first; - } - - terminal.buffer.movePosition(-offset, 0); -} - -void csiEraseCharactersHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - terminal.buffer.eraseCharacters(count); -} - -void csiModeHandler(CSI csi, Terminal terminal) { - // terminal.ActiveBuffer().ClearSelection() - return csiSetModes(csi, terminal); -} - -void csiDeviceStatusReportHandler(CSI csi, Terminal terminal) { - if (csi.params.isEmpty) return; - - switch (csi.params[0]) { - case 5: - terminal.backend?.write("\x1b[0n"); - break; - case 6: // report cursor position - terminal.backend - ?.write("\x1b[${terminal.cursorX + 1};${terminal.cursorY + 1}R"); - break; - default: - terminal.debug - .onError('Unknown Device Status Report identifier: ${csi.params[0]}'); - return; - } -} - -void csiSendDeviceAttributesHandler(CSI csi, Terminal terminal) { - var response = '?1;2'; - - if (csi.prefix == 62 /* '>' */) { - response = '>0;0;0'; - } - - terminal.backend?.write('\x1b[${response}c'); -} - -void csiCursorUpHandler(CSI csi, Terminal terminal) { - var distance = 1; - - if (csi.params.isNotEmpty) { - distance = csi.params.first; - } - - terminal.buffer.movePosition(0, -distance); -} - -void csiCursorDownHandler(CSI csi, Terminal terminal) { - var distance = 1; - - if (csi.params.isNotEmpty) { - distance = csi.params.first; - } - - terminal.buffer.movePosition(0, distance); -} - -/// DECSTBM – Set Top and Bottom Margins (DEC Private) -/// -/// ESC [ Pn; Pn r -/// -/// This sequence sets the top and bottom margins to define the scrolling -/// region. The first parameter is the line number of the first line in the -/// scrolling region; the second parameter is the line number of the bottom line -/// in the scrolling region. Default is the en tire screen (no margins). The -/// minimum size of the scrolling region allowed is two lines, i.e., the top -/// margin must be less than the bottom margin. The cursor is placed in the home -/// position (see Origin Mode DECOM). -void csiSetMarginsHandler(CSI csi, Terminal terminal) { - var top = 1; - var bottom = terminal.viewHeight; - - if (csi.params.length > 2) { - return; - } - - if (csi.params.isNotEmpty) { - top = csi.params[0]; - - if (csi.params.length > 1) { - bottom = csi.params[1]; - } - } - - terminal.buffer.setVerticalMargins(top - 1, bottom - 1); - terminal.buffer.setPosition(0, 0); -} - -void csiDeleteHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - if (count < 1) { - count = 1; - } - - terminal.buffer.deleteChars(count); -} - -void csiTabClearHandler(CSI csi, Terminal terminal) { - // TODO -} - -void csiWindowManipulation(CSI csi, Terminal terminal) { - // not supported -} - -void csiCursorNextLineHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - if (count < 1) { - count = 1; - } - - terminal.buffer.moveCursorY(count); - terminal.buffer.setCursorX(0); -} - -void csiCursorPrecedingLineHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - if (count < 1) { - count = 1; - } - - terminal.buffer.moveCursorY(-count); - terminal.buffer.setCursorX(0); -} - -void csiInsertLinesHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - if (count < 1) { - count = 1; - } - - terminal.buffer.insertLines(count); -} - -void csiDeleteLinesHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - if (count < 1) { - count = 1; - } - - terminal.buffer.deleteLines(count); -} - -void csiScrollUpHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - if (count < 1) { - count = 1; - } - - terminal.buffer.areaScrollUp(count); -} - -void csiScrollDownHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - if (count < 1) { - count = 1; - } - - terminal.buffer.areaScrollDown(count); -} - -void csiInsertBlankCharactersHandler(CSI csi, Terminal terminal) { - var count = 1; - - if (csi.params.isNotEmpty) { - count = csi.params.first; - } - - if (count < 1) { - count = 1; - } - - terminal.buffer.insertBlankCharacters(count); -} diff --git a/lib/terminal/cursor.dart b/lib/terminal/cursor.dart deleted file mode 100644 index 522ef64b..00000000 --- a/lib/terminal/cursor.dart +++ /dev/null @@ -1,12 +0,0 @@ -/// Keeps the default style of newly created cells. -class Cursor { - Cursor({ - required this.fg, - required this.bg, - required this.flags, - }); - - int fg; - int bg; - int flags; -} diff --git a/lib/terminal/modes.dart b/lib/terminal/modes.dart deleted file mode 100644 index c9c5e803..00000000 --- a/lib/terminal/modes.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'package:xterm/mouse/mouse_mode.dart'; -import 'package:xterm/terminal/csi.dart'; -import 'package:xterm/terminal/terminal.dart'; - -final _decset = 'h'.codeUnitAt(0); -final _decrst = 'l'.codeUnitAt(0); - -bool _isEnabled(int finalByte) { - if (finalByte == _decset) { - return true; - } - - if (finalByte == _decrst) { - return false; - } - - // print('unexpected finalByte: $finalByte'); - return true; -} - -void csiSetModes(CSI csi, Terminal terminal) { - if (csi.params.isEmpty) { - // print('warning: no mode specified.'); - return; - } - - final enabled = _isEnabled(csi.finalByte); - - const decPrefix = 63; // '?' - final isDec = csi.prefix == decPrefix; - - for (var mode in csi.params) { - if (isDec) { - csiDecSetMode(mode, enabled, terminal); - } else { - csiSetMode(mode, enabled, terminal); - } - } -} - -void csiSetMode(int mode, bool enabled, Terminal terminal) { - switch (mode) { - case 4: - if (enabled) { - terminal.setInsertMode(); - } else { - terminal.setReplaceMode(); - } - break; - case 20: - if (enabled) { - terminal.setNewLineMode(); - } else { - terminal.setLineFeedMode(); - } - break; - default: - terminal.debug.onError('unsupported mode: $mode'); - return; - } -} - -void csiDecSetMode(int mode, bool enabled, Terminal terminal) { - switch (mode) { - case 1: - terminal.setApplicationCursorKeys(enabled); - break; - // case "?3": - // if (enabled) { - // // DECCOLM - COLumn mode, 132 characters per line - // terminal.setSize(132, uint(lines)); - // } else { - // // DECCOLM - 80 characters per line (erases screen) - // terminal.setSize(80, uint(lines)); - // } - // terminal.clear(); - // case "?4": - // // DECSCLM - case 5: - // DECSCNM - terminal.setScreenMode(enabled); - break; - case 6: - // DECOM - terminal.setOriginMode(enabled); - break; - case 7: - //DECAWM - terminal.setAutoWrapMode(enabled); - break; - case 9: - if (enabled) { - // terminal.setMouseMode(MouseMode.x10); - } else { - terminal.setMouseMode(MouseMode.none); - } - break; - case 12: - case 13: - terminal.setBlinkingCursor(enabled); - break; - case 25: - terminal.setShowCursor(enabled); - break; - case 47: - case 1047: - if (enabled) { - terminal.useAltBuffer(); - } else { - terminal.useMainBuffer(); - } - break; - case 1000: - case 10061000: - // enable mouse tracking - // 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31 - if (enabled) { - // terminal.setMouseMode(MouseMode.vt200); - } else { - terminal.setMouseMode(MouseMode.none); - } - break; - case 1002: - // enable mouse tracking - // 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31 - if (enabled) { - // terminal.setMouseMode(MouseMode.buttonEvent); - } else { - terminal.setMouseMode(MouseMode.none); - } - break; - case 1003: - if (enabled) { - // terminal.setMouseMode(MouseMode.anyEvent); - } else { - terminal.setMouseMode(MouseMode.none); - } - break; - case 1005: - if (enabled) { - // terminal.setMouseExtMode(MouseExt.utf); - } else { - // terminal.setMouseExtMode(MouseExt.none); - } - break; - case 1006: - if (enabled) { - // terminal.setMouseExtMode(MouseExt.sgr); - } else { - // terminal.setMouseExtMode(MouseExt.none); - } - break; - case 1048: - if (enabled) { - terminal.buffer.saveCursor(); - } else { - terminal.buffer.restoreCursor(); - } - break; - case 1049: - if (enabled) { - terminal.useAltBuffer(); - terminal.buffer.clear(); - } else { - terminal.useMainBuffer(); - } - break; - case 2004: - terminal.setBracketedPasteMode(enabled); - break; - default: - terminal.debug.onError('unsupported mode: $mode'); - return; - } -} diff --git a/lib/terminal/osc.dart b/lib/terminal/osc.dart deleted file mode 100644 index ec08a5b4..00000000 --- a/lib/terminal/osc.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:collection'; - -import 'package:xterm/terminal/terminal.dart'; - -// bool _isOscTerminator(int codePoint) { -// final terminator = {0x07, 0x00}; -// // final terminator = {0x07, 0x5c}; -// return terminator.contains(codePoint); -// } - -List? _parseOsc(Queue queue, Set terminators) { - // TODO: add tests for cases such as incomplete sequence. - - final params = []; - final param = StringBuffer(); - - // Keep track of how many characters should be taken from the queue. - var readOffset = 0; - - while (true) { - // The sequence isn't completed, just ignore it. - if (queue.length <= readOffset) { - return null; - } - - final char = queue.elementAt(readOffset++); - - // final char = queue.removeFirst(); - - if (terminators.contains(char)) { - params.add(param.toString()); - break; - } - - const semicolon = 59; - if (char == semicolon) { - params.add(param.toString()); - param.clear(); - continue; - } - - param.writeCharCode(char); - } - - // The sequence is complete. So we consume it from the queue. - for (var i = 0; i < readOffset; i++) { - queue.removeFirst(); - } - - return params; -} - -/// OSC - Operating System Command: sequence starting with ESC ] (7bit) or OSC -/// (\x9D, 8bit) -bool oscHandler(Queue queue, Terminal terminal) { - final params = _parseOsc(queue, terminal.platform.oscTerminators); - - if (params == null) { - return false; - } - - terminal.debug.onOsc(params); - - if (params.isEmpty) { - terminal.debug.onError('osc with no params'); - return true; - } - - if (params.length < 2) { - return true; - } - - final ps = params[0]; - final pt = params[1]; - - switch (ps) { - case '0': - case '2': - terminal.onTitleChange(pt); - break; - case '1': - terminal.onIconChange(pt); - break; - default: - terminal.debug.onError('unknown osc ps: $ps'); - } - - return true; -} diff --git a/lib/terminal/platform.dart b/lib/terminal/platform.dart deleted file mode 100644 index ff830595..00000000 --- a/lib/terminal/platform.dart +++ /dev/null @@ -1,26 +0,0 @@ -class PlatformBehavior { - const PlatformBehavior({ - required this.oscTerminators, - required this.useMacInputBehavior, - }); - - final Set oscTerminators; - final bool useMacInputBehavior; -} - -class PlatformBehaviors { - static const mac = PlatformBehavior( - oscTerminators: {0x07, 0x5c}, - useMacInputBehavior: true, - ); - - static const unix = PlatformBehavior( - oscTerminators: {0x07, 0x5c}, - useMacInputBehavior: false, - ); - - static const windows = PlatformBehavior( - oscTerminators: {0x07, 0x00}, - useMacInputBehavior: false, - ); -} diff --git a/lib/terminal/sbc.dart b/lib/terminal/sbc.dart deleted file mode 100644 index 2ff74a66..00000000 --- a/lib/terminal/sbc.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:xterm/terminal/terminal.dart'; - -typedef SbcHandler = void Function(int, Terminal); - -const sbcMaxCodePoint = 0x0f; - -final sbcHandlers = _sbcHandlers(); - -// Build single byte character lookup table -List _sbcHandlers() { - final result = List.filled(sbcMaxCodePoint + 1, null); - result[0x05] = _voidHandler; - result[0x07] = _bellHandler; - result[0x08] = _backspaceReturnHandler; - result[0x09] = _tabHandler; - result[0x0a] = _newLineHandler; - result[0x0b] = _newLineHandler; - result[0x0c] = _newLineHandler; - result[0x0d] = _carriageReturnHandler; - result[0x0e] = _shiftOutHandler; - result[0x0f] = _shiftInHandler; - return result; -} - -void _bellHandler(int code, Terminal terminal) { - terminal.onBell(); -} - -void _voidHandler(int code, Terminal terminal) { - // unsupported. -} - -void _newLineHandler(int code, Terminal terminal) { - terminal.buffer.newLine(); -} - -void _carriageReturnHandler(int code, Terminal terminal) { - terminal.buffer.carriageReturn(); -} - -void _backspaceReturnHandler(int code, Terminal terminal) { - terminal.buffer.backspace(); -} - -void _shiftOutHandler(int code, Terminal terminal) { - terminal.buffer.charset.use(1); -} - -void _shiftInHandler(int code, Terminal terminal) { - terminal.buffer.charset.use(0); -} - -void _tabHandler(int code, Terminal terminal) { - terminal.tab(); -} diff --git a/lib/terminal/sgr.dart b/lib/terminal/sgr.dart deleted file mode 100644 index b0ae5bc8..00000000 --- a/lib/terminal/sgr.dart +++ /dev/null @@ -1,333 +0,0 @@ -import 'package:xterm/buffer/cell_flags.dart'; -import 'package:xterm/theme/terminal_color.dart'; -import 'package:xterm/terminal/csi.dart'; -import 'package:xterm/terminal/terminal.dart'; -import 'package:xterm/util/lookup_table.dart'; - -/// SGR selects one or more character attributes at the same time. -/// Multiple params (up to 32) are applied from in order from left to right. -/// The changed attributes are applied to all new characters received. -/// If you move characters in the viewport by scrolling or any other means, -/// then the attributes move with the characters. -void sgrHandler(CSI csi, Terminal terminal) { - final params = csi.params.toList(); - - if (params.isEmpty) { - params.add(0); - } - - for (var i = 0; i < params.length; i++) { - final param = params[i]; - switch (param) { - case 0: - terminal.cursor.fg = terminal.theme.foreground; - terminal.cursor.bg = TerminalColor.transparent; - terminal.cursor.flags = 0x00; - break; - case 1: - terminal.cursor.flags |= CellFlags.bold; - break; - case 2: - terminal.cursor.flags |= CellFlags.faint; - break; - case 3: - terminal.cursor.flags |= CellFlags.italic; - break; - case 4: - terminal.cursor.flags |= CellFlags.underline; - break; - case 5: - terminal.cursor.flags |= CellFlags.blink; - break; - case 7: - terminal.cursor.flags |= CellFlags.inverse; - break; - case 8: - terminal.cursor.flags |= CellFlags.invisible; - break; - case 21: - terminal.cursor.flags &= ~CellFlags.bold; - break; - case 22: - terminal.cursor.flags &= ~CellFlags.faint; - break; - case 23: - terminal.cursor.flags &= ~CellFlags.italic; - break; - case 24: - terminal.cursor.flags &= ~CellFlags.underline; - break; - case 25: - terminal.cursor.flags &= ~CellFlags.blink; - break; - case 27: - terminal.cursor.flags &= ~CellFlags.inverse; - break; - case 28: - terminal.cursor.flags &= ~CellFlags.invisible; - break; - case 29: - // not strikethrough - break; - case 39: - terminal.cursor.fg = terminal.theme.foreground; - break; - case 30: - terminal.cursor.fg = terminal.theme.black; - break; - case 31: - terminal.cursor.fg = terminal.theme.red; - break; - case 32: - terminal.cursor.fg = terminal.theme.green; - break; - case 33: - terminal.cursor.fg = terminal.theme.yellow; - break; - case 34: - terminal.cursor.fg = terminal.theme.blue; - break; - case 35: - terminal.cursor.fg = terminal.theme.magenta; - break; - case 36: - terminal.cursor.fg = terminal.theme.cyan; - break; - case 37: - terminal.cursor.fg = terminal.theme.white; - break; - case 90: - terminal.cursor.fg = terminal.theme.brightBlack; - break; - case 91: - terminal.cursor.fg = terminal.theme.brightRed; - break; - case 92: - terminal.cursor.fg = terminal.theme.brightGreen; - break; - case 93: - terminal.cursor.fg = terminal.theme.brightYellow; - break; - case 94: - terminal.cursor.fg = terminal.theme.brightBlue; - break; - case 95: - terminal.cursor.fg = terminal.theme.brightMagenta; - break; - case 96: - terminal.cursor.fg = terminal.theme.brightCyan; - break; - case 97: - terminal.cursor.fg = terminal.theme.brightWhite; - break; - case 49: - terminal.cursor.bg = terminal.theme.background; - break; - case 40: - terminal.cursor.bg = terminal.theme.black; - break; - case 41: - terminal.cursor.bg = terminal.theme.red; - break; - case 42: - terminal.cursor.bg = terminal.theme.green; - break; - case 43: - terminal.cursor.bg = terminal.theme.yellow; - break; - case 44: - terminal.cursor.bg = terminal.theme.blue; - break; - case 45: - terminal.cursor.bg = terminal.theme.magenta; - break; - case 46: - terminal.cursor.bg = terminal.theme.cyan; - break; - case 47: - terminal.cursor.bg = terminal.theme.white; - break; - case 100: - terminal.cursor.bg = terminal.theme.brightBlack; - break; - case 101: - terminal.cursor.bg = terminal.theme.brightRed; - break; - case 102: - terminal.cursor.bg = terminal.theme.brightGreen; - break; - case 103: - terminal.cursor.bg = terminal.theme.brightYellow; - break; - case 104: - terminal.cursor.bg = terminal.theme.brightBlue; - break; - case 105: - terminal.cursor.bg = terminal.theme.brightMagenta; - break; - case 106: - terminal.cursor.bg = terminal.theme.brightCyan; - break; - case 107: - terminal.cursor.bg = terminal.theme.brightWhite; - break; - case 38: // set foreground - final colorResult = parseAnsiColour(params, i, terminal); - terminal.cursor.fg = colorResult[0]; - i += colorResult[1]; - break; - case 48: // set background - final colorResult = parseAnsiColour(params, i, terminal); - terminal.cursor.bg = colorResult[0]; - i += colorResult[1]; - break; - default: - terminal.debug.onError('unknown SGR: $param'); - } - } -} - -/// parse a color from [params] starting from [offset]. -/// Returns a list with 2 entries. Index 0 = color, Index 1 = number of params used -List parseAnsiColour(List params, int offset, Terminal terminal) { - final length = params.length - offset; - - if (length > 2) { - switch (params[offset + 1]) { - case 5: - // 8 bit colour - final colNum = params[offset + 2]; - - if (colNum >= 256 || colNum < 0) { - return [TerminalColor.empty(), 2]; - } - - return [parse8BitSgrColour(colNum, terminal), 2]; - - case 2: - if (length < 4) { - return [TerminalColor.empty(), 0]; - } - - // 24 bit colour - if (length == 5) { - final r = params[offset + 2]; - final g = params[offset + 3]; - final b = params[offset + 4]; - return [TerminalColor.fromARGB(0xff, r, g, b), 4]; - } - - if (length > 5) { - // ISO/IEC International Standard 8613-6 - final r = params[offset + 3]; - final g = params[offset + 4]; - final b = params[offset + 5]; - return [TerminalColor.fromARGB(0xff, r, g, b), 5]; - } - } - } - - return [TerminalColor.empty(), 0]; -} - -final grayscaleColors = FastLookupTable({ - 232: 0xff080808, - 233: 0xff121212, - 234: 0xff1c1c1c, - 235: 0xff262626, - 236: 0xff303030, - 237: 0xff3a3a3a, - 238: 0xff444444, - 239: 0xff4e4e4e, - 240: 0xff585858, - 241: 0xff626262, - 242: 0xff6c6c6c, - 243: 0xff767676, - 244: 0xff808080, - 245: 0xff8a8a8a, - 246: 0xff949494, - 247: 0xff9e9e9e, - 248: 0xffa8a8a8, - 249: 0xffb2b2b2, - 250: 0xffbcbcbc, - 251: 0xffc6c6c6, - 252: 0xffd0d0d0, - 253: 0xffdadada, - 254: 0xffe4e4e4, - 255: 0xffeeeeee, -}); - -int parse8BitSgrColour(int colNum, Terminal terminal) { - // https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit - - switch (colNum) { - case 0: - return terminal.theme.black; - case 1: - return terminal.theme.red; - case 2: - return terminal.theme.green; - case 3: - return terminal.theme.yellow; - case 4: - return terminal.theme.blue; - case 5: - return terminal.theme.magenta; - case 6: - return terminal.theme.cyan; - case 7: - return terminal.theme.white; - case 8: - return terminal.theme.brightBlack; - case 9: - return terminal.theme.brightRed; - case 10: - return terminal.theme.brightGreen; - case 11: - return terminal.theme.brightYellow; - case 12: - return terminal.theme.brightBlue; - case 13: - return terminal.theme.brightMagenta; - case 14: - return terminal.theme.brightCyan; - case 15: - return terminal.theme.white; - } - - if (colNum < 232) { - var r = 0; - var g = 0; - var b = 0; - - final index = colNum - 16; - - for (var i = 0; i < index; i++) { - if (b == 0) { - b = 95; - } else if (b < 255) { - b += 40; - } else { - b = 0; - if (g == 0) { - g = 95; - } else if (g < 255) { - g += 40; - } else { - g = 0; - if (r == 0) { - r = 95; - } else if (r < 255) { - r += 40; - } else { - break; - } - } - } - } - - return TerminalColor.fromARGB(0xff, r, g, b); - } - - return grayscaleColors[colNum.clamp(232, 255)]!; -} diff --git a/lib/terminal/tabs.dart b/lib/terminal/tabs.dart deleted file mode 100644 index f02f1e84..00000000 --- a/lib/terminal/tabs.dart +++ /dev/null @@ -1,28 +0,0 @@ -class Tabs { - final _stops = {}; - - void setAt(int index) { - _stops.add(index); - } - - void clearAt(int index) { - _stops.remove(index); - } - - void clearAll() { - _stops.clear(); - } - - bool isSetAt(int index) { - return _stops.contains(index); - } - - void reset() { - clearAll(); - const maxTabs = 1024; - const tabLength = 4; - for (var i = 0; i < maxTabs; i += tabLength) { - setAt(i); - } - } -} diff --git a/lib/terminal/terminal.dart b/lib/terminal/terminal.dart deleted file mode 100644 index 40e94b9e..00000000 --- a/lib/terminal/terminal.dart +++ /dev/null @@ -1,808 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:math' show max, min; - -import 'package:xterm/buffer/buffer.dart'; -import 'package:xterm/buffer/line/line.dart'; -import 'package:xterm/input/keys.dart'; -import 'package:xterm/input/keytab/keytab.dart'; -import 'package:xterm/input/keytab/keytab_escape.dart'; -import 'package:xterm/input/keytab/keytab_record.dart'; -import 'package:xterm/mouse/mouse_mode.dart'; -import 'package:xterm/mouse/position.dart'; -import 'package:xterm/mouse/selection.dart'; -import 'package:xterm/terminal/ansi.dart'; -import 'package:xterm/terminal/cursor.dart'; -import 'package:xterm/terminal/platform.dart'; -import 'package:xterm/terminal/sbc.dart'; -import 'package:xterm/terminal/tabs.dart'; -import 'package:xterm/terminal/terminal_backend.dart'; -import 'package:xterm/terminal/terminal_search.dart'; -import 'package:xterm/terminal/terminal_search_interaction.dart'; -import 'package:xterm/terminal/terminal_ui_interaction.dart'; -import 'package:xterm/theme/terminal_color.dart'; -import 'package:xterm/theme/terminal_theme.dart'; -import 'package:xterm/theme/terminal_themes.dart'; -import 'package:xterm/util/constants.dart'; -import 'package:xterm/util/debug_handler.dart'; -import 'package:xterm/util/observable.dart'; - -typedef TerminalInputHandler = void Function(String); -typedef BellHandler = void Function(); -typedef TitleChangeHandler = void Function(String); -typedef IconChangeHandler = void Function(String); - -void _defaultBellHandler() {} -void _defaultTitleHandler(String _) {} -void _defaultIconHandler(String _) {} - -class Terminal - with Observable - implements TerminalUiInteraction, TerminalSearchInteraction { - Terminal({ - this.backend, - this.onBell = _defaultBellHandler, - this.onTitleChange = _defaultTitleHandler, - this.onIconChange = _defaultIconHandler, - this.platform = PlatformBehaviors.unix, - this.theme = TerminalThemes.defaultTheme, - required int maxLines, - }) : _maxLines = maxLines { - _search = TerminalSearch(this); - _userSearchTask = _search.createSearchTask("UserSearch"); - backend?.init(); - backend?.exitCode.then((value) { - _isTerminated = true; - _backendExited.complete(value); - }); - backend?.out.listen(write); - _mainBuffer = Buffer(terminal: this, isAltBuffer: false); - _altBuffer = Buffer(terminal: this, isAltBuffer: true); - _buffer = _mainBuffer; - - cursor = Cursor( - fg: theme.foreground, - bg: TerminalColor.transparent, // transparent - flags: 0x00, // no flags - ); - - tabs.reset(); - } - - late TerminalSearch _search; - late TerminalSearchTask _userSearchTask; - - bool _dirty = false; - @override - bool get dirty { - if (_dirty) { - _dirty = false; - return true; - } else { - return false; - } - } - - int _maxLines; - int get maxLines { - return max(viewHeight, _maxLines); - } - - int _viewWidth = 80; - int _viewHeight = 25; - - int get viewWidth => _viewWidth; - int get viewHeight => _viewHeight; - - int get visibleHeight => min(_viewHeight, buffer.height); - @override - int get invisibleHeight => buffer.height - visibleHeight; - - /// ### Insert/Replace Mode (IRM) - /// - /// The terminal displays received characters at the cursor position. - /// Insert/Replace mode determines how the terminal adds characters to the - /// screen. Insert mode displays the new character and moves previously - /// displayed characters to the right. Replace mode adds characters by - /// replacing the character at the cursor position. - /// - /// You can set or reset insert/replace mode as follows. - // ignore: unused_field - bool _replaceMode = true; - - // ignore: unused_field - bool _screenMode = false; // DECSCNM (black on white background) - bool _autoWrapMode = true; - bool get autoWrapMode => _autoWrapMode; - - /// ### DECOM – Origin Mode (DEC Private) - /// - /// This is a private parameter applicable to set mode (SM) and reset mode - /// (RM) control sequences. The reset state causes the origin to be at the - /// upper-left character position on the screen. Line and column numbers are, - /// therefore, independent of current margin settings. The cursor may be - /// positioned outside the margins with a cursor position (CUP) or horizontal - /// and vertical position (HVP) control. - /// - /// The set state causes the origin to be at the upper-left character position - /// within the margins. Line and column numbers are therefore relative to the - /// current margin settings. The cursor is not allowed to be positioned - /// outside the margins. - /// - /// The cursor is moved to the new home position when this mode is set or - /// reset. - /// - /// Lines and columns are numbered consecutively, with the origin being line - /// 1, column 1. - bool get originMode => _originMode; - bool _originMode = false; - - /// ### LNM – Line Feed/New Line Mode - /// - /// This is a parameter applicable to set mode (SM) and reset mode (RM) - /// control sequences. The reset state causes the interpretation of the line - /// feed (LF), defined in ANSI Standard X3.4-1977, to imply only vertical - /// movement of the active position and causes the RETURN key (CR) to send the - /// single code CR. The set state causes the LF to imply movement to the first - /// position of the following line and causes the RETURN key to send the two - /// codes (CR, LF). This is the New Line (NL) option. - /// - /// This mode does not affect the index (IND), or next line (NEL) format - /// effectors. - bool get lineFeedMode => _lineFeedMode; - bool _lineFeedMode = true; - - /// See: [lineFeedMode] - bool get newLineMode => !_lineFeedMode; - - /// ### Bracketed Paste Mode - /// - /// When bracketed paste mode is set, pasted text is bracketed with control - /// sequences so that the program can differentiate pasted text from typed-in - /// text. When bracketed paste mode is set, the program will receive: `ESC - /// [200 ~`, followed by the pasted text, followed by `ESC [ 201 ~`. - bool get bracketedPasteMode => _bracketedPasteMode; - bool _bracketedPasteMode = false; - - bool _showCursor = true; - @override - bool get showCursor => _showCursor; - - /// DECCKM – Cursor Keys Mode (DEC Private) - /// - /// This is a private parameter applicable to set mode (SM) and reset mode - /// (RM) control sequences. This mode is only effective when the terminal is - /// in keypad application mode (see DECKPAM) and the ANSI/VT52 mode (DECANM) - /// is set (see DECANM). Under these conditions, if the cursor key mode is - /// reset, the four cursor function keys will send ANSI cursor control - /// commands. If cursor key mode is set, the four cursor function keys will - /// send application functions. - bool get applicationCursorKeys => _applicationCursorKeys; - bool _applicationCursorKeys = false; - - bool _blinkingCursor = true; - bool get blinkingCursor => _blinkingCursor; - - late Buffer _buffer; - late Buffer _mainBuffer; - late Buffer _altBuffer; - - /// Queue of input characters. addLast() to add, removeFirst() to consume. - final _queue = ListQueue(81920); - - bool _slowMotion = false; - bool get slowMotion => _slowMotion; - - MouseMode _mouseMode = MouseMode.none; - MouseMode get mouseMode => _mouseMode; - - @override - final TerminalTheme theme; - - // final cellAttr = CellAttrTemplate(); - late final Cursor cursor; - - final keytab = Keytab.defaultKeytab(); - final _selection = Selection(); - final tabs = Tabs(); - final debug = DebugHandler(); - - final TerminalBackend? backend; - final BellHandler onBell; - final TitleChangeHandler onTitleChange; - final IconChangeHandler onIconChange; - @override - final PlatformBehavior platform; - - Buffer get buffer { - return _buffer; - } - - @override - int get cursorX => buffer.cursorX; - @override - int get cursorY => buffer.cursorY; - - @override - void setScrollOffsetFromBottom(int scrollOffset) { - final oldOffset = _buffer.scrollOffsetFromBottom; - _buffer.setScrollOffsetFromBottom(scrollOffset); - if (oldOffset != scrollOffset) { - _dirty = true; - refresh(); - } - } - - /// Writes data to the terminal. Terminal sequences and special characters are - /// interpreted. - /// - /// See also: [Buffer.write] - @override - void write(String text) { - _queue.addAll(text.runes); - _processInput(); - backend?.ackProcessed(); - refresh(); - } - - /// Writes data to the terminal. Special characters are interpreted. - /// - /// See also: [Buffer.writeChar] - void writeChar(int codePoint) { - _queue.addLast(codePoint); - _processInput(); - refresh(); - } - - @override - List getVisibleLines() { - return _buffer.getVisibleLines(); - } - - void _processInput() { - while (_queue.isNotEmpty) { - // if (_slowMotion) { - // await Future.delayed(Duration(milliseconds: 100)); - // } - - const esc = 0x1b; - final char = _queue.removeFirst(); - - if (char == esc) { - final finished = ansiHandler(_queue, this); - - // Terminal sequence in the queue is not completed, and no charater is - // consumed. - if (!finished) { - _queue.addFirst(esc); - break; - } - - continue; - } - - _processChar(char); - } - } - - void _processChar(int codePoint) { - // If the character doesn't have special effect. Write it directly to the - // buffer. - if (codePoint > sbcMaxCodePoint) { - debug.onChar(codePoint); - _buffer.writeChar(codePoint); - return; - } - - // The character may have special effect. - final sbcHandler = sbcHandlers[codePoint]; - if (sbcHandler != null) { - debug.onSbc(codePoint); - sbcHandler(codePoint, this); - } - } - - @override - void refresh() { - _dirty = true; - notifyListeners(); - } - - void setSlowMotion(bool enabled) { - _slowMotion = enabled; - } - - void setOriginMode(bool enabled) { - _originMode = enabled; - buffer.setPosition(0, 0); - } - - void setScreenMode(bool enabled) { - _screenMode = true; - } - - void setApplicationCursorKeys(bool enabled) { - _applicationCursorKeys = enabled; - } - - void setShowCursor(bool showCursor) { - _showCursor = showCursor; - } - - void setBlinkingCursor(bool enabled) { - _blinkingCursor = enabled; - } - - void setAutoWrapMode(bool enabled) { - _autoWrapMode = enabled; - } - - void setBracketedPasteMode(bool enabled) { - _bracketedPasteMode = enabled; - } - - void setInsertMode() { - _replaceMode = false; - } - - void setReplaceMode() { - _replaceMode = true; - } - - void setNewLineMode() { - _lineFeedMode = false; - } - - void setLineFeedMode() { - _lineFeedMode = true; - } - - void setMouseMode(MouseMode mode) { - _mouseMode = mode; - } - - void useMainBuffer() { - _buffer = _mainBuffer; - } - - void useAltBuffer() { - _buffer = _altBuffer; - } - - bool isUsingMainBuffer() { - return _buffer == _mainBuffer; - } - - bool isUsingAltBuffer() { - return _buffer == _altBuffer; - } - - /// 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. - @override - void resize( - int newWidth, int newHeight, int newPixelWidth, int newPixelHeight) { - backend?.resize(newWidth, newHeight, newPixelWidth, newPixelHeight); - newWidth = max(newWidth, 1); - newHeight = max(newHeight, 1); - - final oldWidth = _viewWidth; - final oldHeight = _viewHeight; - _viewWidth = newWidth; - _viewHeight = newHeight; - - //we need to resize both buffers so that they are ready when we switch between them - _altBuffer.resize(oldWidth, oldHeight, newWidth, newHeight); - _mainBuffer.resize(oldWidth, oldHeight, newWidth, newHeight); - - if (buffer == _altBuffer) { - buffer.clearScrollback(); - } - - _altBuffer.resetVerticalMargins(); - _mainBuffer.resetVerticalMargins(); - } - - @override - void keyInput( - TerminalKey key, { - bool ctrl = false, - bool alt = false, - bool shift = false, - bool mac = false, - // bool meta, - String? character, - }) { - debug.onMsg(key); - - for (var record in keytab.records) { - if (record.key != key) { - continue; - } - - 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.anyModifier == true && - (ctrl != true && alt != true && shift != true)) { - continue; - } - - if (record.anyModifier == false && - !(ctrl != true && alt != true && shift != true)) { - continue; - } - - if (record.appScreen != null && record.appScreen != isUsingAltBuffer()) { - continue; - } - - if (record.newLine != null && record.newLine != newLineMode) { - continue; - } - - if (record.appCursorKeys != null && - record.appCursorKeys != applicationCursorKeys) { - continue; - } - - if (record.mac != null && record.mac != mac) { - continue; - } - - // TODO: support VT52 - if (record.ansi == false) { - continue; - } - - if (record.action.type == KeytabActionType.input) { - debug.onMsg('input: ${record.action.value}'); - final input = keytabUnescape(record.action.value); - backend?.write(input); - return; - } - } - - if (ctrl) { - if (key.index >= TerminalKey.keyA.index && - key.index <= TerminalKey.keyZ.index) { - final input = key.index - TerminalKey.keyA.index + 1; - backend?.write(String.fromCharCode(input)); - return; - } - } - - if (alt) { - if (key.index >= TerminalKey.keyA.index && - key.index <= TerminalKey.keyZ.index) { - final charCode = key.index - TerminalKey.keyA.index + 65; - - // only process ALT + Key when this combination has no other meaning - // (reflected in the given character argument - if (character == null || - character.toLowerCase() == - String.fromCharCode(charCode).toLowerCase()) { - final input = [0x1b, charCode]; - backend?.write(String.fromCharCodes(input)); - } - return; - } - } - } - - void selectWordOrRow(Position position) { - if (position.y > buffer.lines.length) { - return; - } - - final row = position.y; - - final line = buffer.lines[row]; - - final positionIsInSelection = _selection.contains(position); - final completeLineIsSelected = - _selection.start?.x == 0 && _selection.end?.x == terminalWidth; - - if (positionIsInSelection && !completeLineIsSelected) { - // select area on an already existing selection extends it to the full line - _selection.clear(); - _selection.init(Position(0, row)); - _selection.update(Position(terminalWidth, row)); - } else { - // select the word that is under position - - var start = position.x; - var end = position.x; - - do { - if (start == 0) { - break; - } - final content = line.cellGetContent(start - 1); - if (kWordSeparators.contains(String.fromCharCode(content))) { - break; - } - start--; - } while (true); - do { - if (end >= terminalWidth - 1) { - break; - } - final content = line.cellGetContent(end + 1); - if (kWordSeparators.contains(String.fromCharCode(content))) { - break; - } - end++; - } while (true); - - _selection.clear(); - _selection.init(Position(start, row)); - _selection.update(Position(end, row)); - refresh(); - } - } - - String? getSelectedText() { - if (_selection.isEmpty) { - return null; - } - - final builder = StringBuffer(); - - for (var row = _selection.start!.y; row <= _selection.end!.y; row++) { - if (row >= buffer.height) { - break; - } - - final line = buffer.lines[row]; - - var xStart = 0; - var xEnd = viewWidth - 1; - - if (row == _selection.start!.y) { - xStart = _selection.start!.x; - } else if (!line.isWrapped) { - builder.write("\n"); - } - - if (row == _selection.end!.y) { - xEnd = _selection.end!.x; - } - - for (var col = xStart; col <= xEnd; col++) { - // if (col >= line.length) { - // break; - // } - - if (line.cellGetWidth(col) == 0) { - continue; - } - - var char = line.cellGetContent(col); - - if (char == 0x00) { - const blank = 32; - char = blank; - } - - builder.writeCharCode(char); - } - } - - return builder.toString(); - } - - @override - void paste(String data) { - if (bracketedPasteMode) { - data = '\x1b[200~$data\x1b[201~'; - } - - backend?.write(data); - } - - int get _tabIndexFromCursor { - var index = buffer.cursorX; - - if (buffer.cursorX == viewWidth) { - index = 0; - } - - return index; - } - - void tabSetAtCursor() { - tabs.setAt(_tabIndexFromCursor); - } - - void tabClearAtCursor() { - tabs.clearAt(_tabIndexFromCursor); - } - - void tab() { - while (buffer.cursorX < viewWidth) { - buffer.write(' '); - - if (tabs.isSetAt(buffer.cursorX)) { - break; - } - } - } - - @override - int get backgroundColor => theme.background; - - @override - int get bufferHeight => buffer.height; - - @override - void clearSelection() { - selection?.clear(); - } - - @override - int convertViewLineToRawLine(int viewLine) { - if (viewHeight > buffer.height) { - return viewLine; - } - - return viewLine + (buffer.height - viewHeight); - } - - @override - BufferLine? get currentLine => buffer.currentLine; - - @override - int get cursorColor => theme.cursor; - - @override - String? get selectedText => getSelectedText(); - - @override - bool get isReady => true; - - @override - void onMouseTap(Position position) { - mouseMode.onTap(this, position); - } - - @override - onMouseDoubleTap(Position position) { - mouseMode.onDoubleTap(this, position); - } - - @override - void onPanStart(Position position) { - mouseMode.onPanStart(this, position); - } - - @override - void onPanUpdate(Position position) { - mouseMode.onPanUpdate(this, position); - } - - @override - int get scrollOffsetFromBottom => buffer.scrollOffsetFromBottom; - - @override - int get scrollOffsetFromTop => buffer.scrollOffsetFromTop; - - @override - int get terminalHeight => viewHeight; - - @override - int get terminalWidth => viewWidth; - - @override - Selection? get selection => _selection; - - @override - void raiseOnInput(String input) { - backend?.write(input); - } - - final _backendExited = Completer(); - @override - Future get backendExited => _backendExited.future; - - var _isTerminated = false; - - @override - void terminateBackend() { - if (_isTerminated) { - return; - } - _isTerminated = true; - backend?.terminate(); - } - - @override - bool get isTerminated => _isTerminated; - - @override - void selectAll() { - _selection.init(Position(0, 0)); - _selection.update(Position(terminalWidth, bufferHeight)); - refresh(); - } - - String _composingString = ''; - - @override - String get composingString => _composingString; - - @override - void updateComposingString(String value) { - _composingString = value; - refresh(); - } - - @override - TerminalSearchResult get userSearchResult => _userSearchTask.searchResult; - - @override - int get numberOfSearchHits => _userSearchTask.numberOfSearchHits; - - @override - int? get currentSearchHit => _userSearchTask.currentSearchHit; - - @override - set currentSearchHit(int? currentSearchHit) { - _userSearchTask.currentSearchHit = currentSearchHit; - _scrollCurrentHitIntoView(); - refresh(); - } - - @override - TerminalSearchOptions get userSearchOptions => _userSearchTask.options; - - @override - set userSearchOptions(TerminalSearchOptions options) { - _userSearchTask.options = options; - _scrollCurrentHitIntoView(); - refresh(); - } - - @override - String? get userSearchPattern => _userSearchTask.pattern; - - @override - set userSearchPattern(String? newValue) { - _userSearchTask.pattern = newValue; - _scrollCurrentHitIntoView(); - refresh(); - } - - @override - bool get isUserSearchActive => _userSearchTask.isActive; - - @override - set isUserSearchActive(bool isUserSearchActive) { - _userSearchTask.isActive = isUserSearchActive; - _scrollCurrentHitIntoView(); - refresh(); - } - - void _scrollCurrentHitIntoView() { - if (!_userSearchTask.isActive) { - return; - } - final currentHit = _userSearchTask.currentSearchHitObject; - - if (currentHit != null) { - final desiredScrollOffsetFromTop = - currentHit.startLineIndex + (terminalHeight / 2).floor(); - setScrollOffsetFromBottom(buffer.height - desiredScrollOffsetFromTop); - } - } -} diff --git a/lib/terminal/terminal_backend.dart b/lib/terminal/terminal_backend.dart deleted file mode 100644 index 400f640a..00000000 --- a/lib/terminal/terminal_backend.dart +++ /dev/null @@ -1,29 +0,0 @@ -/// interface for every Terminal backend -abstract class TerminalBackend { - /// initializes the backend - /// This can be used to instantiate instances that are problematic when - /// passed to a Isolate. - /// The [TerminalIsolate] will pass the backend to the [Terminal] that then - /// executes [init] from inside the Isolate. - /// So when your backend needs any complex instances (most of them will) - /// then strongly consider instantiating them here - void init(); - - /// Stream for data that gets read from the backend - Stream get out; - - /// Future that fires when the backend terminates - Future get exitCode; - - /// writes data to this backend - void write(String input); - - /// notifies the backend about a view port resize that happened - void resize(int width, int height, int pixelWidth, int pixelHeight); - - /// terminates this backend - void terminate(); - - /// acknowledges processing of a data junk - void ackProcessed(); -} diff --git a/lib/terminal/terminal_isolate.dart b/lib/terminal/terminal_isolate.dart deleted file mode 100644 index 58fde2a6..00000000 --- a/lib/terminal/terminal_isolate.dart +++ /dev/null @@ -1,666 +0,0 @@ -import 'dart:async'; -import 'dart:isolate'; - -import 'package:xterm/buffer/line/line.dart'; -import 'package:xterm/input/keys.dart'; -import 'package:xterm/mouse/position.dart'; -import 'package:xterm/mouse/selection.dart'; -import 'package:xterm/terminal/platform.dart'; -import 'package:xterm/terminal/terminal.dart'; -import 'package:xterm/terminal/terminal_backend.dart'; -import 'package:xterm/terminal/terminal_search.dart'; -import 'package:xterm/terminal/terminal_ui_interaction.dart'; -import 'package:xterm/theme/terminal_theme.dart'; -import 'package:xterm/theme/terminal_themes.dart'; -import 'package:xterm/util/event_debouncer.dart'; -import 'package:xterm/util/observable.dart'; - -enum _IsolateCommand { - sendPort, - init, - write, - refresh, - clearSelection, - selectAll, - mouseTap, - mouseDoubleTap, - mousePanStart, - mousePanUpdate, - setScrollOffsetFromBottom, - resize, - onInput, - keyInput, - requestNewStateWhenDirty, - paste, - terminateBackend, - updateComposingString, - updateSearchPattern, - updateSearchOptions, - updateCurrentSearchHit, - updateIsUserSearchActive, -} - -enum _IsolateEvent { - titleChanged, - iconChanged, - bell, - notifyChange, - newState, - exit, -} - -/// main entry for the terminal isolate -void terminalMain(SendPort port) async { - final rp = ReceivePort(); - port.send(rp.sendPort); - - Terminal? _terminal; - var _needNotify = true; - - await for (var msg in rp) { - // process incoming commands - final _IsolateCommand action = msg[0]; - switch (action) { - case _IsolateCommand.sendPort: - port = msg[1]; - break; - case _IsolateCommand.init: - final TerminalInitData initData = msg[1]; - _terminal = Terminal( - backend: initData.backend, - onTitleChange: (String title) { - port.send([_IsolateEvent.titleChanged, title]); - }, - onIconChange: (String icon) { - port.send([_IsolateEvent.iconChanged, icon]); - }, - onBell: () { - port.send([_IsolateEvent.bell]); - }, - platform: initData.platform, - theme: initData.theme, - maxLines: initData.maxLines); - _terminal.addListener(() { - if (_needNotify) { - port.send([_IsolateEvent.notifyChange]); - _needNotify = false; - } - }); - _terminal.backendExited - .then((value) => port.send([_IsolateEvent.exit, value])); - port.send([_IsolateEvent.notifyChange]); - break; - case _IsolateCommand.write: - _terminal?.write(msg[1]); - break; - case _IsolateCommand.refresh: - _terminal?.refresh(); - break; - case _IsolateCommand.clearSelection: - _terminal?.clearSelection(); - break; - case _IsolateCommand.selectAll: - _terminal?.selectAll(); - break; - case _IsolateCommand.mouseTap: - _terminal?.onMouseTap(msg[1]); - break; - case _IsolateCommand.mouseDoubleTap: - _terminal?.onMouseDoubleTap(msg[1]); - break; - case _IsolateCommand.mousePanStart: - _terminal?.onPanStart(msg[1]); - break; - case _IsolateCommand.mousePanUpdate: - _terminal?.onPanUpdate(msg[1]); - break; - case _IsolateCommand.setScrollOffsetFromBottom: - _terminal?.setScrollOffsetFromBottom(msg[1]); - break; - case _IsolateCommand.resize: - _terminal?.resize(msg[1], msg[2], msg[3], msg[4]); - break; - case _IsolateCommand.onInput: - _terminal?.backend?.write(msg[1]); - break; - case _IsolateCommand.keyInput: - _terminal?.keyInput( - msg[1], - ctrl: msg[2], - alt: msg[3], - shift: msg[4], - mac: msg[5], - character: msg[6], - ); - break; - case _IsolateCommand.requestNewStateWhenDirty: - if (_terminal == null) { - break; - } - if (_terminal.dirty) { - final newState = TerminalState( - _terminal.scrollOffsetFromBottom, - _terminal.scrollOffsetFromTop, - _terminal.buffer.height, - _terminal.invisibleHeight, - _terminal.viewHeight, - _terminal.viewWidth, - _terminal.selection!, - _terminal.getSelectedText(), - _terminal.theme.background, - _terminal.cursorX, - _terminal.cursorY, - _terminal.showCursor, - _terminal.theme.cursor, - _terminal - .getVisibleLines() - .map((bl) => BufferLine.withDataFrom(bl)) - .toList(growable: false), - _terminal.composingString, - _terminal.userSearchResult, - _terminal.userSearchPattern, - _terminal.userSearchOptions, - _terminal.isUserSearchActive, - ); - port.send([_IsolateEvent.newState, newState]); - _needNotify = true; - } - break; - case _IsolateCommand.paste: - _terminal?.paste(msg[1]); - break; - case _IsolateCommand.terminateBackend: - _terminal?.terminateBackend(); - break; - case _IsolateCommand.updateComposingString: - _terminal?.updateComposingString(msg[1]); - break; - case _IsolateCommand.updateSearchPattern: - _terminal?.userSearchPattern = msg[1]; - break; - case _IsolateCommand.updateSearchOptions: - _terminal?.userSearchOptions = msg[1]; - break; - case _IsolateCommand.updateCurrentSearchHit: - _terminal?.currentSearchHit = msg[1]; - break; - case _IsolateCommand.updateIsUserSearchActive: - _terminal?.isUserSearchActive = msg[1]; - break; - } - } -} - -/// This class holds the initialization data needed for the Terminal. -/// This data has to be passed from the UI Isolate where the TerminalIsolate -/// class gets instantiated into the Isolate that will run the Terminal. -class TerminalInitData { - PlatformBehavior platform; - TerminalTheme theme; - int maxLines; - TerminalBackend? backend; - - TerminalInitData(this.backend, this.platform, this.theme, this.maxLines); -} - -/// This class holds a complete TerminalState as needed by the UI. -/// The state held here is self-contained and has no dependencies to the source -/// Terminal. Therefore it can be safely transferred across Isolate boundaries. -class TerminalState { - int scrollOffsetFromTop; - int scrollOffsetFromBottom; - - int bufferHeight; - int invisibleHeight; - - int viewHeight; - int viewWidth; - - Selection selection; - String? selectedText; - - int backgroundColor; - - int cursorX; - int cursorY; - bool showCursor; - int cursorColor; - - List visibleLines; - - bool consumed = false; - - String composingString; - - TerminalSearchResult searchResult; - String? userSearchPattern; - TerminalSearchOptions userSearchOptions; - bool isUserSearchActive; - - TerminalState( - this.scrollOffsetFromBottom, - this.scrollOffsetFromTop, - this.bufferHeight, - this.invisibleHeight, - this.viewHeight, - this.viewWidth, - this.selection, - this.selectedText, - this.backgroundColor, - this.cursorX, - this.cursorY, - this.showCursor, - this.cursorColor, - this.visibleLines, - this.composingString, - this.searchResult, - this.userSearchPattern, - this.userSearchOptions, - this.isUserSearchActive, - ); -} - -void _defaultBellHandler() {} - -void _defaultTitleHandler(String _) {} - -void _defaultIconHandler(String _) {} - -/// The TerminalIsolate class hosts an Isolate that runs a Terminal. -/// It handles all the communication with and from the Terminal and implements -/// [TerminalUiInteraction] as well as the terminal and therefore can simply -/// be exchanged with a Terminal. -/// This class is the preferred use of a Terminal as the Terminal logic and all -/// the communication with the backend are happening outside the UI thread. -/// -/// There is a special constraints in using this class: -/// The given backend has to be built so that it can be passed into an Isolate. -/// -/// This means in particular that it is not allowed to have any closures in its -/// object graph. -/// It is a good idea to move as much instantiation as possible into the -/// [TerminalBackend.init] method that gets called after the backend instance -/// has been passed and is therefore allowed to instantiate parts of the object -/// graph that do contain closures. -class TerminalIsolate with Observable implements TerminalUiInteraction { - final _receivePort = ReceivePort(); - SendPort? _sendPort; - late Isolate _isolate; - bool _isStarted = false; - - final TerminalBackend? backend; - final BellHandler onBell; - final TitleChangeHandler onTitleChange; - final IconChangeHandler onIconChange; - final PlatformBehavior _platform; - - @override - final TerminalTheme theme; - final int maxLines; - - final Duration minRefreshDelay; - final EventDebouncer _refreshEventDebouncer; - - TerminalState? _lastState; - - TerminalState? get lastState { - return _lastState; - } - - TerminalIsolate({ - this.backend, - this.onBell = _defaultBellHandler, - this.onTitleChange = _defaultTitleHandler, - this.onIconChange = _defaultIconHandler, - PlatformBehavior platform = PlatformBehaviors.unix, - this.theme = TerminalThemes.defaultTheme, - this.minRefreshDelay = const Duration(milliseconds: 16), - required this.maxLines, - }) : _platform = platform, - _refreshEventDebouncer = EventDebouncer(minRefreshDelay); - - @override - int get scrollOffsetFromBottom { - _assertStarted(); - return _lastState!.scrollOffsetFromBottom; - } - - @override - int get scrollOffsetFromTop { - _assertStarted(); - return _lastState!.scrollOffsetFromTop; - } - - @override - int get bufferHeight { - _assertStarted(); - return _lastState!.bufferHeight; - } - - @override - int get terminalHeight { - _assertStarted(); - return _lastState!.viewHeight; - } - - @override - int get terminalWidth { - _assertStarted(); - return _lastState!.viewWidth; - } - - @override - int get invisibleHeight { - _assertStarted(); - return _lastState!.invisibleHeight; - } - - @override - Selection? get selection { - return _lastState?.selection; - } - - @override - bool get showCursor { - return _lastState?.showCursor ?? true; - } - - @override - List getVisibleLines() { - if (_lastState == null) { - return List.empty(); - } - return _lastState!.visibleLines; - } - - @override - int get cursorY { - return _lastState?.cursorY ?? 0; - } - - @override - int get cursorX { - return _lastState?.cursorX ?? 0; - } - - @override - BufferLine? get currentLine { - if (_lastState == null) { - return null; - } - - int visibleLineIndex = - _lastState!.cursorY - _lastState!.scrollOffsetFromTop; - if (visibleLineIndex < 0) { - visibleLineIndex = _lastState!.cursorY; - } - return _lastState!.visibleLines[visibleLineIndex]; - } - - @override - int get cursorColor { - return _lastState?.cursorColor ?? 0; - } - - @override - int get backgroundColor { - return _lastState?.backgroundColor ?? 0; - } - - @override - bool get dirty { - if (_lastState == null) { - return false; - } - if (_lastState!.consumed) { - return false; - } - _lastState!.consumed = true; - return true; - } - - @override - PlatformBehavior get platform => _platform; - - @override - String? get selectedText { - return _lastState?.selectedText; - } - - @override - bool get isReady => _lastState != null; - - Future start({bool testingDontWaitForBootup = false}) async { - final initialRefreshCompleted = Completer(); - var firstReceivePort = ReceivePort(); - _isolate = await Isolate.spawn(terminalMain, firstReceivePort.sendPort); - _sendPort = await firstReceivePort.first; - firstReceivePort.close(); - _sendPort!.send([_IsolateCommand.sendPort, _receivePort.sendPort]); - _receivePort.listen((message) { - _IsolateEvent action = message[0]; - switch (action) { - case _IsolateEvent.bell: - this.onBell(); - break; - case _IsolateEvent.titleChanged: - this.onTitleChange(message[1]); - break; - case _IsolateEvent.iconChanged: - this.onIconChange(message[1]); - break; - case _IsolateEvent.notifyChange: - _refreshEventDebouncer.notifyEvent(() { - poll(); - }); - break; - case _IsolateEvent.newState: - _lastState = message[1]; - if (!initialRefreshCompleted.isCompleted) { - initialRefreshCompleted.complete(true); - } - this.notifyListeners(); - break; - case _IsolateEvent.exit: - _isTerminated = true; - _backendExited.complete(message[1]); - break; - } - }); - _sendPort!.send([ - _IsolateCommand.init, - TerminalInitData(this.backend, this.platform, this.theme, this.maxLines) - ]); - if (!testingDontWaitForBootup) { - await initialRefreshCompleted.future; - } - _isStarted = true; - } - - void stop() { - _assertStarted(); - terminateBackend(); - _isolate.kill(); - } - - void poll() { - _sendPort?.send([_IsolateCommand.requestNewStateWhenDirty]); - } - - void refresh() { - _sendPort?.send([_IsolateCommand.refresh]); - } - - void clearSelection() { - _sendPort?.send([_IsolateCommand.clearSelection]); - } - - @override - void selectAll() { - _sendPort?.send([_IsolateCommand.selectAll]); - } - - @override - void onMouseTap(Position position) { - _sendPort?.send([_IsolateCommand.mouseTap, position]); - } - - @override - void onMouseDoubleTap(Position position) { - _sendPort?.send([_IsolateCommand.mouseDoubleTap, position]); - } - - @override - void onPanStart(Position position) { - _sendPort?.send([_IsolateCommand.mousePanStart, position]); - } - - @override - void onPanUpdate(Position position) { - _sendPort?.send([_IsolateCommand.mousePanUpdate, position]); - } - - @override - void setScrollOffsetFromBottom(int offset) { - _sendPort?.send([_IsolateCommand.setScrollOffsetFromBottom, offset]); - } - - @override - int convertViewLineToRawLine(int viewLine) { - if (_lastState == null) { - return 0; - } - - if (_lastState!.viewHeight > _lastState!.bufferHeight) { - return viewLine; - } - - return viewLine + (_lastState!.bufferHeight - _lastState!.viewHeight); - } - - @override - void write(String text) { - _sendPort?.send([_IsolateCommand.write, text]); - } - - @override - void paste(String data) { - _sendPort?.send([_IsolateCommand.paste, data]); - } - - @override - void resize( - int newWidth, int newHeight, int newPixelWidth, int newPixelHeight) { - _sendPort?.send([ - _IsolateCommand.resize, - newWidth, - newHeight, - newPixelWidth, - newPixelHeight - ]); - } - - @override - void raiseOnInput(String text) { - _sendPort?.send([_IsolateCommand.onInput, text]); - } - - @override - void keyInput( - TerminalKey key, { - bool ctrl = false, - bool alt = false, - bool shift = false, - bool mac = false, - // bool meta, - String? character, - }) { - _sendPort?.send( - [_IsolateCommand.keyInput, key, ctrl, alt, shift, mac, character]); - } - - var _isTerminated = false; - - final _backendExited = Completer(); - - @override - Future get backendExited => _backendExited.future; - - @override - void terminateBackend() { - if (_isTerminated) { - return; - } - _isTerminated = true; - _sendPort?.send([_IsolateCommand.terminateBackend]); - } - - @override - bool get isTerminated => _isTerminated; - - @override - String get composingString { - return _lastState?.composingString ?? ''; - } - - @override - void updateComposingString(String value) { - _sendPort?.send([_IsolateCommand.updateComposingString, value]); - } - - @override - TerminalSearchResult get userSearchResult => - _lastState?.searchResult ?? TerminalSearchResult.empty(); - - @override - int get numberOfSearchHits { - return userSearchResult.allHits.length; - } - - @override - int? get currentSearchHit { - return userSearchResult.currentSearchHit; - } - - @override - set currentSearchHit(int? currentSearchHit) { - _sendPort?.send([_IsolateCommand.updateCurrentSearchHit, currentSearchHit]); - } - - @override - TerminalSearchOptions get userSearchOptions => - _lastState?.userSearchOptions ?? TerminalSearchOptions(); - - @override - set userSearchOptions(TerminalSearchOptions options) { - _sendPort?.send([_IsolateCommand.updateSearchOptions, options]); - } - - @override - String? get userSearchPattern { - return _lastState?.userSearchPattern; - } - - @override - set userSearchPattern(String? newValue) { - _sendPort?.send([_IsolateCommand.updateSearchPattern, newValue]); - } - - @override - bool get isUserSearchActive { - return _lastState?.isUserSearchActive ?? false; - } - - @override - set isUserSearchActive(bool isUserSearchActive) { - _sendPort - ?.send([_IsolateCommand.updateIsUserSearchActive, isUserSearchActive]); - } - - void _assertStarted() { - if (!_isStarted) { - throw Exception( - 'The Terminal Isolate has to be started before using it! (call await terminalIsolate.start()'); - } - } -} diff --git a/lib/terminal/terminal_search.dart b/lib/terminal/terminal_search.dart deleted file mode 100644 index 3300b04d..00000000 --- a/lib/terminal/terminal_search.dart +++ /dev/null @@ -1,383 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:xterm/buffer/line/line.dart'; -import 'package:xterm/terminal/terminal_search_interaction.dart'; -import 'package:xterm/util/constants.dart'; -import 'package:xterm/util/unicode_v11.dart'; - -/// Represents a search result. -/// This instance will be replaced as a whole when the search has to be re-triggered -/// It stores the hits the search produced and the navigation state inside -/// the search results -class TerminalSearchResult { - late final _allHits; - int? _currentSearchHit; - - /// creates a new search result instance from the given hits - TerminalSearchResult.fromHits(List hits) { - _allHits = hits; - - if (_allHits.length > 0) { - _currentSearchHit = _allHits.length; - } else { - _currentSearchHit = null; - } - } - - /// creates an empty search result - TerminalSearchResult.empty() - : _allHits = List.empty(growable: false); - - /// returns all hits of this search result - List get allHits => _allHits; - - /// returns the number of the current search hit - int? get currentSearchHit => _currentSearchHit; - - /// sets the current search hit number - set currentSearchHit(int? currentSearchHit) { - if (_allHits.length <= 0) { - _currentSearchHit = null; - } else { - _currentSearchHit = currentSearchHit != null - ? currentSearchHit.clamp(1, _allHits.length).toInt() - : null; - } - } -} - -/// Represents one search hit -class TerminalSearchHit { - TerminalSearchHit( - this.startLineIndex, this.startIndex, this.endLineIndex, this.endIndex); - - /// index of the line where the hit starts - final int startLineIndex; - - /// index of the hit start inside the start line - final int startIndex; - - /// index of the line where the hit starts - final int endLineIndex; - - /// index of the hit end inside the end line - final int endIndex; - - /// checks if the given cell (line / col) is contained in this hit - bool contains(int line, int col) { - if (line < startLineIndex || line > endLineIndex) { - return false; - } - if (line == startLineIndex && startLineIndex == endLineIndex) { - return col >= startIndex && col < endIndex; - } - if (line == startLineIndex) { - return col >= startIndex; - } - if (line == endLineIndex) { - return col < endIndex; - } - // here we are sure that the given point is inside a full line match - return true; - } -} - -/// represents options for a terminal search -class TerminalSearchOptions extends Equatable { - TerminalSearchOptions({ - this.caseSensitive = false, - this.matchWholeWord = false, - this.useRegex = false, - }); - - /// defines if the search should be case sensitive. If set to [false] then - /// the search will be case insensitive - final bool caseSensitive; - - /// defines if the search should match whole words. - final bool matchWholeWord; - - /// defines if the search should treat the pattern as a regex, or not - final bool useRegex; - - /// creates a new TerminalSearchOptions instance based on this one changing the - /// given parameters - TerminalSearchOptions copyWith( - {bool? caseSensitive, bool? matchWholeWord, bool? useRegex}) { - return TerminalSearchOptions( - caseSensitive: caseSensitive ?? this.caseSensitive, - matchWholeWord: matchWholeWord ?? this.matchWholeWord, - useRegex: useRegex ?? this.useRegex, - ); - } - - @override - bool get stringify => true; - - @override - List get props => [ - caseSensitive, - matchWholeWord, - useRegex, - ]; -} - -/// represents a search task. -/// A search task can deliver search results based on the given parameters. -/// It takes care to cache the results as long as possible and re-trigger a -/// search on demand only when necessary -class TerminalSearchTask { - TerminalSearchTask(this._search, this._terminal, this._dirtyTagName, - this._terminalSearchOptions); - - final TerminalSearch _search; - final TerminalSearchInteraction _terminal; - String? _pattern; - bool _isPatternDirty = true; - RegExp? _searchRegexp; - final String _dirtyTagName; - TerminalSearchOptions _terminalSearchOptions; - - bool _isActive = false; - - /// indicates if the current search task is active - bool get isActive => _isActive; - - /// sets the active state of this search task - set isActive(bool isActive) { - _isActive = isActive; - if (isActive) { - _invalidate(); - } - } - - bool? _hasBeenUsingAltBuffer; - TerminalSearchResult? _lastSearchResult; - - bool _isAnyLineDirty() { - final bufferLength = _terminal.buffer.lines.length; - for (var i = 0; i < bufferLength; i++) { - if (_terminal.buffer.lines[i].isTagDirty(_dirtyTagName)) { - return true; - } - } - return false; - } - - void _markLinesForSearchDone() { - final bufferLength = _terminal.buffer.lines.length; - for (var i = 0; i < bufferLength; i++) { - _terminal.buffer.lines[i].markTagAsNonDirty(_dirtyTagName); - } - } - - bool _isTerminalStateDirty() { - if (_isAnyLineDirty()) { - return true; - } - if (_hasBeenUsingAltBuffer != null && - _hasBeenUsingAltBuffer! != _terminal.isUsingAltBuffer()) { - return true; - } - return false; - } - - bool get _isDirty { - if (_isPatternDirty) { - return true; - } - return _isTerminalStateDirty(); - } - - /// the currently used pattern of this search task - String? get pattern => _pattern; - - /// sets the pattern to use for this search task - set pattern(String? newPattern) { - if (newPattern != _pattern) { - _pattern = newPattern; - _invalidate(); - } - } - - /// the currently used search options - TerminalSearchOptions get options => _terminalSearchOptions; - - /// sets the search options to use - set options(TerminalSearchOptions newOptions) { - if (_terminalSearchOptions == newOptions) { - return; - } - _terminalSearchOptions = newOptions; - _invalidate(); - } - - /// returns the hit that is currently the selected one (based on the search - /// result navigation) - TerminalSearchHit? get currentSearchHitObject { - if (searchResult.currentSearchHit == null) { - return null; - } - if (searchResult.allHits.length >= searchResult.currentSearchHit! && - searchResult.currentSearchHit! > 0) { - return searchResult.allHits[searchResult.currentSearchHit! - 1]; - } - return null; - } - - /// the number of search hits in the current search result - int get numberOfSearchHits => searchResult.allHits.length; - - /// number of the hit that is currently selected - int? get currentSearchHit => searchResult.currentSearchHit; - - /// sets the hit number that shall be selected - set currentSearchHit(int? currentSearchHit) { - searchResult.currentSearchHit = currentSearchHit; - } - - void _invalidate() { - _isPatternDirty = true; - _searchRegexp = null; - _lastSearchResult = null; - } - - String _createRegexPattern(String inputPattern) { - final result = StringBuffer(); - - for (final rune in inputPattern.runes) { - final runeString = String.fromCharCode(rune); - result.write(runeString); - final cellWidth = unicodeV11.wcwidth(rune); - final widthDiff = cellWidth - runeString.length; - if (widthDiff > 0) { - result.write(''.padRight(widthDiff)); - } - } - - return result.toString(); - } - - /// returns the current search result or triggers a new search if it has to - /// the result is a up to date search result either way - TerminalSearchResult get searchResult { - if (_pattern == null || !_isActive) { - return TerminalSearchResult.empty(); - } - if (_lastSearchResult != null && !_isDirty) { - return _lastSearchResult!; - } - - final terminalWidth = _terminal.terminalWidth; - - if (_searchRegexp == null) { - var pattern = _pattern!; - if (!_terminalSearchOptions.useRegex) { - pattern = RegExp.escape(_pattern!); - } - _searchRegexp = RegExp(_createRegexPattern(pattern), - caseSensitive: _terminalSearchOptions.caseSensitive, - multiLine: false); - } - - final hits = List.empty(growable: true); - - for (final match - in _searchRegexp!.allMatches(_search.terminalSearchString)) { - final start = match.start; - final end = match.end; - final startLineIndex = (start / terminalWidth).floor(); - final endLineIndex = (end / terminalWidth).floor(); - - // subtract the lines that got added in order to get the index inside the line - final startIndex = start - startLineIndex * terminalWidth; - final endIndex = end - endLineIndex * terminalWidth; - - if (_terminalSearchOptions.matchWholeWord) { - // we match a whole word when the hit fulfills: - // 1) starts at a line beginning or has a word-separator before it - final startIsOK = - startIndex == 0 || kWordSeparators.contains(match.input[start - 1]); - // 2) ends with a line or has a word-separator after it - final endIsOK = endIndex == terminalWidth || - kWordSeparators.contains(match.input[end]); - - if (!startIsOK || !endIsOK) { - continue; - } - } - - hits.add( - TerminalSearchHit( - startLineIndex, - startIndex, - endLineIndex, - endIndex, - ), - ); - } - - _markLinesForSearchDone(); - - _isPatternDirty = false; - _lastSearchResult = TerminalSearchResult.fromHits(hits); - _hasBeenUsingAltBuffer = _terminal.isUsingAltBuffer(); - return _lastSearchResult!; - } -} - -/// main entry for terminal searches. This class is the factory for search tasks -/// and will cache the search string that gets generated out of the terminal content -/// so that all search tasks created by this search can use the same cached search string -class TerminalSearch { - TerminalSearch(this._terminal); - - final TerminalSearchInteraction _terminal; - String? _cachedSearchString; - int? _lastTerminalWidth; - - /// creates a new search task that will use this search to access a cached variant - /// of the terminal search string - TerminalSearchTask createSearchTask(String dirtyTagName) { - return TerminalSearchTask( - this, _terminal, dirtyTagName, TerminalSearchOptions()); - } - - /// returns the current terminal search string. The search string will be - /// refreshed on demand if - String get terminalSearchString { - final bufferLength = _terminal.buffer.lines.length; - final terminalWidth = _terminal.terminalWidth; - - var isAnySearchStringInvalid = false; - for (var i = 0; i < bufferLength; i++) { - if (!_terminal.buffer.lines[i].hasCachedSearchString) { - isAnySearchStringInvalid = true; - } - } - - late String completeSearchString; - if (_cachedSearchString != null && - _lastTerminalWidth != null && - _lastTerminalWidth! == terminalWidth && - !isAnySearchStringInvalid) { - completeSearchString = _cachedSearchString!; - } else { - final bufferContent = StringBuffer(); - for (var i = 0; i < bufferLength; i++) { - final BufferLine line = _terminal.buffer.lines[i]; - final searchString = line.toSearchString(terminalWidth); - bufferContent.write(searchString); - if (searchString.length < terminalWidth) { - // fill up so that the row / col can be mapped back later on - bufferContent.writeAll( - List.filled(terminalWidth - searchString.length, ' ')); - } - } - completeSearchString = bufferContent.toString(); - _cachedSearchString = completeSearchString; - _lastTerminalWidth = terminalWidth; - } - - return completeSearchString; - } -} diff --git a/lib/terminal/terminal_search_interaction.dart b/lib/terminal/terminal_search_interaction.dart deleted file mode 100644 index 60ff9c9e..00000000 --- a/lib/terminal/terminal_search_interaction.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:xterm/buffer/buffer.dart'; - -/// This interface defines the functionality of a Terminal that is needed -/// by the search functionality -abstract class TerminalSearchInteraction { - /// the current buffer - Buffer get buffer; - - /// indication if the alternative buffer is currently used - bool isUsingAltBuffer(); - - /// the terminal width - int get terminalWidth; -} diff --git a/lib/terminal/terminal_ui_interaction.dart b/lib/terminal/terminal_ui_interaction.dart deleted file mode 100644 index bb0f23d0..00000000 --- a/lib/terminal/terminal_ui_interaction.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:xterm/buffer/line/line.dart'; -import 'package:xterm/input/keys.dart'; -import 'package:xterm/mouse/position.dart'; -import 'package:xterm/mouse/selection.dart'; -import 'package:xterm/terminal/platform.dart'; -import 'package:xterm/terminal/terminal_search.dart'; -import 'package:xterm/theme/terminal_theme.dart'; -import 'package:xterm/util/observable.dart'; - -/// this interface describes what a Terminal UI needs from a Terminal -abstract class TerminalUiInteraction with Observable { - /// The theme associated with this Terminal - TerminalTheme get theme; - - /// the ViewPort scroll offset from the bottom - int get scrollOffsetFromBottom; - - /// the ViewPort scroll offset from the top - int get scrollOffsetFromTop; - - /// the total buffer height - int get bufferHeight; - - /// terminal height (view port) - int get terminalHeight; - - /// terminal width (view port) - int get terminalWidth; - - /// the part of the buffer that is not visible (scrollback) - int get invisibleHeight; - - /// object that describes details about the current selection - Selection? get selection; - - /// [true] when the cursor shall be shown, otherwise [false] - bool get showCursor; - - /// returns the visible lines - List getVisibleLines(); - - /// cursor y coordinate - int get cursorY; - - /// cursor x coordinate - int get cursorX; - - /// current line - BufferLine? get currentLine; - - /// color code for the cursor - int get cursorColor; - - /// color code for the background - int get backgroundColor; - - /// flag that indicates if the terminal is dirty (since the last time this - /// flag has been queried) - bool get dirty; - - /// platform behavior for this terminal - PlatformBehavior get platform; - - /// selected text defined by [selection] - String? get selectedText; - - /// flag that indicates if the Terminal is ready - bool get isReady; - - /// refreshes the Terminal (notifies listeners and sets it to dirty) - void refresh(); - - /// clears the selection - void clearSelection(); - - /// select the whole buffer - void selectAll(); - - /// notify the Terminal about a mouse tap - void onMouseTap(Position position); - - /// notify the Terminal about a mouse double tap - void onMouseDoubleTap(Position position); - - /// notify the Terminal about a pan start - void onPanStart(Position position); - - /// notify the Terminal about a pan update - void onPanUpdate(Position position); - - /// sets the scroll offset from bottom (scrolling) - void setScrollOffsetFromBottom(int offset); - - /// converts the given view line (view port line) index to its position in the - /// overall buffer - int convertViewLineToRawLine(int viewLine); - - /// notifies the Terminal about user input - void raiseOnInput(String input); - - /// writes data to the Terminal - void write(String text); - - /// paste clipboard content to the Terminal - void paste(String data); - - /// notifies the Terminal about a resize that happened. The Terminal will - /// do any resize / reflow logic and notify the backend about the resize - void resize( - int newWidth, int newHeight, int newPixelWidth, int newPixelHeight); - - /// notifies the Terminal about key input - void keyInput( - TerminalKey key, { - bool ctrl = false, - bool alt = false, - bool shift = false, - bool mac = false, - // bool meta, - String? character, - }); - - /// Future that fires when the backend has exited - Future get backendExited; - - /// terminates the backend. If already terminated, nothing happens - void terminateBackend(); - - /// flag that indicates if the backend is already terminated - bool get isTerminated; - - /// returns the current composing string. '' when there is no composing going on - String get composingString; - - /// update the composing string. This gets called by the input handling - /// part of the terminal - void updateComposingString(String value); - - /// returns the list of search hits - TerminalSearchResult get userSearchResult; - - /// gets the number of search hits - int get numberOfSearchHits; - - /// gets the current search hit - int? get currentSearchHit; - - /// sets the current search hit (gets clamped to the valid bounds) - set currentSearchHit(int? currentSearchHit); - - /// gets the current user search options - TerminalSearchOptions get userSearchOptions; - - /// sets new user search options. This invalidates the cached search hits and - /// will re-trigger a new search - set userSearchOptions(TerminalSearchOptions options); - - /// the search pattern of a currently active search or [null] - String? get userSearchPattern; - - /// sets the currently active search pattern - set userSearchPattern(String? pattern); - - /// gets if a user search is active - bool get isUserSearchActive; - - // sets the user search active state - set isUserSearchActive(bool isUserSearchActive); -} diff --git a/lib/theme/terminal_color.dart b/lib/theme/terminal_color.dart deleted file mode 100644 index 87fd7576..00000000 --- a/lib/theme/terminal_color.dart +++ /dev/null @@ -1,15 +0,0 @@ -class TerminalColor { - static int empty() { - return 0xFF000000; - } - - static int transparent = 0x00000000; - - static int fromARGB(int a, int r, int g, int b) { - return (((a & 0xff) << 24) | - ((r & 0xff) << 16) | - ((g & 0xff) << 8) | - ((b & 0xff) << 0)) & - 0xFFFFFFFF; - } -} diff --git a/lib/theme/terminal_style.dart b/lib/theme/terminal_style.dart deleted file mode 100644 index e4a8ee24..00000000 --- a/lib/theme/terminal_style.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; - -class TerminalStyle with EquatableMixin { - static const defaultFontFamily = [ - 'Monaco', - 'Droid Sans Mono', - 'Noto Sans Mono', - 'Roboto Mono', - 'Consolas', - 'Noto Sans Mono CJK SC', - 'Noto Sans Mono CJK TC', - 'Noto Sans Mono CJK KR', - 'Noto Sans Mono CJK JP', - 'Noto Sans Mono CJK HK', - 'Noto Color Emoji', - 'Noto Sans Symbols', - 'Roboto', - 'Ubuntu', - 'Cantarell', - 'DejaVu Sans', - 'Liberation Sans', - 'Arial', - 'Droid Sans Fallback', - 'Cascadia Mono', - 'Arial Unicode MS', - 'sans-serif', - 'monospace', - ]; - - const TerminalStyle({ - this.fontFamily = defaultFontFamily, - this.fontSize = 14, - this.fontWidthScaleFactor = 1.0, - this.fontHeightScaleFactor = 1.1, - this.textStyleProvider, - this.ignoreBoldFlag = false, - }); - - final List fontFamily; - final double fontSize; - final double fontWidthScaleFactor; - final double fontHeightScaleFactor; - final TextStyleProvider? textStyleProvider; - final bool ignoreBoldFlag; - - @override - List get props { - return [ - fontFamily, - fontSize, - fontWidthScaleFactor, - fontHeightScaleFactor, - textStyleProvider, - ignoreBoldFlag - ]; - } -} - -typedef TextStyleProvider = Function({ - TextStyle textStyle, - Color color, - Color backgroundColor, - double fontSize, - FontWeight fontWeight, - FontStyle fontStyle, - double letterSpacing, - double wordSpacing, - TextBaseline textBaseline, - double height, - Locale locale, - Paint foreground, - Paint background, - List shadows, - List fontFeatures, - TextDecoration decoration, - Color decorationColor, - TextDecorationStyle decorationStyle, - double decorationThickness, -}); diff --git a/lib/theme/terminal_theme.dart b/lib/theme/terminal_theme.dart deleted file mode 100644 index be7bca0f..00000000 --- a/lib/theme/terminal_theme.dart +++ /dev/null @@ -1,53 +0,0 @@ -class TerminalTheme { - const TerminalTheme( - {required this.cursor, - required this.selection, - required this.foreground, - required this.background, - required this.black, - required this.white, - required this.red, - required this.green, - required this.yellow, - required this.blue, - required this.magenta, - required this.cyan, - required this.brightBlack, - required this.brightRed, - required this.brightGreen, - required this.brightYellow, - required this.brightBlue, - required this.brightMagenta, - required this.brightCyan, - required this.brightWhite, - required this.searchHitBackground, - required this.searchHitBackgroundCurrent, - required this.searchHitForeground}); - - final int cursor; - final int selection; - - final int foreground; - final int background; - final int black; - final int red; - final int green; - final int yellow; - final int blue; - final int magenta; - final int cyan; - final int white; - - final int brightBlack; - final int brightRed; - final int brightGreen; - final int brightYellow; - final int brightBlue; - final int brightMagenta; - final int brightCyan; - final int brightWhite; - - final int searchHitBackground; - final int searchHitBackgroundCurrent; - final int searchHitForeground; -} diff --git a/lib/theme/terminal_themes.dart b/lib/theme/terminal_themes.dart deleted file mode 100644 index 2144f4de..00000000 --- a/lib/theme/terminal_themes.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:xterm/theme/terminal_theme.dart'; - -class TerminalThemes { - static const defaultTheme = TerminalTheme( - cursor: 0XFFAEAFAD, - selection: 0XFFFFFF40, - foreground: 0XFFCCCCCC, - background: 0XFF1E1E1E, - black: 0XFF000000, - red: 0XFFCD3131, - green: 0XFF0DBC79, - yellow: 0XFFE5E510, - blue: 0XFF2472C8, - magenta: 0XFFBC3FBC, - cyan: 0XFF11A8CD, - white: 0XFFE5E5E5, - brightBlack: 0XFF666666, - brightRed: 0XFFF14C4C, - brightGreen: 0XFF23D18B, - brightYellow: 0XFFF5F543, - brightBlue: 0XFF3B8EEA, - brightMagenta: 0XFFD670D6, - brightCyan: 0XFF29B8DB, - brightWhite: 0XFFFFFFFF, - searchHitBackground: 0XFFFFFF2B, - searchHitBackgroundCurrent: 0XFF31FF26, - searchHitForeground: 0XFF000000, - ); - - static const whiteOnBlack = TerminalTheme( - cursor: 0XFFAEAFAD, - selection: 0XFFFFFF40, - foreground: 0XFFFFFFFF, - background: 0XFF000000, - black: 0XFF000000, - red: 0XFFCD3131, - green: 0XFF0DBC79, - yellow: 0XFFE5E510, - blue: 0XFF2472C8, - magenta: 0XFFBC3FBC, - cyan: 0XFF11A8CD, - white: 0XFFE5E5E5, - brightBlack: 0XFF666666, - brightRed: 0XFFF14C4C, - brightGreen: 0XFF23D18B, - brightYellow: 0XFFF5F543, - brightBlue: 0XFF3B8EEA, - brightMagenta: 0XFFD670D6, - brightCyan: 0XFF29B8DB, - brightWhite: 0XFFFFFFFF, - searchHitBackground: 0XFFFFFF2B, - searchHitBackgroundCurrent: 0XFF31FF26, - searchHitForeground: 0XFF000000, - ); -} diff --git a/lib/ui.dart b/lib/ui.dart new file mode 100644 index 00000000..34f9145d --- /dev/null +++ b/lib/ui.dart @@ -0,0 +1,3 @@ +export 'ui/cursor_type.dart'; +export 'ui/terminal_text_style.dart'; +export 'ui/keyboard_visibility.dart'; diff --git a/lib/ui/char_metrics.dart b/lib/ui/char_metrics.dart new file mode 100644 index 00000000..5d8a00bd --- /dev/null +++ b/lib/ui/char_metrics.dart @@ -0,0 +1,20 @@ +import 'dart:ui'; + +import 'package:xterm/ui/terminal_text_style.dart'; + +Size calcCharMetrics(TerminalStyle style) { + const test = 'mmmmmmmmmm'; + + final textStyle = style.toTextStyle(); + final builder = ParagraphBuilder(textStyle.getParagraphStyle()); + builder.pushStyle(textStyle.getTextStyle()); + builder.addText(test); + + final paragraph = builder.build(); + paragraph.layout(ParagraphConstraints(width: double.infinity)); + + return Size( + paragraph.maxIntrinsicWidth / test.length, + paragraph.height, + ); +} diff --git a/lib/ui/controller.dart b/lib/ui/controller.dart new file mode 100644 index 00000000..c7e11167 --- /dev/null +++ b/lib/ui/controller.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:xterm/core/buffer/range.dart'; + +class TerminalController with ChangeNotifier { + BufferRange? _selection; + + BufferRange? get selection => _selection; + + bool get hasSelection => _selection != null; + + void setSelection(BufferRange? range) { + if (_selection != range) { + _selection = range; + notifyListeners(); + } + } + + void clearSelection() { + _selection = null; + notifyListeners(); + } + + void addHighlight(BufferRange? range) { + // TODO: implement addHighlight + } + + void clearHighlight() { + // TODO: implement clearHighlight + } +} diff --git a/lib/ui/cursor_type.dart b/lib/ui/cursor_type.dart new file mode 100644 index 00000000..0af0a58d --- /dev/null +++ b/lib/ui/cursor_type.dart @@ -0,0 +1,7 @@ +enum TerminalCursorType { + block, + + underline, + + verticalBar, +} diff --git a/lib/ui/custom_text_edit.dart b/lib/ui/custom_text_edit.dart new file mode 100644 index 00000000..988d1d0b --- /dev/null +++ b/lib/ui/custom_text_edit.dart @@ -0,0 +1,275 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CustomTextEdit extends StatefulWidget { + CustomTextEdit({ + Key? key, + required this.child, + required this.onInsert, + required this.onDelete, + required this.onComposing, + required this.onAction, + required this.onKey, + required this.focusNode, + this.autofocus = false, + this.readOnly = false, + // this.initEditingState = TextEditingValue.empty, + this.inputType = TextInputType.text, + this.inputAction = TextInputAction.newline, + this.keyboardAppearance = Brightness.light, + this.deleteDetection = false, + }) : super(key: key); + + final Widget child; + + final void Function(String) onInsert; + + final void Function() onDelete; + + final void Function(String?) onComposing; + + final void Function(TextInputAction) onAction; + + final KeyEventResult Function(RawKeyEvent) onKey; + + final FocusNode focusNode; + + final bool autofocus; + + final bool readOnly; + + final TextInputType inputType; + + final TextInputAction inputAction; + + final Brightness keyboardAppearance; + + final bool deleteDetection; + + @override + CustomTextEditState createState() => CustomTextEditState(); +} + +class CustomTextEditState extends State + implements TextInputClient { + TextInputConnection? _connection; + + @override + void initState() { + widget.focusNode.addListener(_onFocusChange); + super.initState(); + } + + @override + void didUpdateWidget(CustomTextEdit oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.focusNode != oldWidget.focusNode) { + oldWidget.focusNode.removeListener(_onFocusChange); + widget.focusNode.addListener(_onFocusChange); + } + + if (!_shouldCreateInputConnection) { + _closeInputConnectionIfNeeded(); + } else { + if (oldWidget.readOnly && widget.focusNode.hasFocus) { + _openInputConnection(); + } + } + } + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: widget.focusNode, + autofocus: widget.autofocus, + onKey: _onKey, + child: widget.child, + ); + } + + bool get hasInputConnection => _connection != null && _connection!.attached; + + void requestKeyboard() { + if (widget.focusNode.hasFocus) { + _openInputConnection(); + } else { + widget.focusNode.requestFocus(); + } + } + + void closeKeyboard() { + if (hasInputConnection) { + _connection?.close(); + } + } + + void setEditingState(TextEditingValue value) { + _currentEditingState = value; + _connection?.setEditingState(value); + } + + void setEditableRect(Rect rect, Rect caretRect) { + if (!hasInputConnection) { + return; + } + + _connection?.setEditableSizeAndTransform( + rect.size, + Matrix4.translationValues(0, 0, 0), + ); + + _connection?.setCaretRect(caretRect); + } + + void _onFocusChange() { + _openOrCloseInputConnectionIfNeeded(); + } + + KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent event) { + if (_currentEditingState.composing.isCollapsed) { + return widget.onKey(event); + } + + return KeyEventResult.skipRemainingHandlers; + } + + void _openOrCloseInputConnectionIfNeeded() { + if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { + _openInputConnection(); + } else if (!widget.focusNode.hasFocus) { + _closeInputConnectionIfNeeded(); + } + } + + bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly; + + void _openInputConnection() { + if (!_shouldCreateInputConnection) { + return; + } + + if (hasInputConnection) { + _connection!.show(); + } else { + final config = TextInputConfiguration( + inputType: widget.inputType, + inputAction: widget.inputAction, + keyboardAppearance: widget.keyboardAppearance, + autocorrect: false, + enableSuggestions: false, + enableIMEPersonalizedLearning: false, + ); + + _connection = TextInput.attach(this, config); + + _connection!.show(); + + // setEditableRect(Rect.zero, Rect.zero); + + _connection!.setEditingState(_initEditingState); + } + } + + void _closeInputConnectionIfNeeded() { + if (_connection != null && _connection!.attached) { + _connection!.close(); + _connection = null; + } + } + + TextEditingValue get _initEditingState => widget.deleteDetection + ? const TextEditingValue( + text: ' ', + selection: TextSelection.collapsed(offset: 2), + ) + : const TextEditingValue( + text: '', + selection: TextSelection.collapsed(offset: 0), + ); + + late var _currentEditingState = _initEditingState.copyWith(); + + @override + TextEditingValue? get currentTextEditingValue { + return _currentEditingState; + } + + @override + AutofillScope? get currentAutofillScope { + return null; + } + + @override + void updateEditingValue(TextEditingValue value) { + _currentEditingState = value; + + // Get input after composing is done + if (!_currentEditingState.composing.isCollapsed) { + final text = _currentEditingState.text; + final composingText = _currentEditingState.composing.textInside(text); + widget.onComposing(composingText); + return; + } + + widget.onComposing(null); + + if (_currentEditingState.text.length < _initEditingState.text.length) { + widget.onDelete(); + } else { + final textDelta = _currentEditingState.text.substring( + _initEditingState.text.length, + ); + + widget.onInsert(textDelta); + } + + // Reset editing state if composing is done + if (_currentEditingState.composing.isCollapsed && + _currentEditingState.text != _initEditingState.text) { + _connection!.setEditingState(_initEditingState); + } + } + + @override + void performAction(TextInputAction action) { + // print('performAction $action'); + widget.onAction(action); + } + + @override + void updateFloatingCursor(RawFloatingCursorPoint point) { + // print('updateFloatingCursor $point'); + } + + @override + void showAutocorrectionPromptRect(int start, int end) { + // print('showAutocorrectionPromptRect'); + } + + @override + void connectionClosed() { + // print('connectionClosed'); + } + + @override + void performPrivateCommand(String action, Map data) { + // print('performPrivateCommand $action'); + } + + @override + void insertTextPlaceholder(Size size) { + // print('insertTextPlaceholder'); + } + + @override + void removeTextPlaceholder() { + // print('removeTextPlaceholder'); + } + + @override + void showToolbar() { + // print('showToolbar'); + } +} diff --git a/lib/ui/gesture/gesture_detector.dart b/lib/ui/gesture/gesture_detector.dart new file mode 100644 index 00000000..9a5979f1 --- /dev/null +++ b/lib/ui/gesture/gesture_detector.dart @@ -0,0 +1,150 @@ +import 'dart:async'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +class TerminalGestureDetector extends StatefulWidget { + const TerminalGestureDetector({ + super.key, + this.child, + this.onSingleTapUp, + this.onTapUp, + this.onTapDown, + this.onSecondaryTapDown, + this.onLongPressStart, + this.onLongPressMoveUpdate, + this.onLongPressUp, + this.onDragStart, + this.onDragUpdate, + this.onDoubleTapDown, + }); + + final Widget? child; + + final GestureTapUpCallback? onTapUp; + + final GestureTapUpCallback? onSingleTapUp; + + final GestureTapDownCallback? onTapDown; + + final GestureTapDownCallback? onSecondaryTapDown; + + final GestureTapDownCallback? onDoubleTapDown; + + final GestureLongPressStartCallback? onLongPressStart; + + final GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate; + + final GestureLongPressUpCallback? onLongPressUp; + + final GestureDragStartCallback? onDragStart; + + final GestureDragUpdateCallback? onDragUpdate; + + @override + State createState() => + _TerminalGestureDetectorState(); +} + +class _TerminalGestureDetectorState extends State { + Timer? _doubleTapTimer; + + Offset? _lastTapOffset; + + // True if a second tap down of a double tap is detected. Used to discard + // subsequent tap up / tap hold of the same tap. + bool _isDoubleTap = false; + + // The down handler is force-run on success of a single tap and optimistically + // run before a long press success. + void _handleTapDown(TapDownDetails details) { + widget.onTapDown?.call(details); + + if (_doubleTapTimer != null && + _isWithinDoubleTapTolerance(details.globalPosition)) { + // If there was already a previous tap, the second down hold/tap is a + // double tap down. + widget.onDoubleTapDown?.call(details); + + _doubleTapTimer!.cancel(); + _doubleTapTimeout(); + _isDoubleTap = true; + } + } + + void _handleTapUp(TapUpDetails details) { + if (!_isDoubleTap) { + widget.onSingleTapUp?.call(details); + _lastTapOffset = details.globalPosition; + _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); + } + _isDoubleTap = false; + } + + void _doubleTapTimeout() { + _doubleTapTimer = null; + _lastTapOffset = null; + } + + bool _isWithinDoubleTapTolerance(Offset secondTapOffset) { + if (_lastTapOffset == null) { + return false; + } + + final Offset difference = secondTapOffset - _lastTapOffset!; + return difference.distance <= kDoubleTapSlop; + } + + @override + Widget build(BuildContext context) { + final gestures = {}; + + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { + instance + ..onTapDown = _handleTapDown + ..onTapUp = _handleTapUp + ..onSecondaryTapDown = widget.onSecondaryTapDown; + }, + ); + + gestures[LongPressGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer( + debugOwner: this, + supportedDevices: { + PointerDeviceKind.touch, + // PointerDeviceKind.mouse, // for debugging purposes only + }, + ), + (LongPressGestureRecognizer instance) { + instance + ..onLongPressStart = widget.onLongPressStart + ..onLongPressMoveUpdate = widget.onLongPressMoveUpdate + ..onLongPressUp = widget.onLongPressUp; + }, + ); + + gestures[PanGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer( + debugOwner: this, + supportedDevices: {PointerDeviceKind.mouse}, + ), + (PanGestureRecognizer instance) { + instance + ..dragStartBehavior = DragStartBehavior.down + ..onStart = widget.onDragStart + ..onUpdate = widget.onDragUpdate; + }, + ); + + return RawGestureDetector( + gestures: gestures, + excludeFromSemantics: true, + child: widget.child, + ); + } +} diff --git a/lib/ui/gesture/gesture_handler.dart b/lib/ui/gesture/gesture_handler.dart new file mode 100644 index 00000000..0214148d --- /dev/null +++ b/lib/ui/gesture/gesture_handler.dart @@ -0,0 +1,94 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:xterm/ui/terminal_view.dart'; +import 'package:xterm/ui/gesture/gesture_detector.dart'; +import 'package:xterm/ui/render.dart'; + +class TerminalGestureHandler extends StatefulWidget { + const TerminalGestureHandler({ + super.key, + required this.terminalView, + this.child, + this.onTapUp, + this.onSingleTapUp, + this.onTapDown, + this.onSecondaryTapDown, + }); + + final TerminalViewState terminalView; + + final Widget? child; + + final GestureTapUpCallback? onTapUp; + + final GestureTapUpCallback? onSingleTapUp; + + final GestureTapDownCallback? onTapDown; + + final GestureTapDownCallback? onSecondaryTapDown; + + @override + State createState() => _TerminalGestureHandlerState(); +} + +class _TerminalGestureHandlerState extends State { + TerminalViewState get terminalView => widget.terminalView; + + RenderTerminal get renderTerminal => terminalView.renderTerminal; + + DragStartDetails? _lastDragStartDetails; + + LongPressStartDetails? _lastLongPressStartDetails; + + @override + Widget build(BuildContext context) { + return TerminalGestureDetector( + child: widget.child, + onTapUp: widget.onTapUp, + onSingleTapUp: widget.onSingleTapUp, + onTapDown: widget.onTapDown, + onSecondaryTapDown: widget.onSecondaryTapDown, + onLongPressStart: onLongPressStart, + onLongPressMoveUpdate: onLongPressMoveUpdate, + // onLongPressUp: onLongPressUp, + onDragStart: onDragStart, + onDragUpdate: onDragUpdate, + onDoubleTapDown: onDoubleTapDown, + ); + } + + void onDoubleTapDown(TapDownDetails details) { + renderTerminal.selectWord( + details.globalPosition, + ); + } + + void onLongPressStart(LongPressStartDetails details) { + _lastLongPressStartDetails = details; + renderTerminal.selectWord(details.globalPosition); + } + + void onLongPressMoveUpdate(LongPressMoveUpdateDetails details) { + renderTerminal.selectWord( + _lastLongPressStartDetails!.globalPosition, + details.globalPosition, + ); + } + + // void onLongPressUp() {} + + void onDragStart(DragStartDetails details) { + _lastDragStartDetails = details; + + details.kind == PointerDeviceKind.mouse + ? renderTerminal.selectPosition(details.globalPosition) + : renderTerminal.selectWord(details.globalPosition); + } + + void onDragUpdate(DragUpdateDetails details) { + renderTerminal.selectPosition( + _lastDragStartDetails!.globalPosition, + details.globalPosition, + ); + } +} diff --git a/lib/frontend/input_map.dart b/lib/ui/input_map.dart similarity index 84% rename from lib/frontend/input_map.dart rename to lib/ui/input_map.dart index 88c181c1..b26dc5ad 100644 --- a/lib/frontend/input_map.dart +++ b/lib/ui/input_map.dart @@ -1,5 +1,5 @@ import 'package:flutter/services.dart'; -import 'package:xterm/input/keys.dart'; +import 'package:xterm/core/input/keys.dart'; final _idKeyMap = { LogicalKeyboardKey.hyper.keyId: TerminalKey.hyper, @@ -7,15 +7,8 @@ final _idKeyMap = { LogicalKeyboardKey.fnLock.keyId: TerminalKey.fnLock, LogicalKeyboardKey.suspend.keyId: TerminalKey.suspend, LogicalKeyboardKey.resume.keyId: TerminalKey.resume, - // LogicalKeyboardKey.turbo.keyId: TerminalKey.turbo, - // LogicalKeyboardKey.privacyScreenToggle.keyId: TerminalKey.privacyScreenToggle, LogicalKeyboardKey.sleep.keyId: TerminalKey.sleep, LogicalKeyboardKey.wakeUp.keyId: TerminalKey.wakeUp, - // LogicalKeyboardKey.displayToggleIntExt.keyId: TerminalKey.displayToggleIntExt, - // LogicalKeyboardKey.usbReserved.keyId: TerminalKey.usbReserved, - // LogicalKeyboardKey.usbErrorRollOver.keyId: TerminalKey.usbErrorRollOver, - // LogicalKeyboardKey.usbPostFail.keyId: TerminalKey.usbPostFail, - // LogicalKeyboardKey.usbErrorUndefined.keyId: TerminalKey.usbErrorUndefined, LogicalKeyboardKey.keyA.keyId: TerminalKey.keyA, LogicalKeyboardKey.keyB.keyId: TerminalKey.keyB, LogicalKeyboardKey.keyC.keyId: TerminalKey.keyC, @@ -154,16 +147,6 @@ final _idKeyMap = { LogicalKeyboardKey.props.keyId: TerminalKey.props, LogicalKeyboardKey.numpadParenLeft.keyId: TerminalKey.numpadParenLeft, LogicalKeyboardKey.numpadParenRight.keyId: TerminalKey.numpadParenRight, - // LogicalKeyboardKey.numpadBackspace.keyId: TerminalKey.numpadBackspace, - // LogicalKeyboardKey.numpadMemoryStore.keyId: TerminalKey.numpadMemoryStore, - // LogicalKeyboardKey.numpadMemoryRecall.keyId: TerminalKey.numpadMemoryRecall, - // LogicalKeyboardKey.numpadMemoryClear.keyId: TerminalKey.numpadMemoryClear, - // LogicalKeyboardKey.numpadMemoryAdd.keyId: TerminalKey.numpadMemoryAdd, - // LogicalKeyboardKey.numpadMemorySubtract.keyId: - // TerminalKey.numpadMemorySubtract, - // LogicalKeyboardKey.numpadSignChange.keyId: TerminalKey.numpadSignChange, - // LogicalKeyboardKey.numpadClear.keyId: TerminalKey.numpadClear, - // LogicalKeyboardKey.numpadClearEntry.keyId: TerminalKey.numpadClearEntry, LogicalKeyboardKey.controlLeft.keyId: TerminalKey.controlLeft, LogicalKeyboardKey.shiftLeft.keyId: TerminalKey.shiftLeft, LogicalKeyboardKey.altLeft.keyId: TerminalKey.altLeft, @@ -176,13 +159,8 @@ final _idKeyMap = { LogicalKeyboardKey.closedCaptionToggle.keyId: TerminalKey.closedCaptionToggle, LogicalKeyboardKey.brightnessUp.keyId: TerminalKey.brightnessUp, LogicalKeyboardKey.brightnessDown.keyId: TerminalKey.brightnessDown, - // LogicalKeyboardKey.brightnessToggle.keyId: TerminalKey.brightnessToggle, - // LogicalKeyboardKey.brightnessMinimum.keyId: TerminalKey.brightnessMinimum, - // LogicalKeyboardKey.brightnessMaximum.keyId: TerminalKey.brightnessMaximum, - // LogicalKeyboardKey.brightnessAuto.keyId: TerminalKey.brightnessAuto, LogicalKeyboardKey.mediaLast.keyId: TerminalKey.mediaLast, LogicalKeyboardKey.launchPhone.keyId: TerminalKey.launchPhone, - // LogicalKeyboardKey.programGuide.keyId: TerminalKey.programGuide, LogicalKeyboardKey.exit.keyId: TerminalKey.exit, LogicalKeyboardKey.channelUp.keyId: TerminalKey.channelUp, LogicalKeyboardKey.channelDown.keyId: TerminalKey.channelDown, @@ -197,28 +175,16 @@ final _idKeyMap = { LogicalKeyboardKey.eject.keyId: TerminalKey.eject, LogicalKeyboardKey.mediaPlayPause.keyId: TerminalKey.mediaPlayPause, LogicalKeyboardKey.speechInputToggle.keyId: TerminalKey.speechInputToggle, - // LogicalKeyboardKey.bassBoost.keyId: TerminalKey.bassBoost, - // LogicalKeyboardKey.mediaSelect.keyId: TerminalKey.mediaSelect, LogicalKeyboardKey.launchWordProcessor.keyId: TerminalKey.launchWordProcessor, LogicalKeyboardKey.launchSpreadsheet.keyId: TerminalKey.launchSpreadsheet, LogicalKeyboardKey.launchMail.keyId: TerminalKey.launchMail, LogicalKeyboardKey.launchContacts.keyId: TerminalKey.launchContacts, LogicalKeyboardKey.launchCalendar.keyId: TerminalKey.launchCalendar, - // LogicalKeyboardKey.launchApp2.keyId: TerminalKey.launchApp2, - // LogicalKeyboardKey.launchApp1.keyId: TerminalKey.launchApp1, - // LogicalKeyboardKey.launchInternetBrowser.keyId: - // TerminalKey.launchInternetBrowser, LogicalKeyboardKey.logOff.keyId: TerminalKey.logOff, - // LogicalKeyboardKey.lockScreen.keyId: TerminalKey.lockScreen, LogicalKeyboardKey.launchControlPanel.keyId: TerminalKey.launchControlPanel, - // LogicalKeyboardKey.selectTask.keyId: TerminalKey.selectTask, - // LogicalKeyboardKey.launchDocuments.keyId: TerminalKey.launchDocuments, LogicalKeyboardKey.spellCheck.keyId: TerminalKey.spellCheck, - // LogicalKeyboardKey.launchKeyboardLayout.keyId: - // TerminalKey.launchKeyboardLayout, LogicalKeyboardKey.launchScreenSaver.keyId: TerminalKey.launchScreenSaver, LogicalKeyboardKey.launchAssistant.keyId: TerminalKey.launchAssistant, - // LogicalKeyboardKey.launchAudioBrowser.keyId: TerminalKey.launchAudioBrowser, LogicalKeyboardKey.newKey.keyId: TerminalKey.newKey, LogicalKeyboardKey.close.keyId: TerminalKey.close, LogicalKeyboardKey.save.keyId: TerminalKey.save, @@ -237,9 +203,6 @@ final _idKeyMap = { LogicalKeyboardKey.mailReply.keyId: TerminalKey.mailReply, LogicalKeyboardKey.mailForward.keyId: TerminalKey.mailForward, LogicalKeyboardKey.mailSend.keyId: TerminalKey.mailSend, - // LogicalKeyboardKey.keyboardLayoutSelect.keyId: - // TerminalKey.keyboardLayoutSelect, - // LogicalKeyboardKey.showAllWindows.keyId: TerminalKey.showAllWindows, LogicalKeyboardKey.gameButton1.keyId: TerminalKey.gameButton1, LogicalKeyboardKey.gameButton2.keyId: TerminalKey.gameButton2, LogicalKeyboardKey.gameButton3.keyId: TerminalKey.gameButton3, @@ -277,8 +240,6 @@ final _idKeyMap = { LogicalKeyboardKey.meta.keyId: TerminalKey.meta, LogicalKeyboardKey.alt.keyId: TerminalKey.alt, LogicalKeyboardKey.control.keyId: TerminalKey.control, - // LogicalKeyboardKey.backtab.keyId: TerminalKey.backtab, - // LogicalKeyboardKey.returnKey.keyId: TerminalKey.returnKey, }; TerminalKey? inputMap(LogicalKeyboardKey key) { diff --git a/lib/ui/keyboard_visibility.dart b/lib/ui/keyboard_visibility.dart new file mode 100644 index 00000000..85796799 --- /dev/null +++ b/lib/ui/keyboard_visibility.dart @@ -0,0 +1,60 @@ +import 'dart:ui'; + +import 'package:flutter/widgets.dart'; + +class KeyboardVisibilty extends StatefulWidget { + const KeyboardVisibilty({ + Key? key, + required this.child, + this.onKeyboardShow, + this.onKeyboardHide, + }) : super(key: key); + + final Widget child; + + final VoidCallback? onKeyboardShow; + + final VoidCallback? onKeyboardHide; + + @override + KeyboardVisibiltyState createState() => KeyboardVisibiltyState(); +} + +class KeyboardVisibiltyState extends State + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + final bottomInset = window.viewInsets.bottom; + + if (bottomInset != _lastBottomInset) { + if (bottomInset > 0) { + widget.onKeyboardShow?.call(); + } else { + widget.onKeyboardHide?.call(); + } + } + + _lastBottomInset = bottomInset; + + super.didChangeMetrics(); + } + + var _lastBottomInset = 0.0; + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/ui/palette_builder.dart b/lib/ui/palette_builder.dart new file mode 100644 index 00000000..c18196a5 --- /dev/null +++ b/lib/ui/palette_builder.dart @@ -0,0 +1,118 @@ +import 'package:flutter/widgets.dart'; +import 'package:xterm/ui/terminal_theme.dart'; +import 'package:xterm/utils/lookup_table.dart'; + +class PaletteBuilder { + final TerminalTheme theme; + + PaletteBuilder(this.theme); + + List build() { + return List.generate( + 256, + paletteColor, + growable: false, + ); + } + + /// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + Color paletteColor(int colNum) { + switch (colNum) { + case 0: + return theme.black; + case 1: + return theme.red; + case 2: + return theme.green; + case 3: + return theme.yellow; + case 4: + return theme.blue; + case 5: + return theme.magenta; + case 6: + return theme.cyan; + case 7: + return theme.white; + case 8: + return theme.brightBlack; + case 9: + return theme.brightRed; + case 10: + return theme.brightGreen; + case 11: + return theme.brightYellow; + case 12: + return theme.brightBlue; + case 13: + return theme.brightMagenta; + case 14: + return theme.brightCyan; + case 15: + return theme.white; + } + + if (colNum < 232) { + var r = 0; + var g = 0; + var b = 0; + + final index = colNum - 16; + + for (var i = 0; i < index; i++) { + if (b == 0) { + b = 95; + } else if (b < 255) { + b += 40; + } else { + b = 0; + if (g == 0) { + g = 95; + } else if (g < 255) { + g += 40; + } else { + g = 0; + if (r == 0) { + r = 95; + } else if (r < 255) { + r += 40; + } else { + break; + } + } + } + } + + return Color.fromARGB(0xFF, r, g, b); + } + + return Color(_grayscaleColors[colNum.clamp(232, 255)]!); + } +} + +final _grayscaleColors = FastLookupTable({ + 232: 0xff080808, + 233: 0xff121212, + 234: 0xff1c1c1c, + 235: 0xff262626, + 236: 0xff303030, + 237: 0xff3a3a3a, + 238: 0xff444444, + 239: 0xff4e4e4e, + 240: 0xff585858, + 241: 0xff626262, + 242: 0xff6c6c6c, + 243: 0xff767676, + 244: 0xff808080, + 245: 0xff8a8a8a, + 246: 0xff949494, + 247: 0xff9e9e9e, + 248: 0xffa8a8a8, + 249: 0xffb2b2b2, + 250: 0xffbcbcbc, + 251: 0xffc6c6c6, + 252: 0xffd0d0d0, + 253: 0xffdadada, + 254: 0xffe4e4e4, + 255: 0xffeeeeee, +}); diff --git a/lib/frontend/cache.dart b/lib/ui/paragraph_cache.dart similarity index 74% rename from lib/frontend/cache.dart rename to lib/ui/paragraph_cache.dart index 8cd6abe6..7d4df374 100644 --- a/lib/frontend/cache.dart +++ b/lib/ui/paragraph_cache.dart @@ -1,24 +1,19 @@ import 'dart:ui'; -import 'package:flutter/painting.dart'; +import 'package:flutter/widgets.dart'; import 'package:quiver/collection.dart'; -class TextLayoutCache { - TextLayoutCache(this.textDirection, int maximumSize) +class ParagraphCache { + ParagraphCache(int maximumSize) : _cache = LruMap(maximumSize: maximumSize); final LruMap _cache; - final TextDirection textDirection; - - void clear() { - _cache.clear(); - } Paragraph? getLayoutFromCache(int key) { return _cache[key]; } - Paragraph performAndCacheLayout(String text, TextStyle style, int? key) { + Paragraph performAndCacheLayout(String text, TextStyle style, int key) { final builder = ParagraphBuilder(style.getParagraphStyle()); builder.pushStyle(style.getTextStyle()); builder.addText(text); @@ -26,12 +21,14 @@ class TextLayoutCache { final paragraph = builder.build(); paragraph.layout(ParagraphConstraints(width: double.infinity)); - if (key != null) { - _cache[key] = paragraph; - } + _cache[key] = paragraph; return paragraph; } + void clear() { + _cache.clear(); + } + int get length { return _cache.length; } diff --git a/lib/ui/render.dart b/lib/ui/render.dart new file mode 100644 index 00000000..abb37fc9 --- /dev/null +++ b/lib/ui/render.dart @@ -0,0 +1,618 @@ +import 'dart:math' show min, max; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:xterm/core/buffer/cell_flags.dart'; +import 'package:xterm/core/buffer/position.dart'; +import 'package:xterm/core/buffer/range.dart'; +import 'package:xterm/core/cell.dart'; +import 'package:xterm/core/buffer/line.dart'; +import 'package:xterm/core/terminal.dart'; +import 'package:xterm/ui/controller.dart'; +import 'package:xterm/ui/cursor_type.dart'; +import 'package:xterm/ui/palette_builder.dart'; +import 'package:xterm/ui/paragraph_cache.dart'; +import 'package:xterm/ui/terminal_size.dart'; +import 'package:xterm/ui/terminal_text_style.dart'; +import 'package:xterm/ui/terminal_theme.dart'; + +typedef EditableRectCallback = void Function(Rect rect, Rect caretRect); + +class RenderTerminal extends RenderBox { + RenderTerminal({ + required Terminal terminal, + required TerminalController controller, + required ViewportOffset offset, + required EdgeInsets padding, + required bool autoResize, + required Size charMetrics, + required TerminalStyle textStyle, + required TerminalTheme theme, + required FocusNode focusNode, + required TerminalCursorType cursorType, + required bool alwaysShowCursor, + EditableRectCallback? onEditableRect, + String? composingText, + }) : _terminal = terminal, + _controller = controller, + _offset = offset, + _padding = padding, + _autoResize = autoResize, + _charMetrics = charMetrics, + _textStyle = textStyle, + _theme = theme, + _focusNode = focusNode, + _cursorType = cursorType, + _alwaysShowCursor = alwaysShowCursor, + _onEditableRect = onEditableRect, + _composingText = composingText { + _updateColorPalette(); + } + + Terminal _terminal; + set terminal(Terminal terminal) { + if (_terminal == terminal) return; + if (attached) _terminal.removeListener(_onTerminalChange); + _terminal = terminal; + if (attached) _terminal.addListener(_onTerminalChange); + _resizeTerminalIfNeeded(); + markNeedsLayout(); + } + + TerminalController _controller; + set controller(TerminalController controller) { + if (_controller == controller) return; + if (attached) _controller.removeListener(_onControllerUpdate); + _controller = controller; + if (attached) _controller.addListener(_onControllerUpdate); + markNeedsLayout(); + } + + ViewportOffset _offset; + set offset(ViewportOffset value) { + if (value == _offset) return; + if (attached) _offset.removeListener(_hasScrolled); + _offset = value; + if (attached) _offset.addListener(_hasScrolled); + markNeedsLayout(); + } + + EdgeInsets _padding; + set padding(EdgeInsets value) { + if (value == _padding) return; + _padding = value; + markNeedsLayout(); + } + + bool _autoResize; + set autoResize(bool value) { + if (value == _autoResize) return; + _autoResize = value; + markNeedsLayout(); + } + + Size _charMetrics; + set charMetrics(Size value) { + if (value == _charMetrics) return; + _charMetrics = value; + markNeedsLayout(); + } + + TerminalStyle _textStyle; + set textStyle(TerminalStyle value) { + if (value == _textStyle) return; + _textStyle = value; + markNeedsLayout(); + } + + TerminalTheme _theme; + set theme(TerminalTheme value) { + if (value == _theme) return; + _theme = value; + _updateColorPalette(); + markNeedsPaint(); + } + + FocusNode _focusNode; + set focusNode(FocusNode value) { + if (value == _focusNode) return; + if (attached) _focusNode.removeListener(_onFocusChange); + _focusNode = value; + if (attached) _focusNode.addListener(_onFocusChange); + markNeedsPaint(); + } + + TerminalCursorType _cursorType; + set cursorType(TerminalCursorType value) { + if (value == _cursorType) return; + _cursorType = value; + markNeedsPaint(); + } + + bool _alwaysShowCursor; + set alwaysShowCursor(bool value) { + if (value == _alwaysShowCursor) return; + _alwaysShowCursor = value; + markNeedsPaint(); + } + + EditableRectCallback? _onEditableRect; + set onEditableRect(EditableRectCallback? value) { + if (value == _onEditableRect) return; + _onEditableRect = value; + markNeedsLayout(); + } + + String? _composingText; + set composingText(String? value) { + if (value == _composingText) return; + _composingText = value; + markNeedsPaint(); + } + + final _paragraphCache = ParagraphCache(10240); + + late List _colorPalette; + + TerminalSize? _viewportSize; + + void _updateColorPalette() { + _colorPalette = PaletteBuilder(_theme).build(); + } + + var _stickToBottom = true; + + void _hasScrolled() { + _stickToBottom = _offset.pixels >= _maxScrollExtent; + + markNeedsLayout(); + } + + void _onFocusChange() { + markNeedsPaint(); + } + + void _onTerminalChange() { + markNeedsLayout(); + } + + void _onControllerUpdate() { + markNeedsLayout(); + } + + @override + final isRepaintBoundary = true; + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _offset.addListener(_hasScrolled); + _terminal.addListener(_onTerminalChange); + _controller.addListener(_onControllerUpdate); + _focusNode.addListener(_onFocusChange); + } + + @override + void detach() { + super.detach(); + _offset.removeListener(_hasScrolled); + _terminal.removeListener(_onTerminalChange); + _controller.removeListener(_onControllerUpdate); + _focusNode.removeListener(_onFocusChange); + } + + @override + bool hitTestSelf(Offset position) { + return true; + } + + @override + void performLayout() { + size = constraints.biggest; + + _updateViewportSize(); + + _updateScrollOffset(); + + if (_stickToBottom) { + _offset.correctBy(_maxScrollExtent - _offset.pixels); + } + + SchedulerBinding.instance + .addPostFrameCallback((_) => _notifyEditableRect()); + } + + double get lineHeight => _charMetrics.height; + + double get terminalHeight => + _terminal.buffer.lines.length * _charMetrics.height; + + double get scrollOffset => _offset.pixels; + + BufferPosition positionFromOffset(Offset offset) { + final x = offset.dx - _padding.left; + final y = offset.dy - _padding.top + _offset.pixels; + final row = y ~/ _charMetrics.height; + final col = x ~/ _charMetrics.width; + return BufferPosition(col, row); + } + + Offset offsetFromPosition(BufferPosition position) { + final row = position.y; + final col = position.x; + final x = col * _charMetrics.width; + final y = row * _charMetrics.height; + return Offset(x + _padding.left, y + _padding.top - _offset.pixels); + } + + void selectWord(Offset from, [Offset? to]) { + final fromOffset = positionFromOffset(globalToLocal(from)); + final fromBoundary = _terminal.buffer.getWordBoundary(fromOffset); + if (fromBoundary == null) return; + if (to == null) { + _controller.setSelection(fromBoundary); + } else { + final toOffset = positionFromOffset(globalToLocal(to)); + final toBoundary = _terminal.buffer.getWordBoundary(toOffset); + if (toBoundary == null) return; + _controller.setSelection(fromBoundary.merge(toBoundary)); + } + } + + void selectPosition(Offset from, [Offset? to]) { + final fromPosition = positionFromOffset(globalToLocal(from)); + if (to == null) { + _controller.setSelection(BufferRange.collapsed(fromPosition)); + } else { + final toPosition = positionFromOffset(globalToLocal(to)); + _controller.setSelection(BufferRange(fromPosition, toPosition)); + } + } + + void _notifyEditableRect() { + final cursor = localToGlobal(_cursorOffset); + + final rect = Rect.fromLTRB( + cursor.dx, + cursor.dy, + size.width, + cursor.dy + _charMetrics.height, + ); + + final caretRect = cursor & _charMetrics; + + _onEditableRect?.call(rect, caretRect); + } + + void _updateViewportSize() { + if (size <= _charMetrics) { + return; + } + + final viewportSize = TerminalSize( + size.width ~/ _charMetrics.width, + _viewportHeight ~/ _charMetrics.height, + ); + + if (_viewportSize != viewportSize) { + _viewportSize = viewportSize; + _resizeTerminalIfNeeded(); + } + } + + void _resizeTerminalIfNeeded() { + if (_autoResize && _viewportSize != null) { + _terminal.resize( + _viewportSize!.width, + _viewportSize!.height, + _charMetrics.width.round(), + _charMetrics.height.round(), + ); + } + } + + bool get _isComposingText { + return _composingText != null && _composingText!.isNotEmpty; + } + + bool get _shouldShowCursor { + return _terminal.cursorVisibleMode || _alwaysShowCursor || _isComposingText; + } + + double get _viewportHeight { + return size.height - _padding.vertical; + } + + double get _maxScrollExtent { + return max(terminalHeight - _viewportHeight, 0.0); + } + + double get _lineOffset { + return -_offset.pixels + _padding.top; + } + + Offset get _cursorOffset { + return Offset( + _terminal.buffer.cursorX * _charMetrics.width, + _terminal.buffer.absoluteCursorY * _charMetrics.height + _lineOffset, + ); + } + + void _updateScrollOffset() { + _offset.applyViewportDimension(_viewportHeight); + _offset.applyContentDimensions(0, _maxScrollExtent); + } + + @override + void paint(PaintingContext context, Offset offset) { + _paint(context, offset); + context.setWillChangeHint(); + } + + void _paint(PaintingContext context, Offset offset) { + final canvas = context.canvas; + + final lines = _terminal.buffer.lines; + final charHeight = _charMetrics.height; + + final firstLineOffset = _offset.pixels - _padding.top; + final lastLineOffset = _offset.pixels + size.height + _padding.bottom; + + final firstLine = firstLineOffset ~/ charHeight; + final lastLine = lastLineOffset ~/ charHeight; + + final effectFirstLine = firstLine.clamp(0, lines.length - 1); + final effectLastLine = lastLine.clamp(0, lines.length - 1); + + for (var i = effectFirstLine; i <= effectLastLine; i++) { + _paintLine( + canvas, + lines[i], + offset.translate(0, (i * charHeight + _lineOffset).truncateToDouble()), + ); + } + + if (_terminal.buffer.absoluteCursorY >= effectFirstLine && + _terminal.buffer.absoluteCursorY <= effectLastLine) { + final cursorOffset = offset + _cursorOffset; + + if (_isComposingText) { + _paintComposingText(canvas, cursorOffset); + } + + if (_shouldShowCursor) { + _paintCursor(canvas, cursorOffset); + } + } + + if (_controller.selection != null) { + _paintSelection( + canvas, + _controller.selection!, + effectFirstLine, + effectLastLine, + ); + } + } + + void _paintCursor(Canvas canvas, Offset offset) { + final paint = Paint() + ..color = _theme.cursor + ..strokeWidth = 1; + + if (!_focusNode.hasFocus) { + paint.style = PaintingStyle.stroke; + canvas.drawRect(offset & _charMetrics, paint); + return; + } + + switch (_cursorType) { + case TerminalCursorType.block: + paint.style = PaintingStyle.fill; + canvas.drawRect(offset & _charMetrics, paint); + return; + case TerminalCursorType.underline: + return canvas.drawLine( + Offset(offset.dx, _charMetrics.height - 1), + Offset(offset.dx + _charMetrics.width, _charMetrics.height - 1), + paint, + ); + case TerminalCursorType.verticalBar: + return canvas.drawLine( + Offset(offset.dx, 0), + Offset(offset.dx, _charMetrics.height), + paint, + ); + } + } + + void _paintComposingText(Canvas canvas, Offset offset) { + final composingText = _composingText; + + if (composingText == null) { + return; + } + + final style = _textStyle.toTextStyle( + color: _resolveForegroundColor(_terminal.cursor.foreground), + backgroundColor: _theme.background, + underline: true, + ); + + final builder = ParagraphBuilder(style.getParagraphStyle()); + builder.addPlaceholder( + offset.dx, + _charMetrics.height, + PlaceholderAlignment.middle, + ); + builder.pushStyle(style.getTextStyle()); + builder.addText(composingText); + + final paragraph = builder.build(); + paragraph.layout(ParagraphConstraints(width: size.width)); + + canvas.drawParagraph(paragraph, Offset(0, offset.dy)); + } + + void _paintLine(Canvas canvas, BufferLine line, Offset offset) { + final cellData = CellData.empty(); + final cellWidth = _charMetrics.width; + + final visibleCells = size.width ~/ cellWidth + 1; + final effectCells = min(visibleCells, line.length); + + for (var i = 0; i < effectCells; i++) { + line.getCellData(i, cellData); + final charWidth = cellData.content >> CellContent.widthShift; + final cellOffset = offset.translate(i * cellWidth, 0); + _paintCellBackground(canvas, cellOffset, cellData); + _paintCellForeground(canvas, cellOffset, line, cellData); + + if (charWidth == 2) { + i++; + } + } + } + + void _paintSelection( + Canvas canvas, + BufferRange selection, + int firstLine, + int lastLine, + ) { + for (final segment in selection.toSegments()) { + if (segment.line >= _terminal.buffer.lines.length) { + break; + } + + if (segment.line < firstLine) { + continue; + } + + if (segment.line > lastLine) { + break; + } + + final start = segment.start ?? 0; + final end = segment.end ?? _terminal.viewWidth; + + final startOffset = Offset( + start * _charMetrics.width, + segment.line * _charMetrics.height + _lineOffset, + ); + + final endOffset = Offset( + end * _charMetrics.width, + (segment.line + 1) * _charMetrics.height + _lineOffset, + ); + + final paint = Paint() + ..color = _theme.cursor + ..strokeWidth = 1; + + canvas.drawRect( + Rect.fromPoints(startOffset, endOffset), + paint, + ); + } + } + + @pragma('vm:prefer-inline') + void _paintCellForeground( + Canvas canvas, + Offset offset, + BufferLine line, + CellData cellData, + ) { + final charCode = cellData.content & CellContent.codepointMask; + if (charCode == 0) return; + + final hash = cellData.getHash(); + // final hash = cellData.getHash() + line.hashCode; + var paragraph = _paragraphCache.getLayoutFromCache(hash); + + if (paragraph == null) { + final cellFlags = cellData.flags; + + var color = cellFlags & CellFlags.inverse == 0 + ? _resolveForegroundColor(cellData.foreground) + : _resolveBackgroundColor(cellData.background); + + if (cellData.flags & CellFlags.faint != 0) { + color = color.withOpacity(0.5); + } + + final style = _textStyle.toTextStyle( + color: color, + bold: cellFlags & CellFlags.bold != 0, + italic: cellFlags & CellFlags.italic != 0, + underline: cellFlags & CellFlags.underline != 0, + ); + + paragraph = _paragraphCache.performAndCacheLayout( + String.fromCharCode(charCode), + style, + hash, + ); + } + + canvas.drawParagraph(paragraph, offset); + } + + @pragma('vm:prefer-inline') + void _paintCellBackground(Canvas canvas, Offset offset, CellData cellData) { + late Color color; + final colorType = cellData.background & CellColor.typeMask; + + if (cellData.flags & CellFlags.inverse != 0) { + color = _resolveForegroundColor(cellData.foreground); + } else if (colorType == CellColor.normal) { + return; + } else { + color = _resolveBackgroundColor(cellData.background); + } + + final paint = Paint()..color = color; + final doubleWidth = cellData.content >> CellContent.widthShift == 2; + final widthScale = doubleWidth ? 2 : 1; + final size = Size(_charMetrics.width * widthScale + 1, _charMetrics.height); + canvas.drawRect(offset & size, paint); + } + + @pragma('vm:prefer-inline') + Color _resolveForegroundColor(int cellColor) { + final colorType = cellColor & CellColor.typeMask; + final colorValue = cellColor & CellColor.valueMask; + + switch (colorType) { + case CellColor.normal: + return _theme.foreground; + case CellColor.named: + case CellColor.palette: + return _colorPalette[colorValue]; + case CellColor.rgb: + default: + return Color(colorValue | 0xFF000000); + } + } + + @pragma('vm:prefer-inline') + Color _resolveBackgroundColor(int cellColor) { + final colorType = cellColor & CellColor.typeMask; + final colorValue = cellColor & CellColor.valueMask; + + switch (colorType) { + case CellColor.normal: + return _theme.background; + case CellColor.named: + case CellColor.palette: + return _colorPalette[colorValue]; + case CellColor.rgb: + default: + return Color(colorValue | 0xFF000000); + } + } +} diff --git a/lib/ui/terminal_size.dart b/lib/ui/terminal_size.dart new file mode 100644 index 00000000..07258c9a --- /dev/null +++ b/lib/ui/terminal_size.dart @@ -0,0 +1,24 @@ +class TerminalSize { + final int width; + + final int height; + + const TerminalSize(this.width, this.height); + + @override + String toString() => 'TerminalSize($width, $height)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! TerminalSize) { + return false; + } + return other.width == width && other.height == height; + } + + @override + int get hashCode => width.hashCode ^ height.hashCode; +} diff --git a/lib/ui/terminal_text_style.dart b/lib/ui/terminal_text_style.dart new file mode 100644 index 00000000..8da3b287 --- /dev/null +++ b/lib/ui/terminal_text_style.dart @@ -0,0 +1,87 @@ +import 'package:flutter/widgets.dart'; + +const _kDefaultFontSize = 13.0; + +const _kDefaultHeight = 1.2; + +const _kDefaultFontFamily = 'monospace'; + +const _kDefaultFontFamilyFallback = [ + 'Menlo', + 'Monaco', + 'Consolas', + 'Liberation Mono', + 'Courier New', + 'Noto Sans Mono CJK SC', + 'Noto Sans Mono CJK TC', + 'Noto Sans Mono CJK KR', + 'Noto Sans Mono CJK JP', + 'Noto Sans Mono CJK HK', + 'Noto Color Emoji', + 'Noto Sans Symbols', + 'monospace', + 'sans-serif', +]; + +class TerminalStyle { + const TerminalStyle({ + this.fontSize = _kDefaultFontSize, + this.height = _kDefaultHeight, + this.fontFamily = _kDefaultFontFamily, + this.fontFamilyFallback = _kDefaultFontFamilyFallback, + }); + + factory TerminalStyle.fromTextStyle(TextStyle textStyle) { + return TerminalStyle( + fontSize: textStyle.fontSize ?? _kDefaultFontSize, + height: textStyle.height ?? _kDefaultHeight, + fontFamily: textStyle.fontFamily ?? + textStyle.fontFamilyFallback?.first ?? + _kDefaultFontFamily, + fontFamilyFallback: + textStyle.fontFamilyFallback ?? _kDefaultFontFamilyFallback, + ); + } + + final double fontSize; + + final double height; + + final String fontFamily; + + final List fontFamilyFallback; + + TextStyle toTextStyle({ + Color? color, + Color? backgroundColor, + bool bold = false, + bool italic = false, + bool underline = false, + }) { + return TextStyle( + fontSize: fontSize, + height: height, + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + color: color, + backgroundColor: backgroundColor, + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + fontStyle: italic ? FontStyle.italic : FontStyle.normal, + decoration: underline ? TextDecoration.underline : TextDecoration.none, + ); + } + + TerminalStyle copyWith({ + double? fontSize, + double? height, + String? fontFamily, + List? fontFamilyFallback, + }) { + return TerminalStyle( + fontSize: fontSize ?? this.fontSize, + height: height ?? this.height, + fontFamily: fontFamily ?? this.fontFamily, + fontFamilyFallback: fontFamilyFallback ?? this.fontFamilyFallback, + ); + } +} diff --git a/lib/ui/terminal_theme.dart b/lib/ui/terminal_theme.dart new file mode 100644 index 00000000..0b70e422 --- /dev/null +++ b/lib/ui/terminal_theme.dart @@ -0,0 +1,57 @@ +import 'package:flutter/widgets.dart'; + +class TerminalTheme { + const TerminalTheme({ + required this.cursor, + required this.selection, + required this.foreground, + required this.background, + required this.black, + required this.white, + required this.red, + required this.green, + required this.yellow, + required this.blue, + required this.magenta, + required this.cyan, + required this.brightBlack, + required this.brightRed, + required this.brightGreen, + required this.brightYellow, + required this.brightBlue, + required this.brightMagenta, + required this.brightCyan, + required this.brightWhite, + required this.searchHitBackground, + required this.searchHitBackgroundCurrent, + required this.searchHitForeground, + }); + + final Color cursor; + final Color selection; + + final Color foreground; + final Color background; + + final Color black; + final Color red; + final Color green; + final Color yellow; + final Color blue; + final Color magenta; + final Color cyan; + final Color white; + + final Color brightBlack; + final Color brightRed; + final Color brightGreen; + final Color brightYellow; + final Color brightBlue; + final Color brightMagenta; + final Color brightCyan; + final Color brightWhite; + + final Color searchHitBackground; + final Color searchHitBackgroundCurrent; + final Color searchHitForeground; +} diff --git a/lib/ui/terminal_view.dart b/lib/ui/terminal_view.dart new file mode 100644 index 00000000..61bcf1d2 --- /dev/null +++ b/lib/ui/terminal_view.dart @@ -0,0 +1,396 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:xterm/core/terminal.dart'; +import 'package:xterm/ui/cursor_type.dart'; + +import 'package:xterm/ui/input_map.dart'; +import 'package:xterm/core/input/keys.dart'; +import 'package:xterm/ui/char_metrics.dart'; +import 'package:xterm/ui/controller.dart'; +import 'package:xterm/ui/custom_text_edit.dart'; +import 'package:xterm/ui/gesture/gesture_handler.dart'; +import 'package:xterm/ui/keyboard_visibility.dart'; +import 'package:xterm/ui/render.dart'; +import 'package:xterm/ui/terminal_text_style.dart'; +import 'package:xterm/ui/terminal_theme.dart'; +import 'package:xterm/ui/themes.dart'; + +class TerminalView extends StatefulWidget { + const TerminalView( + this.terminal, { + Key? key, + this.controller, + this.theme = TerminalThemes.defaultTheme, + this.textStyle = const TerminalStyle(), + this.padding, + this.scrollController, + this.autoResize = true, + this.backgroundOpacity = 1, + this.focusNode, + this.autofocus = false, + this.onTap, + this.mouseCursor = SystemMouseCursors.text, + this.keyboardType = TextInputType.emailAddress, + this.keyboardAppearance = Brightness.dark, + this.cursorType = TerminalCursorType.block, + this.alwaysShowCursor = false, + this.deleteDetection = false, + }) : super(key: key); + + /// The underlying terminal that this widget renders. + final Terminal terminal; + + final TerminalController? controller; + + /// The theme to use for this terminal. + final TerminalTheme theme; + + /// The style to use for painting characters. + final TerminalStyle textStyle; + + /// Padding around the inner [Scrollable] widget. + final EdgeInsets? padding; + + /// Scroll controller for the inner [Scrollable] widget. + final ScrollController? scrollController; + + /// Should this widget automatically notify the underlying terminal when its + /// size changes. [true] by default. + final bool autoResize; + + /// Opacity of the terminal background. Set to 0 to make the terminal + /// background transparent. + final double backgroundOpacity; + + /// An optional focus node to use as the focus node for this widget. + final FocusNode? focusNode; + + /// True if this widget will be selected as the initial focus when no other + /// node in its scope is currently focused. + final bool autofocus; + + /// Callback for when the user taps on the terminal. + /// + /// This exists because [TerminalView] builds a [GestureDetector] internally + /// to to trigger focus requests, adjust the selection, etc. Handling some of + /// those events by wrapping [TerminalView] with a competingGestureDetector is + /// problematic. + final VoidCallback? onTap; + + /// The mouse cursor for mouse pointers that are hovering over the terminal. + /// [SystemMouseCursors.text] by default. + final MouseCursor mouseCursor; + + /// The type of information for which to optimize the text input control. + /// [TextInputType.emailAddress] by default. + final TextInputType keyboardType; + + /// The appearance of the keyboard. [Brightness.dark] by default. + /// + /// This setting is only honored on iOS devices. + final Brightness keyboardAppearance; + + /// The type of cursor to use. [TerminalCursorType.block] by default. + final TerminalCursorType cursorType; + + /// Whether to always show the cursor. This is useful for debugging. + /// [false] by default. + final bool alwaysShowCursor; + + /// Workaround to detect delete key for platforms and IMEs that does not + /// emit hardware delete event. Prefered on mobile platforms. [false] by + /// default. + final bool deleteDetection; + + @override + State createState() => TerminalViewState(); +} + +class TerminalViewState extends State { + late final FocusNode _focusNode; + + final _customTextEditKey = GlobalKey(); + + final _scrollableKey = GlobalKey(); + + final _viewportKey = GlobalKey(); + + String? _composingText; + + late TerminalController _controller; + + late ScrollController _scrollController; + + RenderTerminal get renderTerminal => + _viewportKey.currentContext!.findRenderObject() as RenderTerminal; + + @override + void initState() { + _focusNode = widget.focusNode ?? FocusNode(); + _controller = widget.controller ?? TerminalController(); + _scrollController = widget.scrollController ?? ScrollController(); + super.initState(); + } + + @override + void didUpdateWidget(TerminalView oldWidget) { + if (oldWidget.focusNode != widget.focusNode) { + _focusNode = widget.focusNode ?? FocusNode(); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + void requestKeyboard() { + _customTextEditKey.currentState?.requestKeyboard(); + } + + void closeKeyboard() { + _customTextEditKey.currentState?.closeKeyboard(); + } + + bool get hasInputConnection { + return _customTextEditKey.currentState?.hasInputConnection == true; + } + + KeyEventResult _onKeyEvent(RawKeyEvent event) { + if (event is! RawKeyDownEvent) { + return KeyEventResult.ignored; + } + + final key = inputMap(event.logicalKey); + + if (key == null) { + return KeyEventResult.ignored; + } + + final handled = widget.terminal.keyInput( + key, + ctrl: event.isControlPressed, + alt: event.isAltPressed, + shift: event.isShiftPressed, + ); + + if (handled) { + _scrollToBottom(); + } + + return handled ? KeyEventResult.handled : KeyEventResult.ignored; + } + + void _onKeyboardShow() { + if (_focusNode.hasFocus) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToBottom(); + }); + } + } + + void _onEditableRect(Rect rect, Rect caretRect) { + _customTextEditKey.currentState?.setEditableRect(rect, caretRect); + } + + void _scrollToBottom() { + final position = _scrollableKey.currentState?.position; + if (position != null) { + position.jumpTo(position.maxScrollExtent); + } + } + + @override + Widget build(BuildContext context) { + // Calculate everytime build happens, because some fonts library + // lazily load fonts (such as google_fonts) and this can change the + // font metrics while textStyle is still the same. + final charMetrics = calcCharMetrics(widget.textStyle); + + Widget child = Scrollable( + key: _scrollableKey, + controller: _scrollController, + viewportBuilder: (context, offset) { + return _TerminalView( + key: _viewportKey, + terminal: widget.terminal, + controller: _controller, + offset: offset, + padding: MediaQuery.of(context).padding, + autoResize: widget.autoResize, + charMetrics: charMetrics, + textStyle: widget.textStyle, + theme: widget.theme, + focusNode: _focusNode, + cursorType: widget.cursorType, + alwaysShowCursor: widget.alwaysShowCursor, + onEditableRect: _onEditableRect, + composingText: _composingText, + ); + }, + ); + + child = Container( + color: widget.theme.background.withOpacity(widget.backgroundOpacity), + padding: widget.padding, + child: child, + ); + + child = CustomTextEdit( + key: _customTextEditKey, + focusNode: _focusNode, + inputType: widget.keyboardType, + keyboardAppearance: widget.keyboardAppearance, + deleteDetection: widget.deleteDetection, + onInsert: (text) { + _scrollToBottom(); + widget.terminal.textInput(text); + }, + onDelete: () { + _scrollToBottom(); + widget.terminal.keyInput(TerminalKey.backspace); + }, + onComposing: (text) { + setState(() => _composingText = text); + }, + onAction: (action) { + _scrollToBottom(); + if (action == TextInputAction.done) { + widget.terminal.keyInput(TerminalKey.enter); + } + }, + onKey: _onKeyEvent, + child: child, + ); + + child = KeyboardVisibilty( + onKeyboardShow: _onKeyboardShow, + child: child, + ); + + child = TerminalGestureHandler( + terminalView: this, + onTapUp: _onTapUp, + onTapDown: _onTapDown, + onSecondaryTapDown: _onSecondaryTapDown, + child: child, + ); + + child = MouseRegion( + cursor: widget.mouseCursor, + child: child, + ); + + return child; + } + + void _onTapUp(_) { + widget.onTap?.call(); + } + + void _onTapDown(_) { + if (_controller.hasSelection) { + _controller.clearSelection(); + } else { + _customTextEditKey.currentState?.requestKeyboard(); + } + } + + void _onSecondaryTapDown(TapDownDetails details) { + final position = renderTerminal.positionFromOffset( + renderTerminal.globalToLocal(details.globalPosition), + ); + + final selection = _controller.selection; + + if (selection == null || !selection.isWithin(position)) { + renderTerminal.selectWord(details.globalPosition); + } + } +} + +class _TerminalView extends LeafRenderObjectWidget { + const _TerminalView({ + Key? key, + required this.terminal, + required this.controller, + required this.offset, + required this.padding, + required this.autoResize, + required this.charMetrics, + required this.textStyle, + required this.theme, + required this.focusNode, + required this.cursorType, + required this.alwaysShowCursor, + this.onEditableRect, + this.composingText, + }) : super(key: key); + + final Terminal terminal; + + final TerminalController controller; + + final ViewportOffset offset; + + final EdgeInsets padding; + + final bool autoResize; + + final Size charMetrics; + + final TerminalStyle textStyle; + + final TerminalTheme theme; + + final FocusNode focusNode; + + final TerminalCursorType cursorType; + + final bool alwaysShowCursor; + + final EditableRectCallback? onEditableRect; + + final String? composingText; + + @override + RenderTerminal createRenderObject(BuildContext context) { + return RenderTerminal( + terminal: terminal, + controller: controller, + offset: offset, + padding: padding, + autoResize: autoResize, + charMetrics: charMetrics, + textStyle: textStyle, + theme: theme, + focusNode: focusNode, + cursorType: cursorType, + alwaysShowCursor: alwaysShowCursor, + onEditableRect: onEditableRect, + composingText: composingText, + ); + } + + @override + void updateRenderObject(BuildContext context, RenderTerminal renderObject) { + renderObject + ..terminal = terminal + ..controller = controller + ..offset = offset + ..padding = padding + ..autoResize = autoResize + ..charMetrics = charMetrics + ..textStyle = textStyle + ..theme = theme + ..focusNode = focusNode + ..cursorType = cursorType + ..alwaysShowCursor = alwaysShowCursor + ..onEditableRect = onEditableRect + ..composingText = composingText; + } +} diff --git a/lib/ui/themes.dart b/lib/ui/themes.dart new file mode 100644 index 00000000..7e44ea7a --- /dev/null +++ b/lib/ui/themes.dart @@ -0,0 +1,56 @@ +import 'package:flutter/widgets.dart'; +import 'package:xterm/ui/terminal_theme.dart'; + +class TerminalThemes { + static const defaultTheme = TerminalTheme( + cursor: Color(0XAAAEAFAD), + selection: Color(0XFFFFFF40), + foreground: Color(0XFFCCCCCC), + background: Color(0XFF1E1E1E), + black: Color(0XFF000000), + red: Color(0XFFCD3131), + green: Color(0XFF0DBC79), + yellow: Color(0XFFE5E510), + blue: Color(0XFF2472C8), + magenta: Color(0XFFBC3FBC), + cyan: Color(0XFF11A8CD), + white: Color(0XFFE5E5E5), + brightBlack: Color(0XFF666666), + brightRed: Color(0XFFF14C4C), + brightGreen: Color(0XFF23D18B), + brightYellow: Color(0XFFF5F543), + brightBlue: Color(0XFF3B8EEA), + brightMagenta: Color(0XFFD670D6), + brightCyan: Color(0XFF29B8DB), + brightWhite: Color(0XFFFFFFFF), + searchHitBackground: Color(0XFFFFFF2B), + searchHitBackgroundCurrent: Color(0XFF31FF26), + searchHitForeground: Color(0XFF000000), + ); + + static const whiteOnBlack = TerminalTheme( + cursor: Color(0XFFAEAFAD), + selection: Color(0XFFFFFF40), + foreground: Color(0XFFFFFFFF), + background: Color(0XFF000000), + black: Color(0XFF000000), + red: Color(0XFFCD3131), + green: Color(0XFF0DBC79), + yellow: Color(0XFFE5E510), + blue: Color(0XFF2472C8), + magenta: Color(0XFFBC3FBC), + cyan: Color(0XFF11A8CD), + white: Color(0XFFE5E5E5), + brightBlack: Color(0XFF666666), + brightRed: Color(0XFFF14C4C), + brightGreen: Color(0XFF23D18B), + brightYellow: Color(0XFFF5F543), + brightBlue: Color(0XFF3B8EEA), + brightMagenta: Color(0XFFD670D6), + brightCyan: Color(0XFF29B8DB), + brightWhite: Color(0XFFFFFFFF), + searchHitBackground: Color(0XFFFFFF2B), + searchHitBackgroundCurrent: Color(0XFF31FF26), + searchHitForeground: Color(0XFF000000), + ); +} diff --git a/lib/util/ansi_color.dart b/lib/util/ansi_color.dart deleted file mode 100644 index 8b49a95a..00000000 --- a/lib/util/ansi_color.dart +++ /dev/null @@ -1,25 +0,0 @@ -class AnsiColor { - static String red(Object text) { - return '\x1b[31m$text\x1b[39m'; - } - - static String green(Object text) { - return '\x1b[32m$text\x1b[39m'; - } - - static String yellow(Object text) { - return '\x1b[33m$text\x1b[39m'; - } - - static String blue(Object text) { - return '\x1b[34m$text\x1b[39m'; - } - - static String magenta(Object text) { - return '\x1b[35m$text\x1b[39m'; - } - - static String cyan(Object text) { - return '\x1b[36m$text\x1b[39m'; - } -} diff --git a/lib/util/bit_flags.dart b/lib/util/bit_flags.dart deleted file mode 100644 index 69b6fab4..00000000 --- a/lib/util/bit_flags.dart +++ /dev/null @@ -1,5 +0,0 @@ -extension BitFlags on int { - bool hasFlag(int flag) { - return this & flag != 0; - } -} diff --git a/lib/util/constants.dart b/lib/util/constants.dart deleted file mode 100644 index 6bfdde50..00000000 --- a/lib/util/constants.dart +++ /dev/null @@ -1,27 +0,0 @@ -const kReleaseMode = bool.fromEnvironment( - 'dart.vm.product', - defaultValue: false, -); - -const kProfileMode = bool.fromEnvironment( - 'dart.vm.profile', - defaultValue: false, -); - -const kDebugMode = !kReleaseMode && !kProfileMode; - -const kIsWeb = identical(0, 0.0); - -final kWordSeparators = [ - String.fromCharCode(0), - ' ', - '.', - ':', - '-', - '\'', - '"', - '*', - '+', - '/', - '\\' -]; diff --git a/lib/util/debug_handler.dart b/lib/util/debug_handler.dart deleted file mode 100644 index 9025554c..00000000 --- a/lib/util/debug_handler.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:convert/convert.dart'; -import 'package:xterm/terminal/csi.dart'; -import 'package:xterm/util/ansi_color.dart'; - -class DebugHandler { - final _buffer = StringBuffer(); - - var _enabled = false; - - void enable([bool enabled = true]) { - _enabled = enabled; - } - - void _checkBuffer() { - if (!_enabled) return; - - if (_buffer.isNotEmpty) { - print(AnsiColor.cyan('─') + _buffer.toString() + AnsiColor.cyan('β”œ')); - _buffer.clear(); - } - } - - void onCsi(CSI csi) { - if (!_enabled) return; - _checkBuffer(); - print(AnsiColor.green('')); - } - - void onEsc(int charAfterEsc) { - if (!_enabled) return; - _checkBuffer(); - print(AnsiColor.green('')); - } - - void onOsc(List params) { - if (!_enabled) return; - _checkBuffer(); - print(AnsiColor.yellow('')); - } - - void onSbc(int codePoint) { - if (!_enabled) return; - _checkBuffer(); - print(AnsiColor.magenta('')); - } - - void onChar(int codePoint) { - if (!_enabled) return; - _buffer.writeCharCode(codePoint); - } - - void onMetrics(String metrics) { - if (!_enabled) return; - print(AnsiColor.blue('')); - } - - void onError(String error) { - if (!_enabled) return; - print(AnsiColor.red('')); - } - - void onMsg(Object msg) { - if (!_enabled) return; - print(AnsiColor.green('')); - } -} diff --git a/lib/util/event_debouncer.dart b/lib/util/event_debouncer.dart deleted file mode 100644 index bdeb943c..00000000 --- a/lib/util/event_debouncer.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:async'; - -/// EventDebouncer makes sure that events aren't fired at a higher frequency -/// than specified. -/// To ensure that EventDebouncer will ignore events that happen in between -/// and just call the latest event that happened. -class EventDebouncer { - final Duration _debounceDuration; - Timer? _debounceTimer; - Function? _latestCallback; - - EventDebouncer(this._debounceDuration); - - void _consumeLatestCallback() { - if (!(_debounceTimer?.isActive ?? false)) { - _debounceTimer = null; - } - - if (_latestCallback == null) { - return; - } - - if (_debounceTimer == null) { - _latestCallback!(); - _latestCallback = null; - _debounceTimer = Timer( - _debounceDuration, - () { - _consumeLatestCallback(); - }, - ); - } - } - - void notifyEvent(Function callback) { - _latestCallback = callback; - _consumeLatestCallback(); - } -} diff --git a/lib/util/scroll_range.dart b/lib/util/scroll_range.dart deleted file mode 100644 index 8341b161..00000000 --- a/lib/util/scroll_range.dart +++ /dev/null @@ -1,6 +0,0 @@ -class ScrollRange { - ScrollRange(this.top, this.bottom); - - final int top; - final int bottom; -} diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 00000000..c29919fe --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,2 @@ +export 'utils/debugger.dart'; +export 'utils/debugger_view.dart'; diff --git a/lib/utils/ascii.dart b/lib/utils/ascii.dart new file mode 100644 index 00000000..b5a67ab6 --- /dev/null +++ b/lib/utils/ascii.dart @@ -0,0 +1,403 @@ +// ignore_for_file: constant_identifier_names + +abstract class Ascii { + /* + * Helper functions + */ + + static bool isNonPrintable(int c) { + return c < 32 || c == 127; + } + + /* + * Non-printable ASCII characters + */ + + /// Null character + static const NULL = 00; + + /// Start of Header + static const SOH = 01; + + /// Start of Text + static const STX = 02; + + /// End of Text, hearts card suit + static const ETX = 03; + + /// End of Transmission, diamonds card suit + static const EOT = 04; + + /// Enquiry, clubs card suit + static const ENQ = 05; + + /// Acknowledgement, spade card suit + static const ACK = 06; + + /// Bell + static const BEL = 07; + + /// Backspace + static const BS = 08; + + /// Horizontal Tab + static const HT = 09; + + /// Line feed + static const LF = 10; + + /// Vertical Tab, male symbol, symbol for Mars + static const VT = 11; + + /// Form feed, female symbol, symbol for Venus + static const FF = 12; + + /// Carriage return + static const CR = 13; + + /// Shift Out + static const SO = 14; + + /// Shift In + static const SI = 15; + + /// Data link escape + static const DLE = 16; + + /// Device control 1 + static const DC1 = 17; + + /// Device control 2 + static const DC2 = 18; + + /// Device control 3 + static const DC3 = 19; + + /// Device control 4 + static const DC4 = 20; + + /// NAK Negative-acknowledge + static const NAK = 21; + + /// Synchronous idle + static const SYN = 22; + + /// End of trans. block + static const ETB = 23; + + /// Cancel + static const CAN = 24; + + /// End of medium + static const EM = 25; + + /// Substitute + static const SUB = 26; + + /// Escape + static const ESC = 27; + + /// File separator + static const FS = 28; + + /// Group separator + static const GS = 29; + + /// Record separator + static const RS = 30; + + /// Unit separator + static const US = 31; + + /// Delete + static const DEL = 127; + + /* + * Printable ASCII characters + */ + + /// Space " " + static const space = 32; + + /// Exclamation mark "!" + static const exclamationMark = 33; + + /// Double quotes '"' + static const doubleQuotes = 34; + + /// Number sign '#' + static const numberSign = 35; + + /// Dollar sign '$' + static const dollarSign = 36; + + /// Percent sign '%' + static const percentSign = 37; + + /// Ampersand '&' + static const ampersand = 38; + + /// Single quote "'" + static const singleQuote = 39; + + /// round brackets or parentheses, opening round bracket '(' + static const openParentheses = 40; + + /// parentheses or round brackets, closing parentheses ')' + static const closeParentheses = 41; + + /// Asterisk '*' + static const asterisk = 42; + + /// Plus sign '+' + static const plus = 43; + + /// Comma "," + static const comma = 44; + + /// Hyphen , minus sign '-' + static const minus = 45; + + /// Dot, full stop '.' + static const dot = 46; + + /// Slash , forward slash , fraction bar , division slash '/' + static const slash = 47; + + /// number zero + static const num0 = 48; + + /// number one + static const num1 = 49; + + /// number two + static const num2 = 50; + + /// number three + static const num3 = 51; + + /// number four + static const num4 = 52; + + /// number five + static const num5 = 53; + + /// number six + static const num6 = 54; + + /// number seven + static const num7 = 55; + + /// number eight + static const num8 = 56; + + /// number nine + static const num9 = 57; + + /// Colon ':' + static const colon = 58; + + /// Semicolon ';' + static const semicolon = 59; + + /// Less-than sign '<' + static const lessThan = 60; + + /// Equals sign '=' + static const equal = 61; + + /// Greater-than sign ; Inequality sign '>' + static const greaterThan = 62; + + /// Question mark '?' + static const questionMark = 63; + + /// At sign '@' + static const atSign = 64; + + /// Capital letter A + static const A = 65; + + /// Capital letter B + static const B = 66; + + /// Capital letter C + static const C = 67; + + /// Capital letter D + static const D = 68; + + /// Capital letter E + static const E = 69; + + /// Capital letter F + static const F = 70; + + /// Capital letter G + static const G = 71; + + /// Capital letter H + static const H = 72; + + /// Capital letter I + static const I = 73; + + /// Capital letter J + static const J = 74; + + /// Capital letter K + static const K = 75; + + /// Capital letter L + static const L = 76; + + /// Capital letter M + static const M = 77; + + /// Capital letter N + static const N = 78; + + /// Capital letter O + static const O = 79; + + /// Capital letter P + static const P = 80; + + /// Capital letter Q + static const Q = 81; + + /// Capital letter R + static const R = 82; + + /// Capital letter S + static const S = 83; + + /// Capital letter T + static const T = 84; + + /// Capital letter U + static const U = 85; + + /// Capital letter V + static const V = 86; + + /// Capital letter W + static const W = 87; + + /// Capital letter X + static const X = 88; + + /// Capital letter Y + static const Y = 89; + + /// Capital letter Z + static const Z = 90; + + /// square brackets or box brackets, opening bracket '[' + static const openBracket = 91; + + /// Backslash , reverse slash '\\' + static const backslash = 92; + + /// box brackets or square brackets, closing bracket ']' + static const closeBracket = 93; + + /// Circumflex accent or Caret '^' + static const caret = 94; + + /// underscore , understrike , underbar or low line '_' + static const underscore = 95; + + /// Grave accent '`' + static const graveAccent = 96; + + /// Lowercase letter a , minuscule a + static const a = 97; + + /// Lowercase letter b , minuscule b + static const b = 98; + + /// Lowercase letter c , minuscule c + static const c = 99; + + /// Lowercase letter d , minuscule d + static const d = 100; + + /// Lowercase letter e , minuscule e + static const e = 101; + + /// Lowercase letter f , minuscule f + static const f = 102; + + /// Lowercase letter g , minuscule g + static const g = 103; + + /// Lowercase letter h , minuscule h + static const h = 104; + + /// Lowercase letter i , minuscule i + static const i = 105; + + /// Lowercase letter j , minuscule j + static const j = 106; + + /// Lowercase letter k , minuscule k + static const k = 107; + + /// Lowercase letter l , minuscule l + static const l = 108; + + /// Lowercase letter m , minuscule m + static const m = 109; + + /// Lowercase letter n , minuscule n + static const n = 110; + + /// Lowercase letter o , minuscule o + static const o = 111; + + /// Lowercase letter p , minuscule p + static const p = 112; + + /// Lowercase letter q , minuscule q + static const q = 113; + + /// Lowercase letter r , minuscule r + static const r = 114; + + /// Lowercase letter s , minuscule s + static const s = 115; + + /// Lowercase letter t , minuscule t + static const t = 116; + + /// Lowercase letter u , minuscule u + static const u = 117; + + /// Lowercase letter v , minuscule v + static const v = 118; + + /// Lowercase letter w , minuscule w + static const w = 119; + + /// Lowercase letter x , minuscule x + static const x = 120; + + /// Lowercase letter y , minuscule y + static const y = 121; + + /// Lowercase letter z , minuscule z + static const z = 122; + + /// braces or curly brackets, opening braces '{' + static const openBrace = 123; + + /// vertical-bar, vbar, vertical line or vertical slash '|' + static const verticalBar = 124; + + /// curly brackets or braces, closing curly brackets '}' + static const closeBrace = 125; + + /// Tilde ; swung dash '~' + static const tilde = 126; +} diff --git a/lib/utils/byte_consumer.dart b/lib/utils/byte_consumer.dart new file mode 100644 index 00000000..4a2ad382 --- /dev/null +++ b/lib/utils/byte_consumer.dart @@ -0,0 +1,107 @@ +import 'dart:collection'; + +class ByteConsumer { + final _queue = ListQueue>(); + + final _consumed = ListQueue>(); + + var _currentOffset = 0; + + var _length = 0; + + var _totalConsumed = 0; + + void add(String data) { + if (data.isEmpty) return; + final runes = data.runes.toList(growable: false); + _queue.addLast(runes); + _length += runes.length; + } + + int peek() { + final data = _queue.first; + if (_currentOffset < data.length) { + return data[_currentOffset]; + } else { + final result = consume(); + rollback(); + return result; + } + } + + int consume() { + final data = _queue.first; + + if (_currentOffset >= data.length) { + _consumed.add(_queue.removeFirst()); + _currentOffset -= data.length; + return consume(); + } + + _length--; + _totalConsumed++; + return data[_currentOffset++]; + } + + /// Rolls back the last [n] call. + void rollback([int n = 1]) { + _currentOffset -= n; + _totalConsumed -= n; + _length += n; + while (_currentOffset < 0) { + final rollback = _consumed.removeLast(); + _queue.addFirst(rollback); + _currentOffset += rollback.length; + } + } + + /// Rolls back to the state when this consumer had [length] bytes. + void rollbackTo(int length) { + rollback(length - _length); + } + + int get length => _length; + + int get totalConsumed => _totalConsumed; + + bool get isEmpty => _length == 0; + + bool get isNotEmpty => _length != 0; + + /// Unreferences data blocks that have been consumed. After calling this + /// method, the consumer will not be able to roll back to consumed blocks. + void unrefConsumedBlocks() { + _consumed.clear(); + } + + /// Resets the consumer to its initial state. + void reset() { + _queue.clear(); + _consumed.clear(); + _currentOffset = 0; + _totalConsumed = 0; + _length = 0; + } +} + +// void main() { +// final consumer = ByteConsumer(); +// consumer.add(Uint8List.fromList([1, 2, 3])); +// consumer.add(Uint8List.fromList([4, 5, 6])); + +// while (consumer.isNotEmpty) { +// print(consumer.consume()); +// } + +// consumer.rollback(5); + +// while (consumer.isNotEmpty) { +// print(consumer.consume()); +// } + +// consumer.rollbackTo(3); + +// while (consumer.isNotEmpty) { +// print(consumer.consume()); +// } +// } diff --git a/lib/utils/char_code.dart b/lib/utils/char_code.dart new file mode 100644 index 00000000..9eb79e0a --- /dev/null +++ b/lib/utils/char_code.dart @@ -0,0 +1,5 @@ +extension StringCharCode on String { + int get charCode { + return codeUnitAt(0); + } +} diff --git a/lib/util/circular_list.dart b/lib/utils/circular_list.dart similarity index 89% rename from lib/util/circular_list.dart rename to lib/utils/circular_list.dart index bc1516aa..0cbb2bc6 100644 --- a/lib/util/circular_list.dart +++ b/lib/utils/circular_list.dart @@ -2,11 +2,14 @@ class CircularList { CircularList(int maxLength) : _array = List.filled(maxLength, null); late List _array; + var _length = 0; + var _startIndex = 0; // Gets the cyclic index for the specified regular index. The cyclic index can then be used on the // backing array to get the element associated with the regular index. + @pragma('vm:prefer-inline') int _getCyclicIndex(int index) { return (_startIndex + index) % _array.length; } @@ -16,9 +19,9 @@ class CircularList { } set maxLength(int value) { - if (value <= 0) - throw ArgumentError.value( - value, 'value', 'maxLength can\'t be negative!'); + if (value <= 0) { + throw ArgumentError.value(value, 'value', "maxLength can't be negative!"); + } if (value == _array.length) return; @@ -54,7 +57,7 @@ class CircularList { } T operator [](int index) { - if (index >= length) { + if (index >= length || index < 0) { throw RangeError.range(index, 0, length - 1); } @@ -62,7 +65,7 @@ class CircularList { } operator []=(int index, T value) { - if (index >= length) { + if (index >= length || index < 0) { throw RangeError.range(index, 0, length - 1); } @@ -75,9 +78,9 @@ class CircularList { } void pushAll(Iterable items) { - items.forEach((element) { + for (var element in items) { push(element); - }); + } } void push(T value) { @@ -112,18 +115,27 @@ class CircularList { /// Inserts [item] at [index]. void insert(int index, T item) { + if (index < 0 || index > _length) { + throw RangeError.range(index, 0, _length); + } + + if (index == _length) { + return push(item); + } + if (index == 0 && _length >= _array.length) { // when something is inserted at index 0 and the list is full then // the new value immediately gets removed => nothing changes return; } + for (var i = _length - 1; i >= index; i--) { _array[_getCyclicIndex(i + 1)] = _array[_getCyclicIndex(i)]; } _array[_getCyclicIndex(index)] = item; - if (_length + 1 > _array.length) { + if (_length >= _array.length) { _startIndex += 1; } else { _length++; @@ -154,10 +166,12 @@ class CircularList { void shiftElements(int start, int count, int offset) { if (count < 0) return; - if (start < 0 || start >= _length) + if (start < 0 || start >= _length) { throw Exception('Start argument is out of range'); - if (start + offset < 0) + } + if (start + offset < 0) { throw Exception('Can not shift elements in list beyond index 0'); + } if (offset > 0) { for (var i = count - 1; i >= 0; i--) { this[start + i + offset] = this[start + i]; diff --git a/lib/utils/debugger.dart b/lib/utils/debugger.dart new file mode 100644 index 00000000..f900b8a9 --- /dev/null +++ b/lib/utils/debugger.dart @@ -0,0 +1,585 @@ +import 'package:xterm/core/escape/handler.dart'; +import 'package:xterm/core/escape/parser.dart'; +import 'package:xterm/core/mouse.dart'; +import 'package:xterm/utils/observable.dart'; + +class TerminalCommand { + TerminalCommand( + this.start, + this.end, + this.chars, + this.escapedChars, + this.explanation, + this.error, + ); + + final int start; + + final int end; + + final String chars; + + final String escapedChars; + + final List explanation; + + final bool error; +} + +class TerminalDebugger with Observable { + late final _parser = EscapeParser(_handler); + + late final _handler = _TerminalDebuggerHandler(recordCommand); + + final recorded = []; + + final commands = []; + + void write(String chunk) { + recorded.addAll(chunk.runes); + _parser.write(chunk); + notifyListeners(); + } + + void recordCommand(String explanation, {bool error = false}) { + final start = _parser.tokenBegin; + final end = _parser.tokenEnd; + + if (commands.isNotEmpty && commands.last.end == end) { + commands.last.explanation.add(explanation); + } else { + final charCodes = recorded.sublist(start, end); + final chars = String.fromCharCodes(charCodes); + final escapedChars = _escape(chars); + commands.add( + TerminalCommand(start, end, chars, escapedChars, [explanation], error), + ); + } + } + + String getRecord(TerminalCommand command) { + final charCodes = recorded.sublist(0, command.end); + return String.fromCharCodes(charCodes); + } + + static String _escape(String chars) { + final escaped = StringBuffer(); + for (final char in chars.runes) { + if (char == 0x1b) { + escaped.write('ESC'); + } else if (char < 32) { + escaped.write('^0x${char.toRadixString(16)}'); + } else if (char == 127) { + escaped.write('^?'); + } else { + escaped.writeCharCode(char); + } + } + return escaped.toString(); + } +} + +class _TerminalDebuggerHandler implements EscapeHandler { + _TerminalDebuggerHandler(this.onCommand); + + final void Function(String explanation, {bool error}) onCommand; + + @override + void writeChar(int char) { + onCommand('writeChar(${String.fromCharCode(char)})'); + } + + /* SBC */ + + @override + void bell() { + onCommand('bell'); + } + + @override + void backspaceReturn() { + onCommand('backspaceReturn'); + } + + @override + void tab() { + onCommand('tab'); + } + + @override + void lineFeed() { + onCommand('lineFeed'); + } + + @override + void carriageReturn() { + onCommand('carriageReturn'); + } + + @override + void shiftOut() { + onCommand('shiftOut'); + } + + @override + void shiftIn() { + onCommand('shiftIn'); + } + + @override + void unknownSBC(int char) { + onCommand('unkownSBC(${String.fromCharCode(char)})', error: true); + } + + /* ANSI sequence */ + + @override + void saveCursor() { + onCommand('saveCursor'); + } + + @override + void restoreCursor() { + onCommand('restoreCursor'); + } + + @override + void index() { + onCommand('index'); + } + + @override + void nextLine() { + onCommand('nextLine'); + } + + @override + void setTapStop() { + onCommand('setTapStop'); + } + + @override + void reverseIndex() { + onCommand('reverseIndex'); + } + + @override + void designateCharset(int charset) { + onCommand('designateCharset($charset)'); + } + + @override + void unkownEscape(int char) { + onCommand('unkownEscape(${String.fromCharCode(char)})', error: true); + } + + /* CSI */ + + @override + void repeatPreviousCharacter(int count) { + onCommand('repeatPreviousCharacter($count)'); + } + + @override + void unknownCSI(int finalByte) { + onCommand('unkownCSI(${String.fromCharCode(finalByte)})', error: true); + } + + @override + void setCursor(int x, int y) { + onCommand('setCursor($x, $y)'); + } + + @override + void setCursorX(int x) { + onCommand('setCursorX($x)'); + } + + @override + void setCursorY(int y) { + onCommand('setCursorY($y)'); + } + + @override + void sendPrimaryDeviceAttributes() { + onCommand('sendPrimaryDeviceAttributes'); + } + + @override + void clearTabStopUnderCursor() { + onCommand('clearTabStopUnderCursor'); + } + + @override + void clearAllTabStops() { + onCommand('clearAllTabStops'); + } + + @override + void moveCursorX(int offset) { + onCommand('moveCursorX($offset)'); + } + + @override + void moveCursorY(int n) { + onCommand('moveCursorY($n)'); + } + + @override + void sendSecondaryDeviceAttributes() { + onCommand('sendSecondaryDeviceAttributes'); + } + + @override + void sendTertiaryDeviceAttributes() { + onCommand('sendTertiaryDeviceAttributes'); + } + + @override + void sendOperatingStatus() { + onCommand('sendOperatingStatus'); + } + + @override + void sendCursorPosition() { + onCommand('sendCursorPosition'); + } + + @override + void setMargins(int i, [int? bottom]) { + onCommand('setMargins($i, $bottom)'); + } + + @override + void cursorNextLine(int amount) { + onCommand('cursorNextLine($amount)'); + } + + @override + void cursorPrecedingLine(int amount) { + onCommand('cursorPrecedingLine($amount)'); + } + + @override + void eraseDisplayBelow() { + onCommand('eraseDisplayBelow'); + } + + @override + void eraseDisplayAbove() { + onCommand('eraseDisplayAbove'); + } + + @override + void eraseDisplay() { + onCommand('eraseDisplay'); + } + + @override + void eraseScrollbackOnly() { + onCommand('eraseScrollbackOnly'); + } + + @override + void eraseLineRight() { + onCommand('eraseLineRight'); + } + + @override + void eraseLineLeft() { + onCommand('eraseLineLeft'); + } + + @override + void eraseLine() { + onCommand('eraseLine'); + } + + @override + void insertLines(int amount) { + onCommand('insertLines($amount)'); + } + + @override + void deleteLines(int amount) { + onCommand('deleteLines($amount)'); + } + + @override + void deleteChars(int amount) { + onCommand('deleteChars($amount)'); + } + + @override + void scrollUp(int amount) { + onCommand('scrollUp($amount)'); + } + + @override + void scrollDown(int amount) { + onCommand('scrollDown($amount)'); + } + + @override + void eraseChars(int amount) { + onCommand('eraseChars($amount)'); + } + + @override + void insertBlankChars(int amount) { + onCommand('insertBlankChars($amount)'); + } + + /* Modes */ + + @override + void setInsertMode(bool enabled) { + onCommand('setInsertMode($enabled)'); + } + + @override + void setLineFeedMode(bool enabled) { + onCommand('setLineFeedMode($enabled)'); + } + + @override + void setUnknownMode(int mode, bool enabled) { + onCommand('setUnknownMode($mode, $enabled)', error: true); + } + + /* DEC Private modes */ + + @override + void setCursorKeysMode(bool enabled) { + onCommand('setCursorKeysMode($enabled)'); + } + + @override + void setReverseDisplayMode(bool enabled) { + onCommand('setReverseDisplayMode($enabled)'); + } + + @override + void setOriginMode(bool enabled) { + onCommand('setOriginMode($enabled)'); + } + + @override + void setColumnMode(bool enabled) { + onCommand('setColumnMode($enabled)'); + } + + @override + void setAutoWrapMode(bool enabled) { + onCommand('setAutoWrapMode($enabled)'); + } + + @override + void setMouseMode(MouseMode mode) { + onCommand('setMouseMode($mode)'); + } + + @override + void setCursorBlinkMode(bool enabled) { + onCommand('setCursorBlinkMode($enabled)'); + } + + @override + void setCursorVisibleMode(bool enabled) { + onCommand('setCursorVisibleMode($enabled)'); + } + + @override + void useAltBuffer() { + onCommand('useAltBuffer'); + } + + @override + void useMainBuffer() { + onCommand('useMainBuffer'); + } + + @override + void clearAltBuffer() { + onCommand('clearAltBuffer'); + } + + @override + void setAppKeypadMode(bool enabled) { + onCommand('setAppKeypadMode($enabled)'); + } + + @override + void setReportFocusMode(bool enabled) { + onCommand('setReportFocusMode($enabled)'); + } + + @override + void setMouseReportMode(MouseReportMode mode) { + onCommand('setMouseReportMode($mode)'); + } + + @override + void setAltBufferMouseScrollMode(bool enabled) { + onCommand('setAltBufferMouseScrollMode($enabled)'); + } + + @override + void setBracketedPasteMode(bool enabled) { + onCommand('setBracketedPasteMode($enabled)'); + } + + @override + void setUnknownDecMode(int mode, bool enabled) { + onCommand('setUnknownDecMode($mode, $enabled)', error: true); + } + + /* Select Graphic Rendition (SGR) */ + + @override + void resetCursorStyle() { + onCommand('resetCursorStyle'); + } + + @override + void setCursorBold() { + onCommand('setCursorBold'); + } + + @override + void setCursorFaint() { + onCommand('setCursorFaint'); + } + + @override + void setCursorItalic() { + onCommand('setCursorItalic'); + } + + @override + void setCursorUnderline() { + onCommand('setCursorUnderline'); + } + + @override + void setCursorBlink() { + onCommand('setCursorBlink'); + } + + @override + void setCursorInverse() { + onCommand('setCursorInverse'); + } + + @override + void setCursorInvisible() { + onCommand('setCursorInvisible'); + } + + @override + void setCursorStrikethrough() { + onCommand('setCursorStrikethrough'); + } + + @override + void unsetCursorBold() { + onCommand('unsetCursorBold'); + } + + @override + void unsetCursorFaint() { + onCommand('unsetCursorFaint'); + } + + @override + void unsetCursorItalic() { + onCommand('unsetCursorItalic'); + } + + @override + void unsetCursorUnderline() { + onCommand('unsetCursorUnderline'); + } + + @override + void unsetCursorBlink() { + onCommand('unsetCursorBlink'); + } + + @override + void unsetCursorInverse() { + onCommand('unsetCursorInverse'); + } + + @override + void unsetCursorInvisible() { + onCommand('unsetCursorInvisible'); + } + + @override + void unsetCursorStrikethrough() { + onCommand('unsetCursorStrikethrough'); + } + + @override + void setForegroundColor16(int color) { + onCommand('setForegroundColor16($color)'); + } + + @override + void setForegroundColor256(int index) { + onCommand('setForegroundColor256($index)'); + } + + @override + void setForegroundColorRgb(int r, int g, int b) { + onCommand('setForegroundColorRgb($r, $g, $b)'); + } + + @override + void resetForeground() { + onCommand('resetForeground'); + } + + @override + void setBackgroundColor16(int color) { + onCommand('setBackgroundColor16($color)'); + } + + @override + void setBackgroundColor256(int index) { + onCommand('setBackgroundColor256($index)'); + } + + @override + void setBackgroundColorRgb(int r, int g, int b) { + onCommand('setBackgroundColorRgb($r, $g, $b)'); + } + + @override + void resetBackground() { + onCommand('resetBackground'); + } + + @override + void unsupportedStyle(int param) { + onCommand('unsupportedStyle($param)', error: true); + } + + /* OSC */ + + @override + void setTitle(String name) { + onCommand('setTitle($name)'); + } + + @override + void setIconName(String name) { + onCommand('setIconName($name)'); + } + + @override + void unknownOSC(String ps) { + onCommand('unknownOSC($ps)', error: true); + } +} diff --git a/lib/utils/debugger_view.dart b/lib/utils/debugger_view.dart new file mode 100644 index 00000000..e6804069 --- /dev/null +++ b/lib/utils/debugger_view.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:xterm/utils/debugger.dart'; + +class TerminalDebuggerView extends StatefulWidget { + const TerminalDebuggerView( + this.debugger, { + Key? key, + this.scrollController, + this.onSeek, + }) : super(key: key); + + final TerminalDebugger debugger; + + final ScrollController? scrollController; + + final void Function(int?)? onSeek; + + @override + State createState() => _TerminalDebuggerViewState(); +} + +class _TerminalDebuggerViewState extends State { + int? selectedCommand; + + @override + void initState() { + widget.debugger.addListener(_onDebuggerChanged); + super.initState(); + } + + @override + void didUpdateWidget(covariant TerminalDebuggerView oldWidget) { + if (oldWidget.debugger != widget.debugger) { + oldWidget.debugger.removeListener(_onDebuggerChanged); + widget.debugger.addListener(_onDebuggerChanged); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + widget.debugger.removeListener(_onDebuggerChanged); + super.dispose(); + } + + void _onDebuggerChanged() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final commands = widget.debugger.commands; + return ListView.builder( + itemExtent: 20, + controller: widget.scrollController, + itemCount: commands.length, + itemBuilder: (context, index) { + final command = commands[index]; + return _CommandItem( + index, + command, + selected: selectedCommand == index, + onTap: () { + if (selectedCommand == index) { + selectedCommand = null; + } else { + setState(() => selectedCommand = index); + } + widget.onSeek?.call(selectedCommand); + }, + ); + }, + ); + } +} + +class _CommandItem extends StatelessWidget { + const _CommandItem( + this.index, + this.command, { + Key? key, + this.onTap, + this.selected = false, + }) : super(key: key); + + final int index; + + final TerminalCommand command; + + final bool selected; + + final void Function()? onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (event) { + if (event.down) { + onTap?.call(); + } + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: selected ? Colors.blue : Colors.transparent, + width: 2, + ), + borderRadius: BorderRadius.circular(5), + ), + child: Row( + children: [ + Container( + width: 50, + child: Text( + '${index + 1}', + style: TextStyle( + color: selected ? Colors.blue : Colors.black, + fontFamily: 'monospace', + fontFamilyFallback: [ + 'Menlo', + 'Monaco', + 'Consolas', + 'Liberation Mono', + 'Courier New', + 'Noto Sans Mono CJK SC', + 'Noto Sans Mono CJK TC', + 'Noto Sans Mono CJK KR', + 'Noto Sans Mono CJK JP', + 'Noto Sans Mono CJK HK', + 'Noto Color Emoji', + 'Noto Sans Symbols', + 'monospace', + 'sans-serif', + ], + ), + textAlign: TextAlign.right, + ), + ), + SizedBox(width: 20), + Container( + width: 100, + child: Text( + command.escapedChars, + style: TextStyle(color: command.error ? Colors.red : null), + ), + ), + Expanded( + child: Container( + child: Text( + command.explanation.join(','), + style: TextStyle(color: command.error ? Colors.red : null), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/util/hash_values.dart b/lib/utils/hash_values.dart similarity index 100% rename from lib/util/hash_values.dart rename to lib/utils/hash_values.dart diff --git a/lib/util/lookup_table.dart b/lib/utils/lookup_table.dart similarity index 90% rename from lib/util/lookup_table.dart rename to lib/utils/lookup_table.dart index 195accaa..377294bb 100644 --- a/lib/util/lookup_table.dart +++ b/lib/utils/lookup_table.dart @@ -10,7 +10,7 @@ class FastLookupTable { } } - this._maxIndex = maxIndex; + _maxIndex = maxIndex; _table = List.filled(maxIndex + 1, null); @@ -29,4 +29,6 @@ class FastLookupTable { return _table[index]; } + + int get maxIndex => _maxIndex; } diff --git a/lib/util/observable.dart b/lib/utils/observable.dart similarity index 51% rename from lib/util/observable.dart rename to lib/utils/observable.dart index 4dfffa72..ac2b78a5 100644 --- a/lib/util/observable.dart +++ b/lib/utils/observable.dart @@ -1,13 +1,11 @@ -typedef _VoidCallback = void Function(); - mixin Observable { - final listeners = <_VoidCallback>{}; + final listeners = {}; - void addListener(_VoidCallback listener) { + void addListener(void Function() listener) { listeners.add(listener); } - void removeListener(_VoidCallback listener) { + void removeListener(void Function() listener) { listeners.remove(listener); } diff --git a/lib/utils/platform.dart b/lib/utils/platform.dart new file mode 100644 index 00000000..3c955fd8 --- /dev/null +++ b/lib/utils/platform.dart @@ -0,0 +1,17 @@ +enum TerminalTargetPlatform { + unknown, + + android, + + ios, + + fuchsia, + + linux, + + macos, + + windows, + + web, +} diff --git a/lib/util/unicode_v11.dart b/lib/utils/unicode_v11.dart similarity index 98% rename from lib/util/unicode_v11.dart rename to lib/utils/unicode_v11.dart index dc9e06bf..fd95ba0b 100644 --- a/lib/util/unicode_v11.dart +++ b/lib/utils/unicode_v11.dart @@ -1,3 +1,5 @@ +// ignore_for_file: constant_identifier_names + import 'dart:typed_data'; const BMP_COMBINING = [ @@ -214,7 +216,7 @@ const BMP_COMBINING = [ [0xFE00, 0xFE0F], [0xFE20, 0xFE2F], [0xFEFF, 0xFEFF], - [0xFFF9, 0xFFFB] + [0xFFF9, 0xFFFB], ]; const HIGH_COMBINING = [ @@ -334,7 +336,7 @@ const HIGH_COMBINING = [ [0x1E944, 0x1E94A], [0xE0001, 0xE0001], [0xE0020, 0xE007F], - [0xE0100, 0xE01EF] + [0xE0100, 0xE01EF], ]; const BMP_WIDE = [ @@ -398,7 +400,7 @@ const BMP_WIDE = [ [0xFE54, 0xFE66], [0xFE68, 0xFE6B], [0xFF01, 0xFF60], - [0xFFE0, 0xFFE6] + [0xFFE0, 0xFFE6], ]; const HIGH_WIDE = [ @@ -454,7 +456,7 @@ const HIGH_WIDE = [ [0x1FA80, 0x1FA82], [0x1FA90, 0x1FA95], [0x20000, 0x2FFFD], - [0x30000, 0x3FFFD] + [0x30000, 0x3FFFD], ]; final table = buildTable(); @@ -477,7 +479,7 @@ Uint8List buildTable() { bool bisearch(int ucs, List> data) { var min = 0; var max = data.length - 1; - var mid; + int mid; if (ucs < data[0][0] || ucs > data[max][1]) { return false; } diff --git a/lib/xterm.dart b/lib/xterm.dart index 5ba203cb..827cc150 100644 --- a/lib/xterm.dart +++ b/lib/xterm.dart @@ -1,7 +1,2 @@ -library xterm; - -export 'terminal/terminal.dart'; -export 'terminal/terminal_backend.dart'; -export 'terminal/terminal_ui_interaction.dart'; -export 'terminal/platform.dart'; -export 'theme/terminal_style.dart'; +export 'core.dart'; +export 'ui.dart'; diff --git a/pubspec.lock b/pubspec.lock index a62fd4be..3b9ff499 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,28 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "24.0.0" + version: "46.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + 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: @@ -28,7 +42,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -42,42 +56,42 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.3.0" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.1.0" + version: "7.2.3" built_collection: dependency: transitive description: @@ -98,7 +112,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -113,20 +127,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.3" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -140,7 +147,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: "direct main" description: @@ -148,6 +155,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" crypto: dependency: transitive description: @@ -155,27 +169,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" + dart_code_metrics: + dependency: "direct dev" + description: + name: dart_code_metrics + url: "https://pub.dartlang.org" + source: hosted + version: "4.17.1" dart_style: dependency: transitive description: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.2.3" equatable: dependency: "direct main" description: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" file: dependency: transitive description: @@ -221,6 +249,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.0" http_multi_server: dependency: transitive description: @@ -255,7 +290,14 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.6.0" + lints: + dependency: "direct dev" + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" logging: dependency: transitive description: @@ -269,14 +311,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" meta: dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -290,7 +339,14 @@ packages: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "5.0.15" + version: "5.3.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" package_config: dependency: transitive description: @@ -304,7 +360,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.2" pedantic: dependency: transitive description: @@ -312,6 +368,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" platform_info: dependency: "direct main" description: @@ -354,6 +417,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" shelf_web_socket: dependency: transitive description: @@ -372,14 +449,28 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.2" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -407,21 +498,35 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.21.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.12" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.16" timing: dependency: transitive description: @@ -442,7 +547,14 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "8.3.0" watcher: dependency: transitive description: @@ -457,6 +569,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" yaml: dependency: transitive description: @@ -465,5 +591,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.0.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 65498826..4c4c625d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,24 +1,27 @@ name: xterm description: xterm.dart is a fast and fully-featured terminal emulator for Flutter applications, with support for mobile and desktop platforms. -version: 2.6.0 +version: 3.0.6-alpha homepage: https://github.com/TerminalStudio/xterm.dart environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" dependencies: convert: ^3.0.0 meta: ^1.3.0 quiver: ^3.0.0 platform_info: ^3.0.0 - equatable: ^2.0.2 + equatable: ^2.0.3 flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter + test: ^1.6.5 + lints: ^2.0.0 + dart_code_metrics: ^4.16.0 mockito: ^5.0.15 build_runner: ^2.1.1 diff --git a/script/benchmark.dart b/script/benchmark.dart index 3a5cc439..90815c09 100644 --- a/script/benchmark.dart +++ b/script/benchmark.dart @@ -31,10 +31,12 @@ class BenchmarkWrite extends Benchmark { static const cycle = 1 << 20; static const data = 'hello world'; + @override String explain() { return "write '$data' to Terminal for $cycle times"; } + @override void benchmark() { final terminal = Terminal(maxLines: 40000); for (var i = 0; i < cycle; i++) { @@ -47,10 +49,12 @@ class BenchmarkWrite2 extends Benchmark { static const cycle = 100000; static const data = '100000'; + @override String explain() { return "write '$data' to Terminal for $cycle times"; } + @override void benchmark() { final terminal = Terminal(maxLines: 40000); for (var i = 0; i < cycle; i++) { @@ -67,10 +71,12 @@ class BenchmarkWriteCMatrix extends Benchmark { static const cycle = 12; late final String data; + @override String explain() { return 'write ${data.length / 1024} kb CMatrix -r output to Terminal for $cycle time(s)'; } + @override void benchmark() { final terminal = Terminal(maxLines: 40000); for (var i = 0; i < cycle; i++) { @@ -87,10 +93,12 @@ class BenchmarkWriteLines extends Benchmark { static const cycle = 10; late final String data; + @override String explain() { return 'write ${data.length / 1024} kb `find .` output to Terminal for $cycle time(s)'; } + @override void benchmark() { final terminal = Terminal(maxLines: 40000); for (var i = 0; i < cycle; i++) { @@ -103,10 +111,12 @@ class BenchmarkWriteBuffer extends Benchmark { static const cycle = 1 << 20; static const data = 'hello world'; + @override String explain() { return "write '$data' to StringBuffer for $cycle times"; } + @override void benchmark() { final buffer = StringBuffer(); for (var i = 0; i < cycle; i++) { diff --git a/test/buffer/buffer_line_test.dart b/test/buffer/buffer_line_test.dart index 650f39a9..43d2cae0 100644 --- a/test/buffer/buffer_line_test.dart +++ b/test/buffer/buffer_line_test.dart @@ -1,251 +1,251 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:xterm/buffer/line/line.dart'; -import 'package:xterm/terminal/cursor.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:xterm/buffer/line/line.dart'; +// import 'package:xterm/terminal/cursor.dart'; void main() { - group("BufferLine Tests", () { - test("creation test", () { - final line = BufferLine(); - expect(line, isNotNull); - }); - - test("set isWrapped", () { - final line = BufferLine(isWrapped: false); - expect(line.isWrapped, isFalse); - - line.isWrapped = true; - expect(line.isWrapped, isTrue); - - line.isWrapped = false; - expect(line.isWrapped, isFalse); - }); - - test("ensure() works", () { - final line = BufferLine(length: 10); - expect(() => line.cellSetContent(1000, 65), throwsRangeError); - - line.ensure(1000); - line.cellSetContent(1000, 65); - }); - - test("insert() works", () { - final line = BufferLine(length: 10); - line.cellSetContent(0, 65); - line.cellSetContent(1, 66); - line.cellSetContent(2, 67); - - line.insert(1); - - final result = [ - line.cellGetContent(0), - line.cellGetContent(1), - line.cellGetContent(2), - line.cellGetContent(3), - ]; - - expect(result, equals([65, 0, 66, 67])); - }); - - test("insertN() works", () { - final line = BufferLine(length: 10); - line.cellSetContent(0, 65); - line.cellSetContent(1, 66); - line.cellSetContent(2, 67); - - line.insertN(1, 2); - - final result = [ - line.cellGetContent(0), - line.cellGetContent(1), - line.cellGetContent(2), - line.cellGetContent(3), - line.cellGetContent(4), - ]; - - expect(result, equals([65, 0, 0, 66, 67])); - }); - - test("removeN() works", () { - final line = BufferLine(length: 10); - line.cellSetContent(0, 65); - line.cellSetContent(1, 66); - line.cellSetContent(2, 67); - line.cellSetContent(3, 68); - line.cellSetContent(4, 69); - - line.removeN(1, 2); - - final result = [ - line.cellGetContent(0), - line.cellGetContent(1), - line.cellGetContent(2), - line.cellGetContent(3), - line.cellGetContent(4), - ]; - - expect(result, equals([65, 68, 69, 0, 0])); - }); - - test("clear() works", () { - final line = BufferLine(length: 10); - line.cellSetContent(1, 65); - line.cellSetContent(2, 66); - line.cellSetContent(3, 67); - line.cellSetContent(4, 68); - line.cellSetContent(5, 69); - - line.clear(); - - final result = [ - line.cellGetContent(1), - line.cellGetContent(2), - line.cellGetContent(3), - line.cellGetContent(4), - line.cellGetContent(5), - ]; - - expect(result, equals([0, 0, 0, 0, 0])); - }); - - test("cellInitialize() works", () { - final line = BufferLine(length: 10); - line.cellInitialize( - 0, - content: 0x01, - width: 0x02, - cursor: Cursor(fg: 0x03, bg: 0x04, flags: 0x05), - ); - - final result = [ - line.cellGetContent(0), - line.cellGetWidth(0), - line.cellGetFgColor(0), - line.cellGetBgColor(0), - line.cellGetFlags(0), - ]; - - expect(result, equals([0x01, 0x02, 0x03, 0x04, 0x05])); - }); - - test("cellHasContent() works", () { - final line = BufferLine(length: 10); - - line.cellSetContent(0, 0x01); - expect(line.cellHasContent(0), isTrue); - - line.cellSetContent(0, 0x00); - expect(line.cellHasContent(0), isFalse); - }); - - test("cellGetContent() and cellSetContent() works", () { - final line = BufferLine(length: 10); - final content = 0x01; - line.cellSetContent(0, content); - expect(line.cellGetContent(0), equals(content)); - }); - - test("cellGetFgColor() and cellSetFgColor() works", () { - final line = BufferLine(length: 10); - final content = 0x01; - line.cellSetFgColor(0, content); - expect(line.cellGetFgColor(0), equals(content)); - }); - - test("cellGetBgColor() and cellSetBgColor() works", () { - final line = BufferLine(length: 10); - final content = 0x01; - line.cellSetBgColor(0, content); - expect(line.cellGetBgColor(0), equals(content)); - }); - - test("cellHasFlag() and cellSetFlag() works", () { - final line = BufferLine(length: 10); - final flag = 0x03; - line.cellSetFlag(0, flag); - expect(line.cellHasFlag(0, flag), isTrue); - }); - - test("cellGetFlags() and cellSetFlags() works", () { - final line = BufferLine(length: 10); - final content = 0x01; - line.cellSetFlags(0, content); - expect(line.cellGetFlags(0), equals(content)); - }); - - test("cellGetWidth() and cellSetWidth() works", () { - final line = BufferLine(length: 10); - final content = 0x01; - line.cellSetWidth(0, content); - expect(line.cellGetWidth(0), equals(content)); - }); - - test("getTrimmedLength() works", () { - final line = BufferLine(length: 10); - expect(line.getTrimmedLength(), equals(0)); - - line.cellSetContent(5, 0x01); - expect(line.getTrimmedLength(), equals(5)); - - line.clear(); - expect(line.getTrimmedLength(), equals(0)); - }); - - test("copyCellsFrom() works", () { - final line1 = BufferLine(length: 10); - final line2 = BufferLine(length: 10); - - line1.cellSetContent(0, 123); - line1.cellSetContent(1, 124); - line1.cellSetContent(2, 125); - - line2.copyCellsFrom(line1, 1, 3, 2); - - expect(line2.cellGetContent(2), equals(0)); - expect(line2.cellGetContent(3), equals(124)); - expect(line2.cellGetContent(4), equals(125)); - expect(line2.cellGetContent(5), equals(0)); - }); - - test("removeRange() works", () { - final line = BufferLine(length: 10); - line.cellSetContent(0, 65); - line.cellSetContent(1, 66); - line.cellSetContent(2, 67); - line.cellSetContent(3, 68); - line.cellSetContent(4, 69); - - line.removeRange(1, 3); - - final result = [ - line.cellGetContent(0), - line.cellGetContent(1), - line.cellGetContent(2), - line.cellGetContent(3), - line.cellGetContent(4), - ]; - - expect(result, equals([65, 68, 69, 0, 0])); - }); - - test("clearRange() works", () { - final line = BufferLine(length: 10); - line.cellSetContent(0, 65); - line.cellSetContent(1, 66); - line.cellSetContent(2, 67); - line.cellSetContent(3, 68); - line.cellSetContent(4, 69); - - line.clearRange(1, 3); - - final result = [ - line.cellGetContent(0), - line.cellGetContent(1), - line.cellGetContent(2), - line.cellGetContent(3), - line.cellGetContent(4), - ]; - - expect(result, equals([65, 0, 0, 68, 69])); - }); - }); + // group("BufferLine Tests", () { + // test("creation test", () { + // final line = BufferLine(); + // expect(line, isNotNull); + // }); + + // test("set isWrapped", () { + // final line = BufferLine(isWrapped: false); + // expect(line.isWrapped, isFalse); + + // line.isWrapped = true; + // expect(line.isWrapped, isTrue); + + // line.isWrapped = false; + // expect(line.isWrapped, isFalse); + // }); + + // test("ensure() works", () { + // final line = BufferLine(length: 10); + // expect(() => line.cellSetContent(1000, 65), throwsRangeError); + + // line.ensure(1000); + // line.cellSetContent(1000, 65); + // }); + + // test("insert() works", () { + // final line = BufferLine(length: 10); + // line.cellSetContent(0, 65); + // line.cellSetContent(1, 66); + // line.cellSetContent(2, 67); + + // line.insert(1); + + // final result = [ + // line.cellGetContent(0), + // line.cellGetContent(1), + // line.cellGetContent(2), + // line.cellGetContent(3), + // ]; + + // expect(result, equals([65, 0, 66, 67])); + // }); + + // test("insertN() works", () { + // final line = BufferLine(length: 10); + // line.cellSetContent(0, 65); + // line.cellSetContent(1, 66); + // line.cellSetContent(2, 67); + + // line.insertN(1, 2); + + // final result = [ + // line.cellGetContent(0), + // line.cellGetContent(1), + // line.cellGetContent(2), + // line.cellGetContent(3), + // line.cellGetContent(4), + // ]; + + // expect(result, equals([65, 0, 0, 66, 67])); + // }); + + // test("removeN() works", () { + // final line = BufferLine(length: 10); + // line.cellSetContent(0, 65); + // line.cellSetContent(1, 66); + // line.cellSetContent(2, 67); + // line.cellSetContent(3, 68); + // line.cellSetContent(4, 69); + + // line.removeN(1, 2); + + // final result = [ + // line.cellGetContent(0), + // line.cellGetContent(1), + // line.cellGetContent(2), + // line.cellGetContent(3), + // line.cellGetContent(4), + // ]; + + // expect(result, equals([65, 68, 69, 0, 0])); + // }); + + // test("clear() works", () { + // final line = BufferLine(length: 10); + // line.cellSetContent(1, 65); + // line.cellSetContent(2, 66); + // line.cellSetContent(3, 67); + // line.cellSetContent(4, 68); + // line.cellSetContent(5, 69); + + // line.clear(); + + // final result = [ + // line.cellGetContent(1), + // line.cellGetContent(2), + // line.cellGetContent(3), + // line.cellGetContent(4), + // line.cellGetContent(5), + // ]; + + // expect(result, equals([0, 0, 0, 0, 0])); + // }); + + // test("cellInitialize() works", () { + // final line = BufferLine(length: 10); + // line.cellInitialize( + // 0, + // content: 0x01, + // width: 0x02, + // cursor: Cursor(fg: 0x03, bg: 0x04, flags: 0x05), + // ); + + // final result = [ + // line.cellGetContent(0), + // line.cellGetWidth(0), + // line.cellGetFgColor(0), + // line.cellGetBgColor(0), + // line.cellGetFlags(0), + // ]; + + // expect(result, equals([0x01, 0x02, 0x03, 0x04, 0x05])); + // }); + + // test("cellHasContent() works", () { + // final line = BufferLine(length: 10); + + // line.cellSetContent(0, 0x01); + // expect(line.cellHasContent(0), isTrue); + + // line.cellSetContent(0, 0x00); + // expect(line.cellHasContent(0), isFalse); + // }); + + // test("cellGetContent() and cellSetContent() works", () { + // final line = BufferLine(length: 10); + // final content = 0x01; + // line.cellSetContent(0, content); + // expect(line.cellGetContent(0), equals(content)); + // }); + + // test("cellGetFgColor() and cellSetFgColor() works", () { + // final line = BufferLine(length: 10); + // final content = 0x01; + // line.cellSetFgColor(0, content); + // expect(line.cellGetFgColor(0), equals(content)); + // }); + + // test("cellGetBgColor() and cellSetBgColor() works", () { + // final line = BufferLine(length: 10); + // final content = 0x01; + // line.cellSetBgColor(0, content); + // expect(line.cellGetBgColor(0), equals(content)); + // }); + + // test("cellHasFlag() and cellSetFlag() works", () { + // final line = BufferLine(length: 10); + // final flag = 0x03; + // line.cellSetFlag(0, flag); + // expect(line.cellHasFlag(0, flag), isTrue); + // }); + + // test("cellGetFlags() and cellSetFlags() works", () { + // final line = BufferLine(length: 10); + // final content = 0x01; + // line.cellSetFlags(0, content); + // expect(line.cellGetFlags(0), equals(content)); + // }); + + // test("cellGetWidth() and cellSetWidth() works", () { + // final line = BufferLine(length: 10); + // final content = 0x01; + // line.cellSetWidth(0, content); + // expect(line.cellGetWidth(0), equals(content)); + // }); + + // test("getTrimmedLength() works", () { + // final line = BufferLine(length: 10); + // expect(line.getTrimmedLength(), equals(0)); + + // line.cellSetContent(5, 0x01); + // expect(line.getTrimmedLength(), equals(5)); + + // line.clear(); + // expect(line.getTrimmedLength(), equals(0)); + // }); + + // test("copyCellsFrom() works", () { + // final line1 = BufferLine(length: 10); + // final line2 = BufferLine(length: 10); + + // line1.cellSetContent(0, 123); + // line1.cellSetContent(1, 124); + // line1.cellSetContent(2, 125); + + // line2.copyCellsFrom(line1, 1, 3, 2); + + // expect(line2.cellGetContent(2), equals(0)); + // expect(line2.cellGetContent(3), equals(124)); + // expect(line2.cellGetContent(4), equals(125)); + // expect(line2.cellGetContent(5), equals(0)); + // }); + + // test("removeRange() works", () { + // final line = BufferLine(length: 10); + // line.cellSetContent(0, 65); + // line.cellSetContent(1, 66); + // line.cellSetContent(2, 67); + // line.cellSetContent(3, 68); + // line.cellSetContent(4, 69); + + // line.removeRange(1, 3); + + // final result = [ + // line.cellGetContent(0), + // line.cellGetContent(1), + // line.cellGetContent(2), + // line.cellGetContent(3), + // line.cellGetContent(4), + // ]; + + // expect(result, equals([65, 68, 69, 0, 0])); + // }); + + // test("clearRange() works", () { + // final line = BufferLine(length: 10); + // line.cellSetContent(0, 65); + // line.cellSetContent(1, 66); + // line.cellSetContent(2, 67); + // line.cellSetContent(3, 68); + // line.cellSetContent(4, 69); + + // line.clearRange(1, 3); + + // final result = [ + // line.cellGetContent(0), + // line.cellGetContent(1), + // line.cellGetContent(2), + // line.cellGetContent(3), + // line.cellGetContent(4), + // ]; + + // expect(result, equals([65, 0, 0, 68, 69])); + // }); + // }); } diff --git a/test/frontend/input_test.dart b/test/frontend/input_test.dart index 48da3429..bf439bb7 100644 --- a/test/frontend/input_test.dart +++ b/test/frontend/input_test.dart @@ -1,140 +1,137 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:xterm/frontend/input_behavior_desktop.dart'; -import 'package:xterm/xterm.dart'; - -import 'input_test.mocks.dart'; - -@GenerateMocks([ - TerminalUiInteraction, -]) +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/annotations.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:xterm/xterm.dart'; + +// @GenerateMocks([ +// TerminalUiInteraction, +// ]) void main() { - group('InputBehaviorDesktop', () { - test('can handle fast typing', () { - final mockTerminal = MockTerminalUiInteraction(); - final inputBehavior = InputBehaviorDesktop(); - - inputBehavior.onTextEdit(composing('l', -1, -1), mockTerminal); - verify(mockTerminal.raiseOnInput('l')); - verifyNever(mockTerminal.updateComposingString(any)); - - inputBehavior.onTextEdit(composing('ls', -1, -1), mockTerminal); - verify(mockTerminal.raiseOnInput('s')); - verifyNever(mockTerminal.updateComposingString(any)); - - inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); - verifyNever(mockTerminal.raiseOnInput(any)); - verifyNever(mockTerminal.updateComposingString(any)); - }); - - test('can handle English', () { - final mockTerminal = MockTerminalUiInteraction(); - final inputBehavior = InputBehaviorDesktop(); - - // typing 'hello' - - inputBehavior.onTextEdit(composing('h', 1, 1), mockTerminal); - verify(mockTerminal.raiseOnInput('h')); - verifyNever(mockTerminal.updateComposingString(any)); - - inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); - inputBehavior.onTextEdit(composing('e', 1, 1), mockTerminal); - verify(mockTerminal.raiseOnInput('e')); - verifyNever(mockTerminal.updateComposingString(any)); - - inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); - inputBehavior.onTextEdit(composing('l', 1, 1), mockTerminal); - verify(mockTerminal.raiseOnInput('l')); - verifyNever(mockTerminal.updateComposingString(any)); - - inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); - inputBehavior.onTextEdit(composing('l', 1, 1), mockTerminal); - verify(mockTerminal.raiseOnInput('l')); - verifyNever(mockTerminal.updateComposingString(any)); - - inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); - inputBehavior.onTextEdit(composing('o', 1, 1), mockTerminal); - verify(mockTerminal.raiseOnInput('o')); - verifyNever(mockTerminal.updateComposingString(any)); - }); - - test('can handle Chinese', () { - final mockTerminal = MockTerminalUiInteraction(); - final inputBehavior = InputBehaviorDesktop(); - - // typing 'δ½ ε₯½' - - inputBehavior.onTextEdit(composing('n', 0, 1), mockTerminal); - inputBehavior.onTextEdit(composing('ni', 0, 2), mockTerminal); - inputBehavior.onTextEdit(composing('ni h', 0, 4), mockTerminal); - inputBehavior.onTextEdit(composing('ni ha', 0, 5), mockTerminal); - inputBehavior.onTextEdit(composing('ni hao', 0, 6), mockTerminal); - inputBehavior.onTextEdit(composing('δ½ ε₯½', 0, 2), mockTerminal); - verify(mockTerminal.updateComposingString(any)).called(6); - verifyNever(mockTerminal.raiseOnInput(any)); - - inputBehavior.onTextEdit(composing('δ½ ε₯½', -1, -1), mockTerminal); - verify(mockTerminal.raiseOnInput('δ½ ε₯½')); - verify(mockTerminal.updateComposingString('')); - }); - - test('can handle Japanese', () { - final mockTerminal = MockTerminalUiInteraction(); - final inputBehavior = InputBehaviorDesktop(); - - // typing 'どうも' - - inputBehavior.onTextEdit(composing('d', 0, 1), mockTerminal); - inputBehavior.onTextEdit(composing('ど', 0, 1), mockTerminal); - inputBehavior.onTextEdit(composing('どう', 0, 2), mockTerminal); - inputBehavior.onTextEdit(composing('どうm', 0, 3), mockTerminal); - inputBehavior.onTextEdit(composing('どうも', 0, 3), mockTerminal); - verify(mockTerminal.updateComposingString(any)).called(5); - verifyNever(mockTerminal.raiseOnInput(any)); - - inputBehavior.onTextEdit(composing('どうも', -1, -1), mockTerminal); - verify(mockTerminal.raiseOnInput('どうも')); - verify(mockTerminal.updateComposingString('')); - }); - - test('can handle Korean', () { - final mockTerminal = MockTerminalUiInteraction(); - final inputBehavior = InputBehaviorDesktop(); - - // typing 'μ•ˆλ…•' - - inputBehavior.onTextEdit(composing('γ…‡', 0, 1), mockTerminal); - inputBehavior.onTextEdit(composing('μ•„', 0, 1), mockTerminal); - inputBehavior.onTextEdit(composing('μ•ˆ', 0, 1), mockTerminal); - inputBehavior.onTextEdit(composing('μ•ˆ', 0, 1), mockTerminal); - verify(mockTerminal.updateComposingString(any)).called(4); - verifyNever(mockTerminal.raiseOnInput(any)); - - inputBehavior.onTextEdit(composing('μ•ˆ', 1, 1), mockTerminal); - verify(mockTerminal.raiseOnInput('μ•ˆ')); - verify(mockTerminal.updateComposingString('')); - - inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); - inputBehavior.onTextEdit(composing('γ„΄', 0, 1), mockTerminal); - inputBehavior.onTextEdit(composing('λ…€', 0, 1), mockTerminal); - inputBehavior.onTextEdit(composing('λ…•', 0, 1), mockTerminal); - verify(mockTerminal.updateComposingString(any)).called(3); - verifyNever(mockTerminal.raiseOnInput(any)); - - inputBehavior.onTextEdit(composing('λ…•', 1, 1), mockTerminal); - verify(mockTerminal.raiseOnInput('λ…•')); - verify(mockTerminal.updateComposingString('')); - }); - }); -} - -TextEditingValue composing(String text, int start, int end) { - return TextEditingValue( - text: text, - selection: TextSelection.collapsed(offset: text.length), - composing: TextRange(start: start, end: end), - ); +// group('InputBehaviorDesktop', () { +// test('can handle fast typing', () { +// final mockTerminal = MockTerminalUiInteraction(); +// final inputBehavior = InputBehaviorDesktop(); + +// inputBehavior.onTextEdit(composing('l', -1, -1), mockTerminal); +// verify(mockTerminal.raiseOnInput('l')); +// verifyNever(mockTerminal.updateComposingString(any)); + +// inputBehavior.onTextEdit(composing('ls', -1, -1), mockTerminal); +// verify(mockTerminal.raiseOnInput('s')); +// verifyNever(mockTerminal.updateComposingString(any)); + +// inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); +// verifyNever(mockTerminal.raiseOnInput(any)); +// verifyNever(mockTerminal.updateComposingString(any)); +// }); + +// test('can handle English', () { +// final mockTerminal = MockTerminalUiInteraction(); +// final inputBehavior = InputBehaviorDesktop(); + +// // typing 'hello' + +// inputBehavior.onTextEdit(composing('h', 1, 1), mockTerminal); +// verify(mockTerminal.raiseOnInput('h')); +// verifyNever(mockTerminal.updateComposingString(any)); + +// inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); +// inputBehavior.onTextEdit(composing('e', 1, 1), mockTerminal); +// verify(mockTerminal.raiseOnInput('e')); +// verifyNever(mockTerminal.updateComposingString(any)); + +// inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); +// inputBehavior.onTextEdit(composing('l', 1, 1), mockTerminal); +// verify(mockTerminal.raiseOnInput('l')); +// verifyNever(mockTerminal.updateComposingString(any)); + +// inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); +// inputBehavior.onTextEdit(composing('l', 1, 1), mockTerminal); +// verify(mockTerminal.raiseOnInput('l')); +// verifyNever(mockTerminal.updateComposingString(any)); + +// inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); +// inputBehavior.onTextEdit(composing('o', 1, 1), mockTerminal); +// verify(mockTerminal.raiseOnInput('o')); +// verifyNever(mockTerminal.updateComposingString(any)); +// }); + +// test('can handle Chinese', () { +// final mockTerminal = MockTerminalUiInteraction(); +// final inputBehavior = InputBehaviorDesktop(); + +// // typing 'δ½ ε₯½' + +// inputBehavior.onTextEdit(composing('n', 0, 1), mockTerminal); +// inputBehavior.onTextEdit(composing('ni', 0, 2), mockTerminal); +// inputBehavior.onTextEdit(composing('ni h', 0, 4), mockTerminal); +// inputBehavior.onTextEdit(composing('ni ha', 0, 5), mockTerminal); +// inputBehavior.onTextEdit(composing('ni hao', 0, 6), mockTerminal); +// inputBehavior.onTextEdit(composing('δ½ ε₯½', 0, 2), mockTerminal); +// verify(mockTerminal.updateComposingString(any)).called(6); +// verifyNever(mockTerminal.raiseOnInput(any)); + +// inputBehavior.onTextEdit(composing('δ½ ε₯½', -1, -1), mockTerminal); +// verify(mockTerminal.raiseOnInput('δ½ ε₯½')); +// verify(mockTerminal.updateComposingString('')); +// }); + +// test('can handle Japanese', () { +// final mockTerminal = MockTerminalUiInteraction(); +// final inputBehavior = InputBehaviorDesktop(); + +// // typing 'どうも' + +// inputBehavior.onTextEdit(composing('d', 0, 1), mockTerminal); +// inputBehavior.onTextEdit(composing('ど', 0, 1), mockTerminal); +// inputBehavior.onTextEdit(composing('どう', 0, 2), mockTerminal); +// inputBehavior.onTextEdit(composing('どうm', 0, 3), mockTerminal); +// inputBehavior.onTextEdit(composing('どうも', 0, 3), mockTerminal); +// verify(mockTerminal.updateComposingString(any)).called(5); +// verifyNever(mockTerminal.raiseOnInput(any)); + +// inputBehavior.onTextEdit(composing('どうも', -1, -1), mockTerminal); +// verify(mockTerminal.raiseOnInput('どうも')); +// verify(mockTerminal.updateComposingString('')); +// }); + +// test('can handle Korean', () { +// final mockTerminal = MockTerminalUiInteraction(); +// final inputBehavior = InputBehaviorDesktop(); + +// // typing 'μ•ˆλ…•' + +// inputBehavior.onTextEdit(composing('γ…‡', 0, 1), mockTerminal); +// inputBehavior.onTextEdit(composing('μ•„', 0, 1), mockTerminal); +// inputBehavior.onTextEdit(composing('μ•ˆ', 0, 1), mockTerminal); +// inputBehavior.onTextEdit(composing('μ•ˆ', 0, 1), mockTerminal); +// verify(mockTerminal.updateComposingString(any)).called(4); +// verifyNever(mockTerminal.raiseOnInput(any)); + +// inputBehavior.onTextEdit(composing('μ•ˆ', 1, 1), mockTerminal); +// verify(mockTerminal.raiseOnInput('μ•ˆ')); +// verify(mockTerminal.updateComposingString('')); + +// inputBehavior.onTextEdit(TextEditingValue.empty, mockTerminal); +// inputBehavior.onTextEdit(composing('γ„΄', 0, 1), mockTerminal); +// inputBehavior.onTextEdit(composing('λ…€', 0, 1), mockTerminal); +// inputBehavior.onTextEdit(composing('λ…•', 0, 1), mockTerminal); +// verify(mockTerminal.updateComposingString(any)).called(3); +// verifyNever(mockTerminal.raiseOnInput(any)); + +// inputBehavior.onTextEdit(composing('λ…•', 1, 1), mockTerminal); +// verify(mockTerminal.raiseOnInput('λ…•')); +// verify(mockTerminal.updateComposingString('')); +// }); +// }); +// } + +// TextEditingValue composing(String text, int start, int end) { +// return TextEditingValue( +// text: text, +// selection: TextSelection.collapsed(offset: text.length), +// composing: TextRange(start: start, end: end), +// ); } diff --git a/test/frontend/input_test.mocks.dart b/test/frontend/input_test.mocks.dart deleted file mode 100644 index b36ccdf8..00000000 --- a/test/frontend/input_test.mocks.dart +++ /dev/null @@ -1,248 +0,0 @@ -// Mocks generated by Mockito 5.0.15 from annotations -// in xterm/test/frontend/input_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i6; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:xterm/buffer/line/line.dart' as _i7; -import 'package:xterm/input/keys.dart' as _i9; -import 'package:xterm/mouse/position.dart' as _i8; -import 'package:xterm/terminal/platform.dart' as _i3; -import 'package:xterm/terminal/terminal_search.dart' as _i4; -import 'package:xterm/terminal/terminal_ui_interaction.dart' as _i5; -import 'package:xterm/theme/terminal_theme.dart' as _i2; - -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -class _FakeTerminalTheme_0 extends _i1.Fake implements _i2.TerminalTheme {} - -class _FakePlatformBehavior_1 extends _i1.Fake implements _i3.PlatformBehavior { -} - -class _FakeTerminalSearchResult_2 extends _i1.Fake - implements _i4.TerminalSearchResult {} - -class _FakeTerminalSearchOptions_3 extends _i1.Fake - implements _i4.TerminalSearchOptions {} - -/// A class which mocks [TerminalUiInteraction]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTerminalUiInteraction extends _i1.Mock - implements _i5.TerminalUiInteraction { - MockTerminalUiInteraction() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.TerminalTheme get theme => (super.noSuchMethod(Invocation.getter(#theme), - returnValue: _FakeTerminalTheme_0()) as _i2.TerminalTheme); - @override - int get scrollOffsetFromBottom => - (super.noSuchMethod(Invocation.getter(#scrollOffsetFromBottom), - returnValue: 0) as int); - @override - int get scrollOffsetFromTop => (super - .noSuchMethod(Invocation.getter(#scrollOffsetFromTop), returnValue: 0) - as int); - @override - int get bufferHeight => - (super.noSuchMethod(Invocation.getter(#bufferHeight), returnValue: 0) - as int); - @override - int get terminalHeight => - (super.noSuchMethod(Invocation.getter(#terminalHeight), returnValue: 0) - as int); - @override - int get terminalWidth => - (super.noSuchMethod(Invocation.getter(#terminalWidth), returnValue: 0) - as int); - @override - int get invisibleHeight => - (super.noSuchMethod(Invocation.getter(#invisibleHeight), returnValue: 0) - as int); - @override - bool get showCursor => - (super.noSuchMethod(Invocation.getter(#showCursor), returnValue: false) - as bool); - @override - int get cursorY => - (super.noSuchMethod(Invocation.getter(#cursorY), returnValue: 0) as int); - @override - int get cursorX => - (super.noSuchMethod(Invocation.getter(#cursorX), returnValue: 0) as int); - @override - int get cursorColor => - (super.noSuchMethod(Invocation.getter(#cursorColor), returnValue: 0) - as int); - @override - int get backgroundColor => - (super.noSuchMethod(Invocation.getter(#backgroundColor), returnValue: 0) - as int); - @override - bool get dirty => - (super.noSuchMethod(Invocation.getter(#dirty), returnValue: false) - as bool); - @override - _i3.PlatformBehavior get platform => - (super.noSuchMethod(Invocation.getter(#platform), - returnValue: _FakePlatformBehavior_1()) as _i3.PlatformBehavior); - @override - bool get isReady => - (super.noSuchMethod(Invocation.getter(#isReady), returnValue: false) - as bool); - @override - _i6.Future get backendExited => - (super.noSuchMethod(Invocation.getter(#backendExited), - returnValue: Future.value(0)) as _i6.Future); - @override - bool get isTerminated => - (super.noSuchMethod(Invocation.getter(#isTerminated), returnValue: false) - as bool); - @override - String get composingString => - (super.noSuchMethod(Invocation.getter(#composingString), returnValue: '') - as String); - @override - _i4.TerminalSearchResult get userSearchResult => (super.noSuchMethod( - Invocation.getter(#userSearchResult), - returnValue: _FakeTerminalSearchResult_2()) as _i4.TerminalSearchResult); - @override - int get numberOfSearchHits => (super - .noSuchMethod(Invocation.getter(#numberOfSearchHits), returnValue: 0) - as int); - @override - set currentSearchHit(int? currentSearchHit) => - super.noSuchMethod(Invocation.setter(#currentSearchHit, currentSearchHit), - returnValueForMissingStub: null); - @override - _i4.TerminalSearchOptions get userSearchOptions => - (super.noSuchMethod(Invocation.getter(#userSearchOptions), - returnValue: _FakeTerminalSearchOptions_3()) - as _i4.TerminalSearchOptions); - @override - set userSearchOptions(_i4.TerminalSearchOptions? options) => - super.noSuchMethod(Invocation.setter(#userSearchOptions, options), - returnValueForMissingStub: null); - @override - set userSearchPattern(String? pattern) => - super.noSuchMethod(Invocation.setter(#userSearchPattern, pattern), - returnValueForMissingStub: null); - @override - bool get isUserSearchActive => - (super.noSuchMethod(Invocation.getter(#isUserSearchActive), - returnValue: false) as bool); - @override - set isUserSearchActive(bool? isUserSearchActive) => super.noSuchMethod( - Invocation.setter(#isUserSearchActive, isUserSearchActive), - returnValueForMissingStub: null); - @override - Set get listeners => - (super.noSuchMethod(Invocation.getter(#listeners), - returnValue: {}) as Set); - @override - List<_i7.BufferLine> getVisibleLines() => - (super.noSuchMethod(Invocation.method(#getVisibleLines, []), - returnValue: <_i7.BufferLine>[]) as List<_i7.BufferLine>); - @override - void refresh() => super.noSuchMethod(Invocation.method(#refresh, []), - returnValueForMissingStub: null); - @override - void clearSelection() => - super.noSuchMethod(Invocation.method(#clearSelection, []), - returnValueForMissingStub: null); - @override - void selectAll() => super.noSuchMethod(Invocation.method(#selectAll, []), - returnValueForMissingStub: null); - @override - void onMouseTap(_i8.Position? position) => - super.noSuchMethod(Invocation.method(#onMouseTap, [position]), - returnValueForMissingStub: null); - @override - void onMouseDoubleTap(_i8.Position? position) => - super.noSuchMethod(Invocation.method(#onMouseDoubleTap, [position]), - returnValueForMissingStub: null); - @override - void onPanStart(_i8.Position? position) => - super.noSuchMethod(Invocation.method(#onPanStart, [position]), - returnValueForMissingStub: null); - @override - void onPanUpdate(_i8.Position? position) => - super.noSuchMethod(Invocation.method(#onPanUpdate, [position]), - returnValueForMissingStub: null); - @override - void setScrollOffsetFromBottom(int? offset) => super.noSuchMethod( - Invocation.method(#setScrollOffsetFromBottom, [offset]), - returnValueForMissingStub: null); - @override - int convertViewLineToRawLine(int? viewLine) => (super.noSuchMethod( - Invocation.method(#convertViewLineToRawLine, [viewLine]), - returnValue: 0) as int); - @override - void raiseOnInput(String? input) => - super.noSuchMethod(Invocation.method(#raiseOnInput, [input]), - returnValueForMissingStub: null); - @override - void write(String? text) => - super.noSuchMethod(Invocation.method(#write, [text]), - returnValueForMissingStub: null); - @override - void paste(String? data) => - super.noSuchMethod(Invocation.method(#paste, [data]), - returnValueForMissingStub: null); - @override - void resize(int? newWidth, int? newHeight, int? newPixelWidth, - int? newPixelHeight) => - super.noSuchMethod( - Invocation.method( - #resize, [newWidth, newHeight, newPixelWidth, newPixelHeight]), - returnValueForMissingStub: null); - @override - void keyInput(_i9.TerminalKey? key, - {bool? ctrl = false, - bool? alt = false, - bool? shift = false, - bool? mac = false, - String? character}) => - super.noSuchMethod( - Invocation.method(#keyInput, [ - key - ], { - #ctrl: ctrl, - #alt: alt, - #shift: shift, - #mac: mac, - #character: character - }), - returnValueForMissingStub: null); - @override - void terminateBackend() => - super.noSuchMethod(Invocation.method(#terminateBackend, []), - returnValueForMissingStub: null); - @override - void updateComposingString(String? value) => - super.noSuchMethod(Invocation.method(#updateComposingString, [value]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); - @override - void addListener(void Function()? listener) => - super.noSuchMethod(Invocation.method(#addListener, [listener]), - returnValueForMissingStub: null); - @override - void removeListener(void Function()? listener) => - super.noSuchMethod(Invocation.method(#removeListener, [listener]), - returnValueForMissingStub: null); - @override - void notifyListeners() => - super.noSuchMethod(Invocation.method(#notifyListeners, []), - returnValueForMissingStub: null); -} diff --git a/test/next/core/buffer/line_test.dart b/test/next/core/buffer/line_test.dart new file mode 100644 index 00000000..571b81b0 --- /dev/null +++ b/test/next/core/buffer/line_test.dart @@ -0,0 +1,42 @@ +import 'package:test/test.dart'; +import 'package:xterm/core/buffer/line.dart'; + +void main() { + group('BufferLine', () { + test('getText() can get text', () { + final line = BufferLine(10); + + final text = 'ABCDEFGHIJ'; + + for (var i = 0; i < text.length; i++) { + line.setCodePoint(i, text.codeUnitAt(i)); + } + + expect(line.getText(), equals(text)); + }); + + test('getText() should support wide characters', () { + final line = BufferLine(10); + + final text = 'πŸ˜€πŸ˜πŸ˜‚πŸ€£πŸ˜ƒ'; + + for (var i = 0; i < text.runes.length; i++) { + line.setCodePoint(i * 2, text.runes.elementAt(i)); + } + + expect(line.getText(), equals(text)); + }); + + test('getTrimmedLength() can get trimmed length', () { + final line = BufferLine(10); + + final text = 'ABCDEF'; + + for (var i = 0; i < text.length; i++) { + line.setCodePoint(i, text.codeUnitAt(i)); + } + + expect(line.getTrimmedLength(), equals(text.length)); + }); + }); +} diff --git a/test/next/core/reflow_test.dart b/test/next/core/reflow_test.dart new file mode 100644 index 00000000..8431de19 --- /dev/null +++ b/test/next/core/reflow_test.dart @@ -0,0 +1,82 @@ +import 'package:test/test.dart'; +import 'package:xterm/core/terminal.dart'; + +void main() { + test('reflow() can reflow a single line', () { + final terminal = Terminal(); + + terminal.write('1234567890abcdefg'); + terminal.resize(10, 10); + + expect(terminal.buffer.lines[0].toString(), '1234567890'); + expect(terminal.buffer.lines[1].toString(), 'abcdefg'); + expect(terminal.buffer.lines[0].isWrapped, isFalse); + expect(terminal.buffer.lines[1].isWrapped, isTrue); + + terminal.resize(13, 10); + + expect(terminal.buffer.lines[0].toString(), '1234567890abc'); + expect(terminal.buffer.lines[1].toString(), 'defg'); + expect(terminal.buffer.lines[0].isWrapped, isFalse); + expect(terminal.buffer.lines[1].isWrapped, isTrue); + + terminal.resize(20, 10); + + expect(terminal.buffer.lines[0].toString(), '1234567890abcdefg'); + expect(terminal.buffer.lines[0].isWrapped, isFalse); + }); + + test('reflow() can reflow a single line to multiple lines', () { + final terminal = Terminal(); + + terminal.write('1234567890abcdefg'); + terminal.resize(5, 10); + + expect(terminal.buffer.lines[0].toString(), '12345'); + expect(terminal.buffer.lines[1].toString(), '67890'); + expect(terminal.buffer.lines[2].toString(), 'abcde'); + expect(terminal.buffer.lines[3].toString(), 'fg'); + + expect(terminal.buffer.lines[0].isWrapped, isFalse); + expect(terminal.buffer.lines[1].isWrapped, isTrue); + expect(terminal.buffer.lines[2].isWrapped, isTrue); + expect(terminal.buffer.lines[3].isWrapped, isTrue); + + terminal.resize(6, 10); + + expect(terminal.buffer.lines[0].toString(), '123456'); + expect(terminal.buffer.lines[1].toString(), '7890ab'); + expect(terminal.buffer.lines[2].toString(), 'cdefg'); + + expect(terminal.buffer.lines[0].isWrapped, isFalse); + expect(terminal.buffer.lines[1].isWrapped, isTrue); + expect(terminal.buffer.lines[2].isWrapped, isTrue); + }); + + test('reflow() can reflow wide characters', () { + final terminal = Terminal(); + + terminal.write('εΊŠε‰ζ˜Žζœˆε…‰η–‘ζ˜―εœ°δΈŠιœœ'); + terminal.resize(10, 10); + + expect(terminal.buffer.lines[0].toString(), 'εΊŠε‰ζ˜Žζœˆε…‰'); + expect(terminal.buffer.lines[1].toString(), 'η–‘ζ˜―εœ°δΈŠιœœ'); + + terminal.resize(9, 10); + + expect(terminal.buffer.lines[0].toString(), 'εΊŠε‰ζ˜Žζœˆ'); + expect(terminal.buffer.lines[1].toString(), 'ε…‰η–‘ζ˜―εœ°'); + expect(terminal.buffer.lines[2].toString(), '上霜'); + + print('-----------'); + + terminal.resize(11, 10); + + expect(terminal.buffer.lines[0].toString(), 'εΊŠε‰ζ˜Žζœˆε…‰'); + expect(terminal.buffer.lines[1].toString(), 'η–‘ζ˜―εœ°δΈŠιœœ'); + + terminal.resize(13, 10); + expect(terminal.buffer.lines[0].toString(), 'εΊŠε‰ζ˜Žζœˆε…‰η–‘'); + expect(terminal.buffer.lines[1].toString(), '是地上霜'); + }); +} diff --git a/test/terminal/terminal_isolate_test.dart b/test/terminal/terminal_isolate_test.dart index fea203b2..352d999e 100644 --- a/test/terminal/terminal_isolate_test.dart +++ b/test/terminal/terminal_isolate_test.dart @@ -1,86 +1,86 @@ -import 'dart:async'; +// import 'dart:async'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:xterm/terminal/terminal_backend.dart'; -import 'package:xterm/terminal/terminal_isolate.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:xterm/terminal/terminal_backend.dart'; +// import 'package:xterm/terminal/terminal_isolate.dart'; void main() { - group('Start behavior tests', () { - test('Using TerminalIsolate when not started throws exception', () { - final fixture = _TestFixture(); - expect(() => fixture.uut.terminalWidth, throwsA(isA())); - }); - test('Using TerminalIsolate after started doesn\'t throw exceptions', - () async { - final fixture = _TestFixture(); - - await fixture.uut.start(testingDontWaitForBootup: true); - - //no throw - fixture.uut.showCursor; - }); - }); + // group('Start behavior tests', () { + // test('Using TerminalIsolate when not started throws exception', () { + // final fixture = _TestFixture(); + // expect(() => fixture.uut.terminalWidth, throwsA(isA())); + // }); + // test('Using TerminalIsolate after started doesn\'t throw exceptions', + // () async { + // final fixture = _TestFixture(); + + // await fixture.uut.start(testingDontWaitForBootup: true); + + // //no throw + // fixture.uut.showCursor; + // }); + // }); } -class _TestFixture { - _TestFixture() { - fakeBackend = FakeBackend(); - uut = TerminalIsolate(maxLines: 10000, backend: fakeBackend); - } - - late final TerminalIsolate uut; - late final FakeBackend fakeBackend; -} - -class FakeBackend implements TerminalBackend { - @override - void ackProcessed() {} - - @override - // TODO: implement exitCode - Future get exitCode => _exitCodeCompleter.future; - - @override - void init() { - _exitCodeCompleter = Completer(); - _outStream = StreamController(); - _hasInitBeenCalled = true; - } - - @override - Stream get out => _outStream.stream; - - @override - void resize(int width, int height, int pixelWidth, int pixelHeight) { - _width = width; - _height = height; - _pixelWidth = pixelWidth; - _pixelHeight = pixelHeight; - } - - @override - void terminate() { - _isTerminated = true; - } - - @override - void write(String _) {} - - bool get hasInitBeenCalled => _hasInitBeenCalled; - bool get isTerminated => _isTerminated; - - int? get width => _width; - int? get height => _height; - int? get pixelWidth => _pixelWidth; - int? get pixelHeight => _pixelHeight; - - bool _hasInitBeenCalled = false; - bool _isTerminated = false; - int? _width; - int? _height; - int? _pixelWidth; - int? _pixelHeight; - - late final _exitCodeCompleter; - late final _outStream; -} +// class _TestFixture { +// _TestFixture() { +// fakeBackend = FakeBackend(); +// uut = TerminalIsolate(maxLines: 10000, backend: fakeBackend); +// } + +// late final TerminalIsolate uut; +// late final FakeBackend fakeBackend; +// } + +// class FakeBackend implements TerminalBackend { +// @override +// void ackProcessed() {} + +// @override +// // TODO: implement exitCode +// Future get exitCode => _exitCodeCompleter.future; + +// @override +// void init() { +// _exitCodeCompleter = Completer(); +// _outStream = StreamController(); +// _hasInitBeenCalled = true; +// } + +// @override +// Stream get out => _outStream.stream; + +// @override +// void resize(int width, int height, int pixelWidth, int pixelHeight) { +// _width = width; +// _height = height; +// _pixelWidth = pixelWidth; +// _pixelHeight = pixelHeight; +// } + +// @override +// void terminate() { +// _isTerminated = true; +// } + +// @override +// void write(String _) {} + +// bool get hasInitBeenCalled => _hasInitBeenCalled; +// bool get isTerminated => _isTerminated; + +// int? get width => _width; +// int? get height => _height; +// int? get pixelWidth => _pixelWidth; +// int? get pixelHeight => _pixelHeight; + +// bool _hasInitBeenCalled = false; +// bool _isTerminated = false; +// int? _width; +// int? _height; +// int? _pixelWidth; +// int? _pixelHeight; + +// late final _exitCodeCompleter; +// late final _outStream; +// } diff --git a/test/terminal/terminal_search_test.dart b/test/terminal/terminal_search_test.dart index d9b9839f..3c2b2dc0 100644 --- a/test/terminal/terminal_search_test.dart +++ b/test/terminal/terminal_search_test.dart @@ -1,407 +1,407 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:xterm/buffer/buffer.dart'; -import 'package:xterm/buffer/line/line.dart'; -import 'package:xterm/terminal/cursor.dart'; -import 'package:xterm/terminal/terminal_search.dart'; -import 'package:xterm/terminal/terminal_search_interaction.dart'; -import 'package:xterm/util/circular_list.dart'; -import 'package:xterm/util/unicode_v11.dart'; - -import 'terminal_search_test.mocks.dart'; - -class TerminalSearchTestCircularList extends CircularList { - TerminalSearchTestCircularList(int maxLines) : super(maxLines); -} - -@GenerateMocks([ - TerminalSearchInteraction, - Buffer, - TerminalSearchTestCircularList, - BufferLine -]) +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/annotations.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:xterm/buffer/buffer.dart'; +// import 'package:xterm/buffer/line/line.dart'; +// import 'package:xterm/terminal/cursor.dart'; +// import 'package:xterm/terminal/terminal_search.dart'; +// import 'package:xterm/terminal/terminal_search_interaction.dart'; +// import 'package:xterm/util/circular_list.dart'; +// import 'package:xterm/util/unicode_v11.dart'; + +// import 'terminal_search_test.mocks.dart'; + +// class TerminalSearchTestCircularList extends CircularList { +// TerminalSearchTestCircularList(int maxLines) : super(maxLines); +// } + +// @GenerateMocks([ +// TerminalSearchInteraction, +// Buffer, +// TerminalSearchTestCircularList, +// BufferLine +// ]) void main() { - group('Terminal Search Tests', () { - test('Creation works', () { - _TestFixture(); - }); - - test('Doesn\'t trigger anything when not activated', () { - final fixture = _TestFixture(); - verifyNoMoreInteractions(fixture.terminalSearchInteractionMock); - final task = fixture.uut.createSearchTask('testsearch'); - task.pattern = "some test"; - task.isActive = false; - task.searchResult; - }); - - test('Basic search works', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent(['Simple Content']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'content'; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: false); - final result = task.searchResult; - expect(result.allHits.length, 1); - expect(result.allHits[0].startLineIndex, 0); - expect(result.allHits[0].startIndex, 7); - expect(result.allHits[0].endLineIndex, 0); - expect(result.allHits[0].endIndex, 14); - }); - - test('Multiline search works', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'line'; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: false); - final result = task.searchResult; - expect(result.allHits.length, 1); - expect(result.allHits[0].startLineIndex, 1); - expect(result.allHits[0].startIndex, 7); - expect(result.allHits[0].endLineIndex, 1); - expect(result.allHits[0].endIndex, 11); - }); - - test('Emoji search works', () { - final fixture = _TestFixture(); - fixture.expectBufferContentLine([ - '🍏', - '🍎', - '🍐', - '🍊', - 'πŸ‹', - '🍌', - 'πŸ‰', - 'πŸ‡', - 'πŸ“', - '🫐', - '🍈', - 'πŸ’', - 'πŸ‘' - ]); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'πŸ‹'; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: false); - final result = task.searchResult; - expect(result.allHits.length, 1); - expect(result.allHits[0].startLineIndex, 0); - expect(result.allHits[0].startIndex, 8); - expect(result.allHits[0].endLineIndex, 0); - expect(result.allHits[0].endIndex, 10); - }); - - test('CJK search works', () { - final fixture = _TestFixture(); - fixture.expectBufferContentLine(['こ', 'γ‚“', 'に', 'け', 'は', 'δΈ–', 'η•Œ']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'は'; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: false); - final result = task.searchResult; - expect(result.allHits.length, 1); - expect(result.allHits[0].startLineIndex, 0); - expect(result.allHits[0].startIndex, 8); - expect(result.allHits[0].endLineIndex, 0); - expect(result.allHits[0].endIndex, 10); - }); - - test('Finding strings directly on line break works', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent([ - 'The search hit is '.padRight(fixture.terminalWidth - 3) + 'spl', - 'it over two lines', - ]); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'split'; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: false); - final result = task.searchResult; - expect(result.allHits.length, 1); - expect(result.allHits[0].startLineIndex, 0); - expect(result.allHits[0].startIndex, 77); - expect(result.allHits[0].endLineIndex, 1); - expect(result.allHits[0].endIndex, 2); - }); - }); - - test('Option: case sensitivity works', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'line'; - task.options = TerminalSearchOptions( - caseSensitive: true, matchWholeWord: false, useRegex: false); - - final result = task.searchResult; - expect(result.allHits.length, 0); - - task.pattern = 'Line'; - final secondResult = task.searchResult; - expect(secondResult.allHits.length, 1); - expect(secondResult.allHits[0].startLineIndex, 1); - expect(secondResult.allHits[0].startIndex, 7); - expect(secondResult.allHits[0].endLineIndex, 1); - expect(secondResult.allHits[0].endIndex, 11); - }); - - test('Option: whole word works', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'lin'; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: true, useRegex: false); - - final result = task.searchResult; - expect(result.allHits.length, 0); - - task.pattern = 'line'; - final secondResult = task.searchResult; - expect(secondResult.allHits.length, 1); - expect(secondResult.allHits[0].startLineIndex, 1); - expect(secondResult.allHits[0].startIndex, 7); - expect(secondResult.allHits[0].endLineIndex, 1); - expect(secondResult.allHits[0].endIndex, 11); - }); - - test('Option: regex works', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: true); - - task.pattern = - r'(^|\s)\w{4}($|\s)'; // match exactly 4 characters (and the whitespace before and/or after) - final secondResult = task.searchResult; - expect(secondResult.allHits.length, 1); - expect(secondResult.allHits[0].startLineIndex, 1); - expect(secondResult.allHits[0].startIndex, 6); - expect(secondResult.allHits[0].endLineIndex, 1); - expect(secondResult.allHits[0].endIndex, 12); - }); - - test('Retrigger search when a BufferLine got dirty works', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent( - ['Simple Content', 'Second Line', 'Third row']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: false); - - task.pattern = 'line'; - final result = task.searchResult; - expect(result.allHits.length, 1); - - // overwrite expectations, nothing dirty => no new search - fixture.expectTerminalSearchContent( - ['Simple Content', 'Second Line', 'Third line'], - isSearchStringCached: true); - task.isActive = false; - task.isActive = true; - - final secondResult = task.searchResult; - expect(secondResult.allHits.length, - 1); // nothing was dirty => we get the cached search result - - // overwrite expectations, one line is dirty => new search - fixture.expectTerminalSearchContent( - ['Simple Content', 'Second Line', 'Third line'], - isSearchStringCached: false, - dirtyIndices: [1]); - - final thirdResult = task.searchResult; - expect(thirdResult.allHits.length, - 2); //search has happened again so the new content is found - - // overwrite expectations, content has changed => new search - fixture.expectTerminalSearchContent( - ['First line', 'Second Line', 'Third line'], - isSearchStringCached: false, - dirtyIndices: [0]); - - final fourthResult = task.searchResult; - expect(fourthResult.allHits.length, - 3); //search has happened again so the new content is found - }); - test('Handles regex special characters in non regex mode correctly', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent(['Simple Content', 'Second Line.\\{']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'line.\\{'; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: false); - - final result = task.searchResult; - expect(result.allHits.length, 1); - expect(result.allHits[0].startLineIndex, 1); - expect(result.allHits[0].startIndex, 7); - expect(result.allHits[0].endLineIndex, 1); - expect(result.allHits[0].endIndex, 14); - }); - test('TerminalWidth change leads to retriggering search', () { - final fixture = _TestFixture(); - fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); - final task = fixture.uut.createSearchTask('testsearch'); - task.isActive = true; - task.pattern = 'line'; - task.options = TerminalSearchOptions( - caseSensitive: false, matchWholeWord: false, useRegex: false); - - final result = task.searchResult; - expect(result.allHits.length, 1); - - // change data to detect a search re-run - fixture.expectTerminalSearchContent( - ['First line', 'Second Line']); //has 2 hits - task.isActive = false; - task.isActive = true; - final secondResult = task.searchResult; - expect( - secondResult.allHits.length, 1); //nothing changed so the cache is used - - fixture.terminalWidth = 79; - task.isActive = false; - task.isActive = true; - final thirdResult = task.searchResult; - //we changed the terminal width which triggered a re-run of the search - expect(thirdResult.allHits.length, 2); - }); + // group('Terminal Search Tests', () { + // test('Creation works', () { + // _TestFixture(); + // }); + + // test('Doesn\'t trigger anything when not activated', () { + // final fixture = _TestFixture(); + // verifyNoMoreInteractions(fixture.terminalSearchInteractionMock); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.pattern = "some test"; + // task.isActive = false; + // task.searchResult; + // }); + + // test('Basic search works', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent(['Simple Content']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'content'; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: false); + // final result = task.searchResult; + // expect(result.allHits.length, 1); + // expect(result.allHits[0].startLineIndex, 0); + // expect(result.allHits[0].startIndex, 7); + // expect(result.allHits[0].endLineIndex, 0); + // expect(result.allHits[0].endIndex, 14); + // }); + + // test('Multiline search works', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'line'; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: false); + // final result = task.searchResult; + // expect(result.allHits.length, 1); + // expect(result.allHits[0].startLineIndex, 1); + // expect(result.allHits[0].startIndex, 7); + // expect(result.allHits[0].endLineIndex, 1); + // expect(result.allHits[0].endIndex, 11); + // }); + + // test('Emoji search works', () { + // final fixture = _TestFixture(); + // fixture.expectBufferContentLine([ + // '🍏', + // '🍎', + // '🍐', + // '🍊', + // 'πŸ‹', + // '🍌', + // 'πŸ‰', + // 'πŸ‡', + // 'πŸ“', + // '🫐', + // '🍈', + // 'πŸ’', + // 'πŸ‘' + // ]); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'πŸ‹'; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: false); + // final result = task.searchResult; + // expect(result.allHits.length, 1); + // expect(result.allHits[0].startLineIndex, 0); + // expect(result.allHits[0].startIndex, 8); + // expect(result.allHits[0].endLineIndex, 0); + // expect(result.allHits[0].endIndex, 10); + // }); + + // test('CJK search works', () { + // final fixture = _TestFixture(); + // fixture.expectBufferContentLine(['こ', 'γ‚“', 'に', 'け', 'は', 'δΈ–', 'η•Œ']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'は'; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: false); + // final result = task.searchResult; + // expect(result.allHits.length, 1); + // expect(result.allHits[0].startLineIndex, 0); + // expect(result.allHits[0].startIndex, 8); + // expect(result.allHits[0].endLineIndex, 0); + // expect(result.allHits[0].endIndex, 10); + // }); + + // test('Finding strings directly on line break works', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent([ + // 'The search hit is '.padRight(fixture.terminalWidth - 3) + 'spl', + // 'it over two lines', + // ]); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'split'; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: false); + // final result = task.searchResult; + // expect(result.allHits.length, 1); + // expect(result.allHits[0].startLineIndex, 0); + // expect(result.allHits[0].startIndex, 77); + // expect(result.allHits[0].endLineIndex, 1); + // expect(result.allHits[0].endIndex, 2); + // }); + // }); + + // test('Option: case sensitivity works', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'line'; + // task.options = TerminalSearchOptions( + // caseSensitive: true, matchWholeWord: false, useRegex: false); + + // final result = task.searchResult; + // expect(result.allHits.length, 0); + + // task.pattern = 'Line'; + // final secondResult = task.searchResult; + // expect(secondResult.allHits.length, 1); + // expect(secondResult.allHits[0].startLineIndex, 1); + // expect(secondResult.allHits[0].startIndex, 7); + // expect(secondResult.allHits[0].endLineIndex, 1); + // expect(secondResult.allHits[0].endIndex, 11); + // }); + + // test('Option: whole word works', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'lin'; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: true, useRegex: false); + + // final result = task.searchResult; + // expect(result.allHits.length, 0); + + // task.pattern = 'line'; + // final secondResult = task.searchResult; + // expect(secondResult.allHits.length, 1); + // expect(secondResult.allHits[0].startLineIndex, 1); + // expect(secondResult.allHits[0].startIndex, 7); + // expect(secondResult.allHits[0].endLineIndex, 1); + // expect(secondResult.allHits[0].endIndex, 11); + // }); + + // test('Option: regex works', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: true); + + // task.pattern = + // r'(^|\s)\w{4}($|\s)'; // match exactly 4 characters (and the whitespace before and/or after) + // final secondResult = task.searchResult; + // expect(secondResult.allHits.length, 1); + // expect(secondResult.allHits[0].startLineIndex, 1); + // expect(secondResult.allHits[0].startIndex, 6); + // expect(secondResult.allHits[0].endLineIndex, 1); + // expect(secondResult.allHits[0].endIndex, 12); + // }); + + // test('Retrigger search when a BufferLine got dirty works', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent( + // ['Simple Content', 'Second Line', 'Third row']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: false); + + // task.pattern = 'line'; + // final result = task.searchResult; + // expect(result.allHits.length, 1); + + // // overwrite expectations, nothing dirty => no new search + // fixture.expectTerminalSearchContent( + // ['Simple Content', 'Second Line', 'Third line'], + // isSearchStringCached: true); + // task.isActive = false; + // task.isActive = true; + + // final secondResult = task.searchResult; + // expect(secondResult.allHits.length, + // 1); // nothing was dirty => we get the cached search result + + // // overwrite expectations, one line is dirty => new search + // fixture.expectTerminalSearchContent( + // ['Simple Content', 'Second Line', 'Third line'], + // isSearchStringCached: false, + // dirtyIndices: [1]); + + // final thirdResult = task.searchResult; + // expect(thirdResult.allHits.length, + // 2); //search has happened again so the new content is found + + // // overwrite expectations, content has changed => new search + // fixture.expectTerminalSearchContent( + // ['First line', 'Second Line', 'Third line'], + // isSearchStringCached: false, + // dirtyIndices: [0]); + + // final fourthResult = task.searchResult; + // expect(fourthResult.allHits.length, + // 3); //search has happened again so the new content is found + // }); + // test('Handles regex special characters in non regex mode correctly', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent(['Simple Content', 'Second Line.\\{']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'line.\\{'; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: false); + + // final result = task.searchResult; + // expect(result.allHits.length, 1); + // expect(result.allHits[0].startLineIndex, 1); + // expect(result.allHits[0].startIndex, 7); + // expect(result.allHits[0].endLineIndex, 1); + // expect(result.allHits[0].endIndex, 14); + // }); + // test('TerminalWidth change leads to retriggering search', () { + // final fixture = _TestFixture(); + // fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']); + // final task = fixture.uut.createSearchTask('testsearch'); + // task.isActive = true; + // task.pattern = 'line'; + // task.options = TerminalSearchOptions( + // caseSensitive: false, matchWholeWord: false, useRegex: false); + + // final result = task.searchResult; + // expect(result.allHits.length, 1); + + // // change data to detect a search re-run + // fixture.expectTerminalSearchContent( + // ['First line', 'Second Line']); //has 2 hits + // task.isActive = false; + // task.isActive = true; + // final secondResult = task.searchResult; + // expect( + // secondResult.allHits.length, 1); //nothing changed so the cache is used + + // fixture.terminalWidth = 79; + // task.isActive = false; + // task.isActive = true; + // final thirdResult = task.searchResult; + // //we changed the terminal width which triggered a re-run of the search + // expect(thirdResult.allHits.length, 2); + // }); } -class _TestFixture { - _TestFixture({ - terminalWidth = 80, - }) : _terminalWidth = terminalWidth { - uut = TerminalSearch(terminalSearchInteractionMock); - when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth); - } - - int _terminalWidth; - int get terminalWidth => _terminalWidth; - set terminalWidth(int terminalWidth) { - _terminalWidth = terminalWidth; - when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth); - } - - void expectBufferContentLine( - List cellData, { - isUsingAltBuffer = false, - }) { - final buffer = _getBufferFromCellData(cellData); - when(terminalSearchInteractionMock.buffer).thenReturn(buffer); - when(terminalSearchInteractionMock.isUsingAltBuffer()) - .thenReturn(isUsingAltBuffer); - } - - void expectTerminalSearchContent( - List lines, { - isUsingAltBuffer = false, - isSearchStringCached = true, - List? dirtyIndices, - }) { - final buffer = _getBuffer(lines, - isCached: isSearchStringCached, dirtyIndices: dirtyIndices); - - when(terminalSearchInteractionMock.buffer).thenReturn(buffer); - when(terminalSearchInteractionMock.isUsingAltBuffer()) - .thenReturn(isUsingAltBuffer); - } - - final terminalSearchInteractionMock = MockTerminalSearchInteraction(); - late final TerminalSearch uut; - - MockBuffer _getBufferFromCellData(List cellData) { - final result = MockBuffer(); - final circularList = MockTerminalSearchTestCircularList(); - when(result.lines).thenReturn(circularList); - when(circularList[0]).thenReturn(_getBufferLineFromData(cellData)); - when(circularList.length).thenReturn(1); - - return result; - } - - MockBuffer _getBuffer( - List lines, { - isCached = true, - List? dirtyIndices, - }) { - final result = MockBuffer(); - final circularList = MockTerminalSearchTestCircularList(); - when(result.lines).thenReturn(circularList); - - final bufferLines = _getBufferLinesWithSearchContent( - lines, - isCached: isCached, - dirtyIndices: dirtyIndices, - ); - - when(circularList[any]).thenAnswer( - (realInvocation) => bufferLines[realInvocation.positionalArguments[0]]); - when(circularList.length).thenReturn(bufferLines.length); - - return result; - } - - BufferLine _getBufferLineFromData(List cellData) { - final result = BufferLine(length: _terminalWidth); - int currentIndex = 0; - for (var data in cellData) { - final codePoint = data.runes.first; - final width = unicodeV11.wcwidth(codePoint); - result.cellInitialize( - currentIndex, - content: codePoint, - width: width, - cursor: Cursor(bg: 0, fg: 0, flags: 0), - ); - currentIndex++; - for (int i = 1; i < width; i++) { - result.cellInitialize( - currentIndex, - content: 0, - width: 0, - cursor: Cursor(bg: 0, fg: 0, flags: 0), - ); - currentIndex++; - } - } - return result; - } - - List _getBufferLinesWithSearchContent( - List content, { - isCached = true, - List? dirtyIndices, - }) { - final result = List.empty(growable: true); - for (int i = 0; i < content.length; i++) { - final bl = MockBufferLine(); - when(bl.hasCachedSearchString).thenReturn(isCached); - when(bl.toSearchString(any)).thenReturn(content[i]); - if (dirtyIndices?.contains(i) ?? false) { - when(bl.isTagDirty(any)).thenReturn(true); - } else { - when(bl.isTagDirty(any)).thenReturn(false); - } - result.add(bl); - } - - return result; - } -} +// class _TestFixture { +// _TestFixture({ +// terminalWidth = 80, +// }) : _terminalWidth = terminalWidth { +// uut = TerminalSearch(terminalSearchInteractionMock); +// when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth); +// } + +// int _terminalWidth; +// int get terminalWidth => _terminalWidth; +// set terminalWidth(int terminalWidth) { +// _terminalWidth = terminalWidth; +// when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth); +// } + +// void expectBufferContentLine( +// List cellData, { +// isUsingAltBuffer = false, +// }) { +// final buffer = _getBufferFromCellData(cellData); +// when(terminalSearchInteractionMock.buffer).thenReturn(buffer); +// when(terminalSearchInteractionMock.isUsingAltBuffer()) +// .thenReturn(isUsingAltBuffer); +// } + +// void expectTerminalSearchContent( +// List lines, { +// isUsingAltBuffer = false, +// isSearchStringCached = true, +// List? dirtyIndices, +// }) { +// final buffer = _getBuffer(lines, +// isCached: isSearchStringCached, dirtyIndices: dirtyIndices); + +// when(terminalSearchInteractionMock.buffer).thenReturn(buffer); +// when(terminalSearchInteractionMock.isUsingAltBuffer()) +// .thenReturn(isUsingAltBuffer); +// } + +// final terminalSearchInteractionMock = MockTerminalSearchInteraction(); +// late final TerminalSearch uut; + +// MockBuffer _getBufferFromCellData(List cellData) { +// final result = MockBuffer(); +// final circularList = MockTerminalSearchTestCircularList(); +// when(result.lines).thenReturn(circularList); +// when(circularList[0]).thenReturn(_getBufferLineFromData(cellData)); +// when(circularList.length).thenReturn(1); + +// return result; +// } + +// MockBuffer _getBuffer( +// List lines, { +// isCached = true, +// List? dirtyIndices, +// }) { +// final result = MockBuffer(); +// final circularList = MockTerminalSearchTestCircularList(); +// when(result.lines).thenReturn(circularList); + +// final bufferLines = _getBufferLinesWithSearchContent( +// lines, +// isCached: isCached, +// dirtyIndices: dirtyIndices, +// ); + +// when(circularList[any]).thenAnswer( +// (realInvocation) => bufferLines[realInvocation.positionalArguments[0]]); +// when(circularList.length).thenReturn(bufferLines.length); + +// return result; +// } + +// BufferLine _getBufferLineFromData(List cellData) { +// final result = BufferLine(length: _terminalWidth); +// int currentIndex = 0; +// for (var data in cellData) { +// final codePoint = data.runes.first; +// final width = unicodeV11.wcwidth(codePoint); +// result.cellInitialize( +// currentIndex, +// content: codePoint, +// width: width, +// cursor: Cursor(bg: 0, fg: 0, flags: 0), +// ); +// currentIndex++; +// for (int i = 1; i < width; i++) { +// result.cellInitialize( +// currentIndex, +// content: 0, +// width: 0, +// cursor: Cursor(bg: 0, fg: 0, flags: 0), +// ); +// currentIndex++; +// } +// } +// return result; +// } + +// List _getBufferLinesWithSearchContent( +// List content, { +// isCached = true, +// List? dirtyIndices, +// }) { +// final result = List.empty(growable: true); +// for (int i = 0; i < content.length; i++) { +// final bl = MockBufferLine(); +// when(bl.hasCachedSearchString).thenReturn(isCached); +// when(bl.toSearchString(any)).thenReturn(content[i]); +// if (dirtyIndices?.contains(i) ?? false) { +// when(bl.isTagDirty(any)).thenReturn(true); +// } else { +// when(bl.isTagDirty(any)).thenReturn(false); +// } +// result.add(bl); +// } + +// return result; +// } +// } diff --git a/test/terminal/terminal_search_test.mocks.dart b/test/terminal/terminal_search_test.mocks.dart deleted file mode 100644 index 49708ca4..00000000 --- a/test/terminal/terminal_search_test.mocks.dart +++ /dev/null @@ -1,555 +0,0 @@ -// Mocks generated by Mockito 5.0.15 from annotations -// in xterm/test/terminal/terminal_search_test.dart. -// Do not manually edit this file. - -import 'package:mockito/mockito.dart' as _i1; -import 'package:xterm/buffer/buffer.dart' as _i2; -import 'package:xterm/buffer/line/line.dart' as _i5; -import 'package:xterm/terminal/charset.dart' as _i3; -import 'package:xterm/terminal/cursor.dart' as _i9; -import 'package:xterm/terminal/terminal_search_interaction.dart' as _i7; -import 'package:xterm/util/circular_list.dart' as _i4; -import 'package:xterm/util/scroll_range.dart' as _i6; - -import 'terminal_search_test.dart' as _i8; - -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -class _FakeBuffer_0 extends _i1.Fake implements _i2.Buffer {} - -class _FakeCharset_1 extends _i1.Fake implements _i3.Charset {} - -class _FakeCircularList_2 extends _i1.Fake implements _i4.CircularList {} - -class _FakeBufferLine_3 extends _i1.Fake implements _i5.BufferLine {} - -class _FakeScrollRange_4 extends _i1.Fake implements _i6.ScrollRange {} - -class _FakeBufferLineData_5 extends _i1.Fake implements _i5.BufferLineData {} - -/// A class which mocks [TerminalSearchInteraction]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTerminalSearchInteraction extends _i1.Mock - implements _i7.TerminalSearchInteraction { - MockTerminalSearchInteraction() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.Buffer get buffer => (super.noSuchMethod(Invocation.getter(#buffer), - returnValue: _FakeBuffer_0()) as _i2.Buffer); - @override - int get terminalWidth => - (super.noSuchMethod(Invocation.getter(#terminalWidth), returnValue: 0) - as int); - @override - bool isUsingAltBuffer() => - (super.noSuchMethod(Invocation.method(#isUsingAltBuffer, []), - returnValue: false) as bool); - @override - String toString() => super.toString(); -} - -/// A class which mocks [Buffer]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockBuffer extends _i1.Mock implements _i2.Buffer { - MockBuffer() { - _i1.throwOnMissingStub(this); - } - - @override - bool get isAltBuffer => - (super.noSuchMethod(Invocation.getter(#isAltBuffer), returnValue: false) - as bool); - @override - _i3.Charset get charset => (super.noSuchMethod(Invocation.getter(#charset), - returnValue: _FakeCharset_1()) as _i3.Charset); - @override - _i4.CircularList<_i5.BufferLine> get lines => - (super.noSuchMethod(Invocation.getter(#lines), - returnValue: _FakeCircularList_2<_i5.BufferLine>()) - as _i4.CircularList<_i5.BufferLine>); - @override - set lines(_i4.CircularList<_i5.BufferLine>? _lines) => - super.noSuchMethod(Invocation.setter(#lines, _lines), - returnValueForMissingStub: null); - @override - int get viewHeight => - (super.noSuchMethod(Invocation.getter(#viewHeight), returnValue: 0) - as int); - @override - int get viewWidth => - (super.noSuchMethod(Invocation.getter(#viewWidth), returnValue: 0) - as int); - @override - int get scrollOffsetFromBottom => - (super.noSuchMethod(Invocation.getter(#scrollOffsetFromBottom), - returnValue: 0) as int); - @override - int get scrollOffsetFromTop => (super - .noSuchMethod(Invocation.getter(#scrollOffsetFromTop), returnValue: 0) - as int); - @override - bool get isUserScrolling => (super - .noSuchMethod(Invocation.getter(#isUserScrolling), returnValue: false) - as bool); - @override - int get cursorX => - (super.noSuchMethod(Invocation.getter(#cursorX), returnValue: 0) as int); - @override - int get cursorY => - (super.noSuchMethod(Invocation.getter(#cursorY), returnValue: 0) as int); - @override - int get marginTop => - (super.noSuchMethod(Invocation.getter(#marginTop), returnValue: 0) - as int); - @override - int get marginBottom => - (super.noSuchMethod(Invocation.getter(#marginBottom), returnValue: 0) - as int); - @override - _i5.BufferLine get currentLine => - (super.noSuchMethod(Invocation.getter(#currentLine), - returnValue: _FakeBufferLine_3()) as _i5.BufferLine); - @override - int get height => - (super.noSuchMethod(Invocation.getter(#height), returnValue: 0) as int); - @override - bool get hasScrollableRegion => - (super.noSuchMethod(Invocation.getter(#hasScrollableRegion), - returnValue: false) as bool); - @override - bool get isInScrollableRegion => - (super.noSuchMethod(Invocation.getter(#isInScrollableRegion), - returnValue: false) as bool); - @override - void write(String? text) => - super.noSuchMethod(Invocation.method(#write, [text]), - returnValueForMissingStub: null); - @override - void writeChar(int? codePoint) => - super.noSuchMethod(Invocation.method(#writeChar, [codePoint]), - returnValueForMissingStub: null); - @override - _i5.BufferLine getViewLine(int? index) => - (super.noSuchMethod(Invocation.method(#getViewLine, [index]), - returnValue: _FakeBufferLine_3()) as _i5.BufferLine); - @override - int convertViewLineToRawLine(int? viewLine) => (super.noSuchMethod( - Invocation.method(#convertViewLineToRawLine, [viewLine]), - returnValue: 0) as int); - @override - int convertRawLineToViewLine(int? rawLine) => (super.noSuchMethod( - Invocation.method(#convertRawLineToViewLine, [rawLine]), - returnValue: 0) as int); - @override - void newLine() => super.noSuchMethod(Invocation.method(#newLine, []), - returnValueForMissingStub: null); - @override - void carriageReturn() => - super.noSuchMethod(Invocation.method(#carriageReturn, []), - returnValueForMissingStub: null); - @override - void backspace() => super.noSuchMethod(Invocation.method(#backspace, []), - returnValueForMissingStub: null); - @override - List<_i5.BufferLine> getVisibleLines() => - (super.noSuchMethod(Invocation.method(#getVisibleLines, []), - returnValue: <_i5.BufferLine>[]) as List<_i5.BufferLine>); - @override - void eraseDisplayFromCursor() => - super.noSuchMethod(Invocation.method(#eraseDisplayFromCursor, []), - returnValueForMissingStub: null); - @override - void eraseDisplayToCursor() => - super.noSuchMethod(Invocation.method(#eraseDisplayToCursor, []), - returnValueForMissingStub: null); - @override - void eraseDisplay() => - super.noSuchMethod(Invocation.method(#eraseDisplay, []), - returnValueForMissingStub: null); - @override - void eraseLineFromCursor() => - super.noSuchMethod(Invocation.method(#eraseLineFromCursor, []), - returnValueForMissingStub: null); - @override - void eraseLineToCursor() => - super.noSuchMethod(Invocation.method(#eraseLineToCursor, []), - returnValueForMissingStub: null); - @override - void eraseLine() => super.noSuchMethod(Invocation.method(#eraseLine, []), - returnValueForMissingStub: null); - @override - void eraseCharacters(int? count) => - super.noSuchMethod(Invocation.method(#eraseCharacters, [count]), - returnValueForMissingStub: null); - @override - _i6.ScrollRange getAreaScrollRange() => - (super.noSuchMethod(Invocation.method(#getAreaScrollRange, []), - returnValue: _FakeScrollRange_4()) as _i6.ScrollRange); - @override - void areaScrollDown(int? lines) => - super.noSuchMethod(Invocation.method(#areaScrollDown, [lines]), - returnValueForMissingStub: null); - @override - void areaScrollUp(int? lines) => - super.noSuchMethod(Invocation.method(#areaScrollUp, [lines]), - returnValueForMissingStub: null); - @override - void index() => super.noSuchMethod(Invocation.method(#index, []), - returnValueForMissingStub: null); - @override - void reverseIndex() => - super.noSuchMethod(Invocation.method(#reverseIndex, []), - returnValueForMissingStub: null); - @override - void cursorGoForward() => - super.noSuchMethod(Invocation.method(#cursorGoForward, []), - returnValueForMissingStub: null); - @override - void setCursorX(int? cursorX) => - super.noSuchMethod(Invocation.method(#setCursorX, [cursorX]), - returnValueForMissingStub: null); - @override - void setCursorY(int? cursorY) => - super.noSuchMethod(Invocation.method(#setCursorY, [cursorY]), - returnValueForMissingStub: null); - @override - void moveCursorX(int? offset) => - super.noSuchMethod(Invocation.method(#moveCursorX, [offset]), - returnValueForMissingStub: null); - @override - void moveCursorY(int? offset) => - super.noSuchMethod(Invocation.method(#moveCursorY, [offset]), - returnValueForMissingStub: null); - @override - void setPosition(int? cursorX, int? cursorY) => - super.noSuchMethod(Invocation.method(#setPosition, [cursorX, cursorY]), - returnValueForMissingStub: null); - @override - void movePosition(int? offsetX, int? offsetY) => - super.noSuchMethod(Invocation.method(#movePosition, [offsetX, offsetY]), - returnValueForMissingStub: null); - @override - void setScrollOffsetFromBottom(int? offsetFromBottom) => super.noSuchMethod( - Invocation.method(#setScrollOffsetFromBottom, [offsetFromBottom]), - returnValueForMissingStub: null); - @override - void setScrollOffsetFromTop(int? offsetFromTop) => super.noSuchMethod( - Invocation.method(#setScrollOffsetFromTop, [offsetFromTop]), - returnValueForMissingStub: null); - @override - void screenScrollUp(int? lines) => - super.noSuchMethod(Invocation.method(#screenScrollUp, [lines]), - returnValueForMissingStub: null); - @override - void screenScrollDown(int? lines) => - super.noSuchMethod(Invocation.method(#screenScrollDown, [lines]), - returnValueForMissingStub: null); - @override - void saveCursor() => super.noSuchMethod(Invocation.method(#saveCursor, []), - returnValueForMissingStub: null); - @override - void restoreCursor() => - super.noSuchMethod(Invocation.method(#restoreCursor, []), - returnValueForMissingStub: null); - @override - void setVerticalMargins(int? top, int? bottom) => - super.noSuchMethod(Invocation.method(#setVerticalMargins, [top, bottom]), - returnValueForMissingStub: null); - @override - void resetVerticalMargins() => - super.noSuchMethod(Invocation.method(#resetVerticalMargins, []), - returnValueForMissingStub: null); - @override - void deleteChars(int? count) => - super.noSuchMethod(Invocation.method(#deleteChars, [count]), - returnValueForMissingStub: null); - @override - void clearScrollback() => - super.noSuchMethod(Invocation.method(#clearScrollback, []), - returnValueForMissingStub: null); - @override - void clear() => super.noSuchMethod(Invocation.method(#clear, []), - returnValueForMissingStub: null); - @override - void insertBlankCharacters(int? count) => - super.noSuchMethod(Invocation.method(#insertBlankCharacters, [count]), - returnValueForMissingStub: null); - @override - void insertLines(int? count) => - super.noSuchMethod(Invocation.method(#insertLines, [count]), - returnValueForMissingStub: null); - @override - void insertLine() => super.noSuchMethod(Invocation.method(#insertLine, []), - returnValueForMissingStub: null); - @override - void deleteLines(int? count) => - super.noSuchMethod(Invocation.method(#deleteLines, [count]), - returnValueForMissingStub: null); - @override - void deleteLine() => super.noSuchMethod(Invocation.method(#deleteLine, []), - returnValueForMissingStub: null); - @override - void resize(int? oldWidth, int? oldHeight, int? newWidth, int? newHeight) => - super.noSuchMethod( - Invocation.method( - #resize, [oldWidth, oldHeight, newWidth, newHeight]), - returnValueForMissingStub: null); - @override - dynamic adjustSavedCursor(int? dx, int? dy) => - super.noSuchMethod(Invocation.method(#adjustSavedCursor, [dx, dy])); - @override - String toString() => super.toString(); -} - -/// A class which mocks [TerminalSearchTestCircularList]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTerminalSearchTestCircularList extends _i1.Mock - implements _i8.TerminalSearchTestCircularList { - MockTerminalSearchTestCircularList() { - _i1.throwOnMissingStub(this); - } - - @override - int get maxLength => - (super.noSuchMethod(Invocation.getter(#maxLength), returnValue: 0) - as int); - @override - set maxLength(int? value) => - super.noSuchMethod(Invocation.setter(#maxLength, value), - returnValueForMissingStub: null); - @override - int get length => - (super.noSuchMethod(Invocation.getter(#length), returnValue: 0) as int); - @override - set length(int? value) => - super.noSuchMethod(Invocation.setter(#length, value), - returnValueForMissingStub: null); - @override - bool get isFull => - (super.noSuchMethod(Invocation.getter(#isFull), returnValue: false) - as bool); - @override - void forEach(void Function(_i5.BufferLine)? callback) => - super.noSuchMethod(Invocation.method(#forEach, [callback]), - returnValueForMissingStub: null); - @override - _i5.BufferLine operator [](int? index) => - (super.noSuchMethod(Invocation.method(#[], [index]), - returnValue: _FakeBufferLine_3()) as _i5.BufferLine); - @override - void operator []=(int? index, _i5.BufferLine? value) => - super.noSuchMethod(Invocation.method(#[]=, [index, value]), - returnValueForMissingStub: null); - @override - void clear() => super.noSuchMethod(Invocation.method(#clear, []), - returnValueForMissingStub: null); - @override - void pushAll(Iterable<_i5.BufferLine>? items) => - super.noSuchMethod(Invocation.method(#pushAll, [items]), - returnValueForMissingStub: null); - @override - void push(_i5.BufferLine? value) => - super.noSuchMethod(Invocation.method(#push, [value]), - returnValueForMissingStub: null); - @override - _i5.BufferLine pop() => (super.noSuchMethod(Invocation.method(#pop, []), - returnValue: _FakeBufferLine_3()) as _i5.BufferLine); - @override - void remove(int? index, [int? count = 1]) => - super.noSuchMethod(Invocation.method(#remove, [index, count]), - returnValueForMissingStub: null); - @override - void insert(int? index, _i5.BufferLine? item) => - super.noSuchMethod(Invocation.method(#insert, [index, item]), - returnValueForMissingStub: null); - @override - void insertAll(int? index, List<_i5.BufferLine>? items) => - super.noSuchMethod(Invocation.method(#insertAll, [index, items]), - returnValueForMissingStub: null); - @override - void trimStart(int? count) => - super.noSuchMethod(Invocation.method(#trimStart, [count]), - returnValueForMissingStub: null); - @override - void shiftElements(int? start, int? count, int? offset) => super.noSuchMethod( - Invocation.method(#shiftElements, [start, count, offset]), - returnValueForMissingStub: null); - @override - void replaceWith(List<_i5.BufferLine>? replacement) => - super.noSuchMethod(Invocation.method(#replaceWith, [replacement]), - returnValueForMissingStub: null); - @override - List<_i5.BufferLine> toList() => - (super.noSuchMethod(Invocation.method(#toList, []), - returnValue: <_i5.BufferLine>[]) as List<_i5.BufferLine>); - @override - String toString() => super.toString(); -} - -/// A class which mocks [BufferLine]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockBufferLine extends _i1.Mock implements _i5.BufferLine { - MockBufferLine() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.BufferLineData get data => (super.noSuchMethod(Invocation.getter(#data), - returnValue: _FakeBufferLineData_5()) as _i5.BufferLineData); - @override - bool get isWrapped => - (super.noSuchMethod(Invocation.getter(#isWrapped), returnValue: false) - as bool); - @override - set isWrapped(bool? value) => - super.noSuchMethod(Invocation.setter(#isWrapped, value), - returnValueForMissingStub: null); - @override - bool get hasCachedSearchString => - (super.noSuchMethod(Invocation.getter(#hasCachedSearchString), - returnValue: false) as bool); - @override - void markTagAsNonDirty(String? tag) => - super.noSuchMethod(Invocation.method(#markTagAsNonDirty, [tag]), - returnValueForMissingStub: null); - @override - bool isTagDirty(String? tag) => - (super.noSuchMethod(Invocation.method(#isTagDirty, [tag]), - returnValue: false) as bool); - @override - void ensure(int? length) => - super.noSuchMethod(Invocation.method(#ensure, [length]), - returnValueForMissingStub: null); - @override - void insert(int? index) => - super.noSuchMethod(Invocation.method(#insert, [index]), - returnValueForMissingStub: null); - @override - void insertN(int? index, int? count) => - super.noSuchMethod(Invocation.method(#insertN, [index, count]), - returnValueForMissingStub: null); - @override - void removeN(int? index, int? count) => - super.noSuchMethod(Invocation.method(#removeN, [index, count]), - returnValueForMissingStub: null); - @override - void clear() => super.noSuchMethod(Invocation.method(#clear, []), - returnValueForMissingStub: null); - @override - void erase(_i9.Cursor? cursor, int? start, int? end, - [bool? resetIsWrapped = false]) => - super.noSuchMethod( - Invocation.method(#erase, [cursor, start, end, resetIsWrapped]), - returnValueForMissingStub: null); - @override - void cellClear(int? index) => - super.noSuchMethod(Invocation.method(#cellClear, [index]), - returnValueForMissingStub: null); - @override - void cellInitialize(int? index, - {int? content, int? width, _i9.Cursor? cursor}) => - super.noSuchMethod( - Invocation.method(#cellInitialize, [index], - {#content: content, #width: width, #cursor: cursor}), - returnValueForMissingStub: null); - @override - bool cellHasContent(int? index) => - (super.noSuchMethod(Invocation.method(#cellHasContent, [index]), - returnValue: false) as bool); - @override - int cellGetContent(int? index) => - (super.noSuchMethod(Invocation.method(#cellGetContent, [index]), - returnValue: 0) as int); - @override - void cellSetContent(int? index, int? content) => - super.noSuchMethod(Invocation.method(#cellSetContent, [index, content]), - returnValueForMissingStub: null); - @override - int cellGetFgColor(int? index) => - (super.noSuchMethod(Invocation.method(#cellGetFgColor, [index]), - returnValue: 0) as int); - @override - void cellSetFgColor(int? index, int? color) => - super.noSuchMethod(Invocation.method(#cellSetFgColor, [index, color]), - returnValueForMissingStub: null); - @override - int cellGetBgColor(int? index) => - (super.noSuchMethod(Invocation.method(#cellGetBgColor, [index]), - returnValue: 0) as int); - @override - void cellSetBgColor(int? index, int? color) => - super.noSuchMethod(Invocation.method(#cellSetBgColor, [index, color]), - returnValueForMissingStub: null); - @override - int cellGetFlags(int? index) => - (super.noSuchMethod(Invocation.method(#cellGetFlags, [index]), - returnValue: 0) as int); - @override - void cellSetFlags(int? index, int? flags) => - super.noSuchMethod(Invocation.method(#cellSetFlags, [index, flags]), - returnValueForMissingStub: null); - @override - int cellGetWidth(int? index) => - (super.noSuchMethod(Invocation.method(#cellGetWidth, [index]), - returnValue: 0) as int); - @override - void cellSetWidth(int? index, int? width) => - super.noSuchMethod(Invocation.method(#cellSetWidth, [index, width]), - returnValueForMissingStub: null); - @override - void cellClearFlags(int? index) => - super.noSuchMethod(Invocation.method(#cellClearFlags, [index]), - returnValueForMissingStub: null); - @override - bool cellHasFlag(int? index, int? flag) => - (super.noSuchMethod(Invocation.method(#cellHasFlag, [index, flag]), - returnValue: false) as bool); - @override - void cellSetFlag(int? index, int? flag) => - super.noSuchMethod(Invocation.method(#cellSetFlag, [index, flag]), - returnValueForMissingStub: null); - @override - void cellErase(int? index, _i9.Cursor? cursor) => - super.noSuchMethod(Invocation.method(#cellErase, [index, cursor]), - returnValueForMissingStub: null); - @override - int getTrimmedLength([int? cols]) => - (super.noSuchMethod(Invocation.method(#getTrimmedLength, [cols]), - returnValue: 0) as int); - @override - void copyCellsFrom(_i5.BufferLine? src, int? srcCol, int? dstCol, int? len) => - super.noSuchMethod( - Invocation.method(#copyCellsFrom, [src, srcCol, dstCol, len]), - returnValueForMissingStub: null); - @override - void removeRange(int? start, int? end) => - super.noSuchMethod(Invocation.method(#removeRange, [start, end]), - returnValueForMissingStub: null); - @override - void clearRange(int? start, int? end) => - super.noSuchMethod(Invocation.method(#clearRange, [start, end]), - returnValueForMissingStub: null); - @override - String toDebugString(int? cols) => - (super.noSuchMethod(Invocation.method(#toDebugString, [cols]), - returnValue: '') as String); - @override - String toSearchString(int? cols) => - (super.noSuchMethod(Invocation.method(#toSearchString, [cols]), - returnValue: '') as String); - @override - String toString() => super.toString(); -} diff --git a/test/util/circular_list_test.dart b/test/util/circular_list_test.dart index d52bcb1a..3f746e34 100644 --- a/test/util/circular_list_test.dart +++ b/test/util/circular_list_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:xterm/util/circular_list.dart'; +import 'package:xterm/utils/circular_list.dart'; void main() { group("CircularList Tests", () {