preface

Electron is well known, and many of you may know that it is used to develop desktop applications, but it has not been implemented in the project, and there is a lack of practice projects.

Many open source command line terminals are developed using Electron. This article will teach you how to write a command line terminal using Electron from scratch.

As a complete field project example, the terminal demo will also be integrated into the Electron open source learning project, Electron-Playground, which currently has 800+ Star⭐️ and its biggest feature is the wySIWYG demonstration of various features of Electron, Help you learn Electron quickly.

Let’s try Electron

Download the demo

The command line terminal demo in this article has a small amount of code, only three files in total, and the annotations are detailed enough. It is recommended to experience the details of a project operation after reading it.

Project presentations

Clear command demonstration

This essentially resets the array of the history command line output to an empty array.

Failed to perform arrow switch

According to the child process close event, to determine whether the execution is successful, switch the icon.

The CD command

Identify the CD command, add the command to obtain the path (PWD /chdir), and then change the obtained path to the final path.

Giit submit code demo

The project address

Open source address: electron-terminal-demo

Start and debug

The installation

npm install
Copy the code

Start the

  1. Run projects through vscode debugging, which allows direct debugger debugging in vscode.

  2. If you are not using the vscode editor, you can also start using the command line.

npm run start
Copy the code

Three files implement a command line terminal

directory

  1. Initialize the project.
  2. Project directory structure
  3. Electron startup entry Index – Create window
  4. Process communication class -processMessage.
  5. Window HTML page – Command line panel
  6. What does the command line panel do
    • Core method: child_process.spawn- Executes the command line to listen for output from the command line
    • Stderr is not directly recognized as a command line execution error
    • Command line terminal to execute commands to save the output of the core code
    • HTML complete code
    • More details on the command line terminal
  7. Download the demo
    • Project presentations
    • The project address
    • Start and debug
  8. summary

Initialize the project

npm init
npm install electron -D
Copy the code

If the Electron installation fails, you need to add a. NPMRC file to change the installation address of Electron. The file content is as follows:

registry=https://registry.npm.taobao.org/
electron_mirror=https://npm.taobao.org/mirrors/electron/
chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver
Copy the code

Modify the main and scripts entry options for package.json, and now package.json looks like this, which is very succinct:

{
  "name": "electron-terminal"."version": "1.0.0"."main": "./src/index.js"."scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^ 11.1.1." "}}Copy the code

Project directory structure

Our final project will look like this, except for the page CSS file, we only need to implement three files under SRC.

. ├ ─ ─ vscode// start the project using vscode's debugging capabilities├── ├─ SRC │ ├─ index.js// Electron startup entry - create window│ └ ─ ─ intrinsic processMessage. Js// Master and renderer communication class - process communication, listening time│ └ ─ ─ index. HTML// Window HTML page - Command line panel, execute commands, and listen for output│ └ ─ ─ index. The CSS// Window HTML CSS style section is not written├ ─ ─ package. Json └ ─ ─ the NPMRC// Change the NPM installation package address└ ─ ─ gitignoreCopy the code

Electron startup entry Index – Create window

  1. Create Windows that give them the ability to use Node directly.
  2. Window loads the local HTML page
  3. Load the main thread and render communication logic
// ./src/index.js
const { app, BrowserWindow } = require('electron')
const processMessage = require('./processMessage')

// Create window
function createWindow() {
  // Create window
  const win = new BrowserWindow({
    width: 800.height: 600.webPreferences: {
      nodeIntegration: true.// The page directly uses node's ability to import node modules to execute commands}})// Load the local page
  win.loadFile('./src/index.html')
  win.webContents.openDevTools() // Open the console
  // The main thread communicates with the renderer
  const ProcessMessage = new processMessage(win)
  ProcessMessage.init()
}

// App Ready create window
app.whenReady().then(createWindow)
Copy the code

Process communication class -processMessage

Electron is divided into the main process and the renderer process. Because processes are different, they need to notify each other at the corresponding time of various events to perform some functions.

This class is used for communication between them, the electron communication part of the package is very simple, just follow it.

// ./src/processMessage.js
const { ipcMain } = require('electron')
class ProcessMessage {
  /** * Process communication *@param {*} Win creates Windows */
  constructor(win) {
    this.win = win
  }
  init() {
    this.watch()
    this.on()
  }
  // Listen for the renderer event communication
  watch() {
    // The page is ready
    ipcMain.on('page-ready'.() = > {
      this.sendFocus()
    })
  }
  // Monitor events of Windows, APP, and other modules
  on() {
    // Monitor whether the window is focused
    this.win.on('focus'.() = > {
      this.sendFocus(true)})this.win.on('blur'.() = > {
      this.sendFocus(false)})}/** * Window focus event send *@param {*} IsActive whether to focus */
  sendFocus(isActive) {
    // The main thread sends events to the window
    this.win.webContents.send('win-focus', isActive)
  }
}
module.exports = ProcessMessage
Copy the code

Window HTML page – Command line panel

When we created the window, we gave the window the ability to use Node directly in HTML.

So we don’t need to use process communication to execute commands and render output, we can do it directly in a file.

The core of a terminal is executing commands, rendering command line output, and saving command line output.

It’s all done in this file, less than 250 lines of code.

What does the command line panel do

  • Pages: Introduce vue, Element, CSS files to handle pages

  • Template template – Renders the output of the current command line execution and the output of the history command line execution

  • Core: Execute commands to listen for command line output

    • Execute commands and listen to the output of executing commands, rendering the output synchronously.
    • After the command is executed, save the command output.
    • Render history command line output.
    • Special handling of some commands, such as the details below.
  • Details around executing the command line

    • Identify the CD and save the CD path according to the system
    • Identify clear to clear all output.
    • Execution success and failure arrow ICONS are displayed.
    • Focus window, focus input.
    • After the command is executed, scroll to the bottom.
    • And so on.

Core method: child_process.spawn- Executes the command line to listen for output from the command line

Child_process. Spawn is introduced

Spawn is an asynchronous method provided by node child process module child_process.

It is used to execute commands and can listen to the output of command line execution in real time.

When I first learned about the API, I felt that this approach was tailor-made for command line terminals.

The core of the terminal is to execute the command line and output the information during the execution of the command line in real time.

Here’s how it’s used.

use

const { spawn } = require('child_process');
const ls = spawn('ls', {
  encoding: 'utf8'.cwd: process.cwd(), // Run the command path
  shell: true.// Run the shell command
})

// Listen for standard output
ls.stdout.on('data'.(data) = > {
  console.log(`stdout: ${data}`);
});

// Listen for standard error
ls.stderr.on('data'.(data) = > {
  console.error(`stderr: ${data}`);
});

// Child process closure event
ls.on('close'.(code) = > {
  console.log('Child process exits, exit code${code}`);
});
Copy the code

The API is simple to use, but the output of terminal information requires a lot of detail, such as this one.

Stderr is not directly recognized as a command line execution error

Stderr is standard error output, but the information in it is not all error information, different tools will have different processing.

For Git, there is a lot of command line output on Stederr.

For example, git clone, Git push, etc., the information output in Stederr, we can not regard it as an error.

Git always sends stederr detailed status information and progress reports, as well as read-only information.

See Git stderr Exploration for details.

It is not clear that other tools/command lines have similar operations, but it is clear that stederr’s message should not be regarded as an error message.

PS: If you want to provide better support for Git, you need to perform special processing according to different git commands, such as the following clear command and CD command special processing.

Check whether the command line is successfully executed according to the child process close event

We should detect the exit code for the close event, if code is 0 the command line is successful, otherwise it fails.

Command line terminal to execute commands to save the output of the core code

So here’s the core code for the command line panel, and I’m going to post it up for you to see,

The rest is code for details, experience optimization, state handling, etc. The full HTML will be posted below.

const { spawn } = require('child_process') // Use node child_process module
// Execute the command line
actionCommand() {
  // Process the command command
  const command = this.command.trim()
  this.isClear(command)
  if (this.command === ' ') return
  // Execute the command line
  this.action = true
  this.handleCommand = this.cdCommand(command)
  const ls = spawn(this.handleCommand, {
    encoding: 'utf8'.cwd: this.path, // Run the command path
    shell: true.// Run the shell command
  })
  // Listen for the output of the command line execution process
  ls.stdout.on('data'.(data) = > {
    const value = data.toString().trim()
    this.commandMsg.push(value)
    console.log(`stdout: ${value}`)
  })

  ls.stderr.on('data'.this.stderrMsgHandle)
  ls.on('close'.this.closeCommandAction)
},
Error or detailed status progress reports such as Git push
stderrMsgHandle(data) {
  console.log(`stderr: ${data}`)
  this.commandMsg.push(`stderr: ${data}`)},// Save the information and update the status
closeCommandAction(code) {
  // Save the execution information
  this.commandArr.push({
    code, // Check whether the command is executed successfully
    path: this.path, // Execution path
    command: this.command, // Execute the command
    commandMsg: this.commandMsg.join('\r'), // Execution information
  })
  / / to empty
  this.updatePath(this.handleCommand, code)
  this.commandFinish()
  console.log(
    'Child process exits, exit code${code}Run,${code === 0 ? 'success' : 'failure'}`)}Copy the code

HTML complete code

Here is the complete HTML code with detailed comments to suggest what is done in the command line panel above to read the source code.

<! DOCTYPEhtml>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>The simple electron terminal</title>
    <link
      rel="stylesheet"
      href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
    />
    <script src="https://unpkg.com/vue"></script>
    <! -- Introducing element -->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <! -- css -->
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <div id="app">
      <div class="main-class">
        <! Render past command line -->
        <div v-for="item in commandArr">
          <div class="command-action">
            <! -- Execute successful or failed icon switch -->
            <i
              :class="['el-icon-right', 'command-action-icon', { 'error-icon': item.code !== 0 }]"
            ></i>
            <! -- Past execution address and command line, information -->
            <span class="command-action-path">{{ item.path }} $</span>
            <span class="command-action-contenteditable"
              >{{ item.command }}</span
            >
          </div>
          <div class="output-command">{{ item.commandMsg }}</div>
        </div>
        <! -- Current input command line -->
        <div
          class="command-action command-action-editor"
          @mouseup="timeoutFocusInput"
        >
          <i class="el-icon-right command-action-icon"></i>
          <! -- Execute address -->
          <span class="command-action-path">{{ path }} $</span>
          <! -- Command line input -->
          <span
            :contenteditable="action ? false : 'plaintext-only'"
            class="command-action-contenteditable"
            @input="onDivInput($event)"
            @keydown="keyFn"
          ></span>
        </div>
        <! -- Current command line output -->
        <div class="output-command">
          <div v-for="item in commandMsg">{{item}}</div>
        </div>
      </div>
    </div>

    <script>
      const { ipcRenderer } = require('electron')
      const { spawn } = require('child_process')
      const path = require('path')

      var app = new Vue({
        el: '#app'.data: {
          path: ' '.// Command line directory
          command: ' '.// The user enters the command
          handleCommand: ' '.// Processed user commands such as clearing leading and trailing Spaces, adding commands to get paths
          commandMsg: [].// Current command information
          commandArr: [].// Save past command line output
          isActive: true.// Whether the terminal is focused
          action: false.// Whether the command is being executed
          inputDom: null.// Input box dom
          addPath: ' '.// The MAC command is PWD window command is chdir
        },
        mounted() {
          this.addGetPath()
          this.inputDom = document.querySelector(
            '.command-action-contenteditable'
          )
          this.path = process.cwd() // Initialize the path
          this.watchFocus()
          ipcRenderer.send('page-ready') // Tell the main process that the page is ready
        },
        methods: {
          // Enter to execute the command
          keyFn(e) {
            if (e.keyCode == 13) {
              this.actionCommand()
              e.preventDefault()
            }
          },
          // Execute the command
          actionCommand() {
            const command = this.command.trim()
            this.isClear(command)
            if (this.command === ' ') return
            this.action = true
            this.handleCommand = this.cdCommand(command)
            const ls = spawn(this.handleCommand, {
              encoding: 'utf8'.cwd: this.path, // Run the command path
              shell: true.// Run the shell command
            })
            // Listen for the output of the command line execution process
            ls.stdout.on('data'.(data) = > {
              const value = data.toString().trim()
              this.commandMsg.push(value)
              console.log(`stdout: ${value}`)})// Error or detailed status progress reports such as git push and git clone
            ls.stderr.on('data'.(data) = > {
              const value = data.toString().trim()
              this.commandMsg.push(`stderr: ${data}`)
              console.log(`stderr: ${data}`)})// The child process shutdown event saves information and updates the status
            ls.on('close'.this.closeCommandAction) 
          },
          // Save the information and update the status
          closeCommandAction(code) {
            // Save the execution information
            this.commandArr.push({
              code, // Check whether the command is executed successfully
              path: this.path, // Execution path
              command: this.command, // Execute the command
              commandMsg: this.commandMsg.join('\r'), // Execution information
            })
            / / to empty
            this.updatePath(this.handleCommand, code)
            this.commandFinish()
            console.log(
              'Child process exits, exit code${code}Run,${code === 0 ? 'success' : 'failure'}`)},// CD command processing
          cdCommand(command) {
            let pathCommand = ' '
            if (this.command.startsWith('cd ')) {
              pathCommand = this.addPath
            } else if (this.command.indexOf(' cd ')! = = -1) {
              pathCommand = this.addPath
            }
            return command + pathCommand
            // Directory automatic association... A lot of detail functions can be done but not necessary 2
          },
          // Clear the history
          isClear(command) {
            if (command === 'clear') {
              this.commandArr = []
              this.commandFinish()
            }
          },
          // Get the paths under different systems
          addGetPath() {
            const systemName = getOsInfo()
            if (systemName === 'Mac') {
              this.addPath = ' && pwd'
            } else if (systemName === 'Windows') {
              this.addPath = ' && chdir'}},// Reset parameters after the command is executed
          commandFinish() {
            this.commandMsg = []
            this.command = ' '
            this.inputDom.textContent = ' '
            this.action = false
            // Activate the editor
            this.$nextTick(() = > {
              this.focusInput()
              this.scrollBottom()
            })
          },
          // Check whether addPath is added to the command
          updatePath(command, code) {
            if(code ! = =0) return
            const isPathChange = command.indexOf(this.addPath) ! = = -1
            if (isPathChange) {
              this.path = this.commandMsg[this.commandMsg.length - 1]}},// Save the input command line
          onDivInput(e) {
            this.command = e.target.textContent
          },
          / / click on the div
          timeoutFocusInput() {
            setTimeout(() = > {
              this.focusInput()
            }, 200)},// Focus input
          focusInput() {
            this.inputDom.focus() // Resolve the problem that ff cannot locate without getting focus
            var range = window.getSelection() / / create the range
            range.selectAllChildren(this.inputDom) //range selects all children under obj
            range.collapseToEnd() // The cursor moves to the end
            this.inputDom.focus()
          },
          // Scroll to the bottom
          scrollBottom() {
            let dom = document.querySelector('#app')
            dom.scrollTop = dom.scrollHeight // Roll height
            dom = null
          },
          // Monitor window is in focus and out of focus
          watchFocus() {
            ipcRenderer.on('win-focus'.(event, message) = > {
              this.isActive = message
              if (message) {
                this.focusInput()
              }
            })
          },
        },
      })

      // Obtain operating system information
      function getOsInfo() {
        var userAgent = navigator.userAgent.toLowerCase()
        var name = 'Unknown'
        if (userAgent.indexOf('win') > -1) {
          name = 'Windows'
        } else if (userAgent.indexOf('iphone') > -1) {
          name = 'iPhone'
        } else if (userAgent.indexOf('mac') > -1) {
          name = 'Mac'
        } else if (
          userAgent.indexOf('x11') > -1 ||
          userAgent.indexOf('unix') > -1 ||
          userAgent.indexOf('sunname') > -1 ||
          userAgent.indexOf('bsd') > -1
        ) {
          name = 'Unix'
        } else if (userAgent.indexOf('linux') > -1) {
          if (userAgent.indexOf('android') > -1) {
            name = 'Android'
          } else {
            name = 'Linux'}}return name
      }
    </script>
  </body>
</html>
Copy the code

The above is the entire project code implementation, a total of only three files.

For more details

This project is ultimately a simple demo, but there are many details that need to be added if you want to make it into a full open source project.

There are also all sorts of weird requirements and customizations, such as the following:

  • command+cTerminate the command
  • cdAutomatic directory completion
  • Command to save up and down key sliding
  • Git and other common functions are treated separately.
  • The color of the output information changes
  • , etc.

summary

This is the implementation principle of the command line terminal. It is highly recommended that you download and experience this project. It is best to debug it in a single step, so that you will be more familiar with Electron.

Project IDEA was born out of another open source project of our team: electric-Playground, which aims to let friends learn the electron combat project.

The electron playground is used to help the front-end friends to better and faster learn and understand the front-end desktop technology electron and avoid detours as much as possible.

It allows us to learn electron very quickly in the following way.

  1. Tutorial article with GIF examples and a working demo.
  2. Systematical collation of Electron related APIS and functions.
  3. With a walkthrough, try out the electron’s features yourself.

Front-end advanced accumulation, public account, GitHub, WX :OBkoro1, email: [email protected]

The above 2021/01/12