This article is relatively chicken ribs, interested in the following article (edited in 2021.04.18)

preface

  • I’m still the nightmare beast that uses the ancestral code 🤫.

  • I had a dream that someone came back and said to review the period for the final exam. This was the third article. Recently, due to the increased demand for this part of the project, I took a whole afternoon to optimize the code for this part.

  • It all started when I needed a full terminal emulator for my personal project.

The UI of the personal project is a pure Flutter project, which does not involve any native pages. If a terminal simulator needs to be integrated, then:

  • 1. I can connect Termux open source View with PlatformView.
  • 2. Reconstruct a cross-platform terminal emulator using Flutter

My initial intention to use Flutter for my personal project was not to cross ios, but to cross PC, so is there a choice 🤣?

The last article was written in a hurry, and it was just an analysis of the underlying implementation principles of the terminal emulator.

In this article we describe how to attach it to Flutter and cross MAC/Linux/Android platforms with minimal code changes.

Open source a complete terminal emulator for Flutter

The open source address in the previous article was the address of the project that integrated it

This paper mainly deals with

  • 1.Dart Creates a terminal
  • 2.Dart implementation of terminal input and output
  • 3. Rewrite of terminal sequence
  • 4. Display of the Flutter terminal
  • 5. Manage and create multiple terminals

Open source addresses are at the end

1.Dart Creates a terminal

As can be seen from the previous article, C Native provides us with two functions (see the previous article for details).

  • Creating a Terminal Pair
int create_ptm(int rows,int columns)
Copy the code
  • Execute the subroutine on the obtained terminal pair
int create_subprocess(char *env,char const *cmd,char const *cwd,char *const argv[],char **envp,int *pProcessId,int ptmfd)
Copy the code

In fact, there should be several more. Currently, because the font of Flutter is as Design, it is impossible to set the screen width to control the timing of its line break. Please send me a private message if you have

Dart: A set of FFI is simply a matter of matching native methods or functions to DART methods or functions one by one and then binding them to each other.

This part requires ffI’s package

1.1 Creating terminal Pairs

The corresponding declaration of native functions in Dart

typedef create_ptm = Int32 Function(Int32 row, Int32 column);
Copy the code

Don’t capitalize the name because it’s a native function

The function that corresponds to the Dart callable

typedef CreatePtm = int Function(int row, int column);
Copy the code

Create a pointer to the native function

final Pointer<NativeFunction<create_ptm>> getPtmIntPointer =
    dylib.lookup<NativeFunction<create_ptm>>('create_ptm');
Copy the code

Dart uses generics to represent the type to which a pointer points

Int *; / / Pointer<Int32> corresponds to int *

Use the pointer above to initialize functions that can be called by DART

The binding process

final CreatePtm createPtm = getPtmIntPointer.asFunction<CreatePtm>();
Copy the code

Call to create

final int currentPtm = createPtm(300.300);
Copy the code

When this line of code is executed, an additional file is immediately created in the /dev/pts-/ directory of the corresponding device, so this also checks if the function was successfully called.

300,300 is the width and height of the terminal simulator, a value written randomly. Its value will affect the position of the terminal newline, which has not been studied. Again, I have no control over the timing of font newlines.

So you go to the terminal and you’ve created a pair

1.2 Execute subroutines on acquired terminal pairs

As you can see, this function requires a lot of parameters, so the corresponding DART code is complex

But the overall pattern for this part is the same

Corresponding to the statement

typedef create_subprocess = Void Function(
    Pointer<Utf8> env,
    Pointer<Utf8> cmd,
    Pointer<Utf8> cwd,
    Pointer<Pointer<Utf8>> argv,
    Pointer<Pointer<Utf8>> envp,
    Pointer<Int32> pProcessId,
    Int32 ptmfd);
typedef CreateSubprocess = void Function(
    Pointer<Utf8> env,
    Pointer<Utf8> cmd,
    Pointer<Utf8> cwd,
    Pointer<Pointer<Utf8>> argv,
    Pointer<Pointer<Utf8>> envp,
    Pointer<Int32> pProcessId,
    int ptmfd);
Copy the code

Complete code (with detailed comments)

    // Find the native pointer to the create_subprocess function in the current terminal
    final Pointer<NativeFunction<create_subprocess>> createSubprocessPointer =
        dylib.lookup<NativeFunction<create_subprocess>>('create_subprocess');

    /// Convert the pointer above to a method that DART can execute
    final CreateSubprocess createSubprocess =
        createSubprocessPointer.asFunction<CreateSubprocess>();
    // Create a secondary pointer to a native char and request a space of one byte
    final Pointer<Pointer<Utf8>> argv = allocate(count: 1);

    /// Sets the first level pointer of a double pointer to null
    /// Is equivalent to
    ///    char **p = (char **)malloc(1);
    ///    p[1] = 0;    p[1] = NULL;    *p = 0; *p = NULL;
    /// All four statements on the previous line are equivalent
    /// The reason for assigning the first pointer to null is that the C side of the loop iterates over argv by determining whether the current pointer is null as an exit condition
    argv[0] = Pointer<Utf8>.fromAddress(0);

    /// Define a two-level pointer to store the current terminal environment information. This two-level pointer corresponds to a two-dimensional array in C language
    Pointer<Pointer<Utf8>> envp;

    ///
    final Map<String.String> environment = <String.String> {}; environment.addAll(Platform.environment);/// Add the bin directory of the current App to the environment variable
    environment['PATH'] =
        '${EnvirPath.filesPath}/usr/bin:' + environment['PATH'];

    /// Allocates memory for the number of column elements plus one, and the last space is used to set the null pointer to allow the native loop to exit
    envp = allocate(count: environment.keys.length + 1);

    /// Copy the Map contents into a two-dimensional array
    for (int i = 0; i < environment.keys.length; i++) {
      envp[i] = Utf8.toUtf8(
          '${environment.keys.elementAt(i)}=${environment[environment.keys.elementAt(i)]}');
    }

    /// The last element is assigned a null pointer
    envp[environment.keys.length] = Pointer<Utf8>.fromAddress(0);

    /// Define a pointer to int
    /// Pointer is a common method in C. Pointers are passed in both directions and can be changed directly by the function called
    final Pointer<Int32> processId = allocate();

    /// Initialize to zero
    processId.value = 0;

    /// ShPath indicates the path of C Native programs
    /// Depending on the nature of the terminal, this command is usually sh or bash or some similar program
    /// And generally takes no arguments, so argv above is null
    String shPath;

    /// Even on Android devices, sh can be found in environment variables
    /// The sh linked by BusyBox may exist in the App's data directory, which is different from the sh provided by the system
    /// If the sh command is executed directly, the sh command of the data directory will be executed first. Therefore, /system/bin/sh is specified
    if (Platform.isAndroid)
      shPath = '/system/bin/sh';
    else
      shPath = 'sh';
    createSubprocess(
      Utf8.toUtf8(' '),
      Utf8.toUtf8(shPath),
      Utf8.toUtf8(
          Platform.isAndroid ? '/data/data/com.nightmare/files/home' : '. '),
      argv,
      envp,
      processId,
      currentPtm,
    );
    term.pid = processId.value;
    terms.add(term);
    print(processId.value);

    /// Remember to release dynamically applied space
    free(argv);
    free(envp);
    free(processId);
Copy the code

I encapsulate all of this in the NitermController class

NitermController code

NitermController class

A Term UI page corresponds to a controller, and when the controller is created, the current terminal is created.

The addListener function is used by the UI to bind the terminal to get the output

2.Dart implementation of terminal input and output

Rather than implementing the input and output of a terminal, it is better to understand the operation of a pair of file descriptors

2.1 Interaction with C Native

Let’s look at the function definition

typedef get_output_from_fd = Pointer<Uint8> Function(Int32);
typedef GetOutFromFd = Pointer<Uint8> Function(int);

typedef write_to_fd = Void Function(Int32, Pointer<Utf8>);
typedef WriteToFd = void Function(int, Pointer<Utf8>);
Copy the code

These two pairs of functions are from the previous article, but will not be explained much

2.2 Defining a FileDescriptor class

  • To initialize a FileDescriptor object we only need an int, and on the Dart side we also need a DynamicLibrary instance. It can also be recreated. Since this class is currently only used by NitermController, we use the DynamicLibrary instance of NitermController.
  • A FileDescriptor is bound to a FD, providing write and read functions externally.

The complete code

Complete code for FileDescriptor

3. Preparation of three common terminal sequences

A terminal control sequence is when a terminal gives you a particular output, it doesn’t want those characters to be printed on the screen, it does something specific.

3.1 Define terminal sequence constant classes

// This is the class for terminal control sequence
// This is the class for terminal control sequence
class TermControlSequences {
  // The terminal output sequence when the delete key is pressed
  static const List<int> deleteChar = <int> [8.32.8];
  // Resets the terminal sequence
  static const List<int> reset_term = <int> [27.99.27.40.66.27.91.109.27.91.74.27.91.63.50.53.104,];// The buzzer sequence
  static const List<int> buzzing = <int> [7];
}
Copy the code

The above sequence is only the sequence without affecting the normal operation of my current project, there are many more to be rewritten.

3.2 Control the output content

The contents of a particular sequence do not need to be output, and I put all this in the addListener function of the NitermController.

3.2.1 Deletion sequence of terminals

When delete is pressed, the terminal outputs [8,32,8]

As we know from the previous article, the Dart side also obtains the output from the PTM side of the terminal through an endless loop, and then splice the obtained output into the historical output.

Each output containing all pairs of [8,32,8] needs to be deleted and the number of pairs is recorded to delete the screen output.

The relevant code

final int deleteNum = RegExp(utf8.decode(TermControlSequences.deleteChar))
      .allMatches(result)
      .length;
    if (deleteNum > 0) {
    print('= = = = = >$deleteNumSequence of deleted characters');
    result = result.replaceAll(RegExp(utf8.decode(TermControlSequences.deleteChar)), ' ');
    termOutput = termOutput.substring(0, termOutput.length - deleteNum);
}
Copy the code

Where result is the output obtained once, termOutput is the output of the entire terminal

3.2.2 Terminal reset sequence

After entering the reset command, the terminal outputs [27, 99, 27, 40, 66, 27, 91, 109, 27, 91, 74, 27, 91, 63, 50, 53, 104,] to the screen.

When this sequence is included in a single output, the screen is immediately empty, but everything else that follows the sequence continues to output

The relevant code

  final bool hasRest =
  result.contains(utf8.decode(TermControlSequences.reset_term));
  print('hasRest====>$hasRest');
  if (hasRest) {
      termOutput = ' ';
      result =
      result.replaceAll(utf8.decode(TermControlSequences.reset_term), ' ');
  }
Copy the code

The trouble is that this set of sequences cannot be looked up from the output of a particular sequence using RegExp, and the encoding fails.

3.2.3 Buzzer of terminal

In some cases the terminal will beep to alert the user

For example, when the current user input has been deleted, we repeatedly press the delete key, the terminal will output character \ B, if this character is displayed on the screen, there will be a small space, which is certainly not what we want.

When the terminal outputs the sequence [7], then [7] is the whole sequence of a certain time

The relevant code

if (result == utf8.decode(TermControlSequences.buzzing)) {
    // When there is no content to delete, '\b' will be output and the terminal will beep to prompt the user
    print('=====> Beep ');
    continue;
}
Copy the code

4. UI of the Flutter terminal

4.1 Widget selection

Terminals are not simply black and white

When you type the following command

echo -e "\\033[1;34m Nightmare \\033[0m"
Copy the code

It will be a blue font that appears purple on the MAC.

So you need a RichText. Since the terminal is a sliding list, the top component of RichText is the ListView, and we need to control the ListView to slide to the bottom as the output arrives.

4.2 Theme Modification

Just for the background color, I adapted three themes for this terminal, namely Manjaro, Termux and MacOS.

See source code for details

Change the topic

Give a specified parameter when constructing the NitermController.

NitermController(
  theme: NitermThemes.manjaro,
)
Copy the code

4.3 Obtaining User Input

Since RichText is selected for the entire page, can we use WidgetSpan to add a text input box at the end of the screen output?

After repeated attempts, I found that this was not friendly.

So we use a ListView to contain the above Widget and a text entry box.

This is what it looks like:

We then set all the colors of the TextField to transparent

4.3.1 Identification of the CTRL key

It can be seen from the above pictures that I actually added the following 4 buttons. Finally, after repeated attempts, it was found that the standard terminal did not input its original corresponding character after pressing the CTRL key, but the ASCII-64 corresponding to the current character

4.3.2 Determine whether to input or delete

To compatible with terminal control light target, after I use editingController selection. The end and preservation of input to determine position

If the current cursor position is larger than before, simply enter the character of the current cursor into the terminal.

Instead, we type the terminal with an ASCII value of 127, representing deletion.

4.3.3 Enter, delete, CTRL key identification code

if (editingController.selection.end > textSelectionOffset) {
    currentInput = strCall[editingController.selection.end - 1];
    if (isUseCtrl) {
        nitermController.write(String.fromCharCode(
        currentInput.toUpperCase().codeUnits[0] - 64));
        isUseCtrl = false;
        setState(() {});
    } else{ nitermController.write(currentInput); }}else {
    nitermController.write(String.fromCharCode(127));
}
Copy the code

4.4 Generating rich text components

It’s actually technically part of the rewrite of the terminal sequence, but it directly affects the UI display, so it’s moved over here.

In order to achieve a complete separation of business logic and UI, we still hand it over to the NitermController

We need to achieve the following

His principle and

echo -e "\\033[1;34m Nightmare \\033[0m"
Copy the code

Is the same

This part of the test is my algorithm, this part of the code can be said to be terrible.

When we don’t write this part of the sequence

So that’s the algorithm

  • Split the entire string according to ‘\033[‘. The corresponding unitsCode is [27, 91].

\033 is the base 8 of ESC

  • Set TextSpan for this part of the output based on the value of the first element

This part of the code is too long, see the NitermController buildTextSpan function for details

Look at what I’ve rewritten so far

When I perform

echo -e "\033[0;30m ------Nightmare------ \033[0m"/ * * /echo -e "\033[0;37m ------Nightmare------ \033[0m"/ * * /echo -e "\033[1;30m ------Nightmare------ \033[0m"/ * * /echo -e "\033[1;37m ------Nightmare------ \033[0m"
echo -e "\033[4;30m ------Nightmare------ \033[0m"/ * * /echo -e "\033[4;37m ------Nightmare------ \033[0m"
echo -e "\033[7;30m ------Nightmare------ \033[0m"/ * * /echo -e "\033[7;37m ------Nightmare------ \033[0m"

Copy the code

preview

That is support

  • Color display
  • Color highlight
  • Font underline
  • Color reversal

5. Manage and create multiple terminals

Let’s use the savory Provider and first observe Termux’s multi-terminal processing.

You can see that the screen content of each terminal is preserved. So the data that we need to share in our state is the NitermController,

5.1 define ChangeNotifier

class NitermNotifier extends ChangeNotifier {
  final List<NitermController> _controlls = <NitermController>[
    NitermController(),
  ];
  List<NitermController> get controlls => _controlls;
  voidaddNewTerm() { _controlls.add(NitermController()); notifyListeners(); }}Copy the code

A state is created with a terminal by default.

5.2 Using Status Management

code

class NitermParent extends StatefulWidget {
  @override
  _NitermParentState createState() => _NitermParentState();
}

class _NitermParentState extends State<NitermParent> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<NitermNotifier>(
      create: (_) => NitermNotifier(),
      child: Builder(
        builder: (BuildContext c) {
          final NitermNotifier nitermNotifier = Provider.of<NitermNotifier>(c);
          return Stack(
            children: <Widget>[
              PageView.builder(
                itemCount: nitermNotifier.controlls.length,
                itemBuilder: (BuildContext c, int i) {
                  returnNiterm( nitermController: nitermNotifier.controlls[i], ); },),,); },),); }}Copy the code

Final preview

The code for this section is in Example

To this extremely crude implementation of the Flutter terminal simulator. To be continuously optimized.

6. Terminal integration extension 😑

In the very low probability that you need to integrate this terminal emulator, for example if you want to develop a VERSION of VS Code for Flutter?

6.1 Direct Use

There are installation packages or executables for Android/Linux/MAC under prebuilt_app

6.2 the sample

Under Example is a simple example of a multi-terminal that runs directly on an Android device.

6.3 Integration of existing projects

6.3.1 Adding a Dependency

flutter_terminal:
    git:
      url: git://github.com/Nightmare-MY/flutter_terminal.git
Copy the code

6.3.2 Adding the SO library

I have not yet been able to make this package directly integrated into the project, so you need to copy the platform’s dynamic library under prebuilt_so to where the application can get it.

For android projects, put libterm.so of the corresponding device in the Libs folder of android terminal

6.3.4 import packages

import 'package:flutter_terminal/flutter_terminal.dart';
Copy the code

6.3.5 Changing the SO library Path

Integration into Android doesn’t need to change, just add the so library

Nitermcontroller. libPath=' where you put so 'Copy the code

Put it where the current project can get it

Attention!!

  • At present, this package is still in the testing stage, and there are still a lot of print output in it. Please do not integrate the project officially launched.

Extended function

I add an asynchronous function for Controll as follows

  Future<void> defineTermFunc(String func) async {
    print('Define the function... ');
    String cache = ' ';
    addListener((String output) {
      cache = output;
      print('output=====>$output');
    });
    print('Create temporary scripts... ');
    await File('${EnvirPath.binPath}/tmp_func').writeAsString(func);
    write(
        "source ${EnvirPath.binPath}/tmp_func\nrm -rf ${EnvirPath.binPath}/tmp_func\necho 'define_func_finish'\n");
    while(! cache.contains('define_func_finish')) {
      await Future<void>.delayed(const Duration(milliseconds: 100));
    }
    termOutput = ' ';
    removeListener();
  }
Copy the code

If you need the terminal to perform a lot of automated code for you, but you don’t want that part of the code to be visible to the user. Take advantage of the shell’s functional programming.

Such as:

String func= ''' function CustomFunc(){ echo *** } '''
NitermController controller = NitermController();
await controller.defineTermFunc(func);

/ / pseudo code
// push ---->
Niterm(
    controller: controller,
    script: 'CustomFunc',),Copy the code

7. Preview effects 🧐

The Android platform

MAC

Yes, this is not a built-in terminal, there is a DEBUG in the upper right corner

Linux platform

Left for the terminal, the display effect is also very problematic, the font is garbled.

8. How to compile terminal so library 🤔

In the open source outer layer there is a Niterm folder, which is the C Native source we used.

Niterm folder

MAC/Linux platform

compile

Use the outer CMakeFileList configuration

mkdir build
cd build
camke ..
make
Copy the code

Finally, find the appropriate so library in the build directory.

Change the configuration

android

Cross-compile using the compilation script that comes with the folder.

mac

Due to sandbox permissions on the MAC side, the terminal can’t access any other path, so you need to go to Xcode to enable permission access and put the dylib file in a place where the terminal can read it. Then change the path to the default MAC dynamic library in NitermController.

Linux

After compiling the so library, create a lib directory at the level of your executable and make sure the name of the so library is libterm.so. View the Controller code.

Windows

🤣 died.

What if we could find a port for dup2 in Windows and construct a virtual PTMX feature file? However, there is too little information. According to the performance of VS Code and others in WIN terminal, it is definitely feasible, but there is no specific implementation reference.

conclusion

  • Everything is in the code 😑.

Please let me know if you find any junk code

  • There are few references to the code for both scrCPy and the terminal parts of this article, so I can’t even remember how much time it took me.
  • It took several days to write this article with the optimized code, and your thumbs up is a sign of support.
  • Leave any questions in the comments section and I will try my best to solve your problems.

Address — — — — > flutter_terminal

The last open source library was later integrated with something new, and this one stands alone.