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.