Need help with vue-command?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

ndabAP
140 Stars 33 Forks MIT License 107 Commits 19 Opened issues

Description

A fully working, most feature-rich Vue.js terminal emulator

Services available

!
?

Need anything else?

Contributors list

# 70,672
vuejs2
vue2
sails
HTML
8 commits
# 1,606
ugui
graysca...
fish-sh...
cypress
2 commits
# 2,933
query-l...
python-...
mvvm-fr...
wechat-...
0 commits

vue-command

A fully working, most feature-rich Vue.js terminal emulator. See the demo and check the demo source code.

Features

  • Parses arguments with
    getopts
  • Supports asynchronous commands
  • Browse history (with /)
  • Autocompletion resolver (with )
  • Customize terminal with slots
  • Search history (with Ctrl + r)

Installation

$ npm install vue-command --save

Usage

Let's start with a very simple example. We want to send "Hello world" to

Stdout
when entering
hello-world
.

Now a more complex one. Let's assume we want to build the Nano editor available in many shells.

We will use the provided

environment
variable to make sure the editor is only visible when this command is executing and inject a function called
terminate
to tell the terminal that the command has been finished when the user enters Ctrl + x. Furthermore, we inject the
setIsFullscreen
function to switch the terminal into fullscreen mode.

Now the command has to return the component.

Properties

There are two types of commands: Built-in and regular ones. In most cases regular commands are appropriate. Built-in commands provide higher flexibility, see section Built-in for more information.

Some properties can be changed by the terminal, therefore, the

sync
modifier has to be added.

| Property | Type | Default | Sync | Description | |---------------------------|------------|-----------------------------------------------------------------------------------|------|----------------------------------------------------------------------------------------------| |

autocompletion-resolver
|
Function
|
null
| No | See Autocompletion resolver | |
built-in
|
Object
|
{}
| No | See Built-in section | |
commands
|
Object
|
{}
| No | See Commands section | |
cursor
|
Number
|
0
| Yes | Sets the
Stdin
cursor position | |
event-listeners
|
Array
|
[EVENT_LISTENERS.autocomplete, EVENT_LISTENERS.history, EVENT_LISTENERS.search]
| No | See Event listeners section | |
executed
|
Set
|
new Set()
| Yes | Executed programs, see "Overwriting
executed
functions"
| |
help-text
|
String
|
Type help
| No | Sets the placeholder | |
help-timeout
|
Number
|
4000
| No | Sets the placeholder timeout | |
hide-bar
|
Boolean
|
false
| No | Hides the bar | |
hide-prompt
|
Boolean
|
false
| No | Hides the prompt | |
hide-title
|
Boolean
|
false
| No | Hides the title | |
history
|
Array
|
[]
| Yes | Executed commands | |
intro
|
String
|
Fasten your seatbelts!
| No | Sets the intro | |
is-fullscreen
|
Boolean
|
false
| Yes | Sets the terminal fullscreen mode | |
is-in-progress
|
Boolean
|
false
| Yes | Sets the terminal progress status | |
not-found
|
String
|
not found
| No | Sets the command not found text | |
parser-options
|
Object
|
{}
| No | Sets the parser options | |
pointer
|
Number
|
0
| Yes | Sets the command pointer | |
prompt
|
String
|
[email protected]:#
| No | Sets the prompt | |
show-help
|
Boolean
|
false
| No | Shows the placeholder | |
show-intro
|
Boolean
|
false
| No | Shows the intro | |
stdin
|
String
|
''
| Yes | Sets the current
Stdin
| |
title
|
String
|
[email protected]: ~
| No | Sets the title |

Commands

commands
must be an object containing key-value pairs where key is the command and the value is a function that will be called with the
getops
arguments
. The function can return a
Promise
and must return or resolve a Vue.js component. To return strings or nothing use one of the convenient helper methods:

| Function | Description | |----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| |

createStdout(content: String, isInnerText: Boolean, isEscapeHtml: Boolean, name: String, ...mixins: Array): Object
| Returns a
Stdout
component containing a span element with given inner content | |
createStderr(content: String, isEscapeHtml: Boolean, name: String, ...mixins: Array): Object
| Returns a
Stderr
component containing a span element with given inner content | |
createDummyStdout(name: String, ...mixins: Array): Object
| Returns a dummy
Stdout
to show a
Stdin
|

Helper methods can be imported by name:

import { createStdout, createStderr, createDummyStdout } from 'vue-command'

If none of the helper methods is used, the command has to be manually terminated inside the component. Next to termination it's possible to inject the following functions to manipulate the terminal or signal an event:

| Function | Description | |------------------------------------------|-------------------------------------------------------------| |

emitExecute
| Emit command execution event | |
emitExecuted
| Emit command executed event | |
emitInput(input: String)
| Emit the current input | |
setCursor(cursor: Number)
| Set cursor position | |
setIsFullscreen(isFullscreen: Boolean)
| Change if the terminal is in fullscreen mode | |
setIsInProgress(isInProgress: Boolean)
| Change if the terminal is in progress | |
setPointer(pointer: Number)
| Set command history pointer | |
setStdin(stdin: String)
| Set the current
Stdin
| |
terminate
| Executes common final tasks after command has been finished |

Functions can be injected into your component by name:

inject: ['setIsFullscreen', 'setIsInProgress', 'terminate']

In your component you have access to a

context
and an
environment
variable. The
environment
variable contains the following properties (note that built-in commands have to take care by theirselves about the terminals state):

| Property | Description | |-------------------------|----------------------------------------------------| |

isExecuting: Boolean
| Is the current component executing | |
isFullscreen: Boolean
| Is the terminal in fullscreen mode | |
isInProgress: Boolean
| Is any command active |

The

context
variable contains the following properties:

| Property | Description | |------------------|------------------------------------| |

cursor: Number
| Copy of cursor position at
Stdin
| |
executed: Set
| Copy of executed programs | |
history: Array
| Copy of executed commands | |
parsed: Object
| Parsed
getops
arguments | |
pointer: Number
| Copy of history command pointer | |
stdin: String
| Copy of
Stdin
|

Built-in

Built-in commands provide more control over the terminals behaviour. On the other side, they have to take care about every regular command step. As a matter of fact, regular commands are just calling helper methods or change properties which could be also called or changed by built-in commands. Regular commands can be seen as a facade to built-in commands.

Since built-in commands can capture any command, it's necessary to take care of autocompletion and the command not found experience.

The first argument that is called within the built-in command is the unparsed

Stdin
. It's possible to use a custom parser at this place. The second argument is the terminal instance. You can use the
commandNotFound
method if no built-in or regular command has been found.

To fully simulate a regular command circle a built-in command has to follow these steps:

  1. Call
    setIsInProgress
    with
    true
    to tell there is a command in progress
  2. Add the programm to the
    executed
    Set
    property
  3. Increase the history pointer with
    setPointer
  4. Execute actual task
  5. Push the
    Stdout
    component into the
    history
    property
  6. Call
    setIsInProgress
    with
    false
    to tell there is no command in progress anymore

Autocompletion resolver

It is possible to provide a function that is called when the user hits the key. This function needs to take care of the autocompletion experience and should make usage of properties like

history
and
stdin
. The following shows a possible, simple autocompletion function:
this.autocompletionResolver = () => {
  // Make sure only programs are autocompleted. See below for version with options
  const command = this.stdin.split(' ')
  if (command.length > 1) {
    return
  }

const autocompleteableProgram = command[0] // Collect all autocompletion candidates let candidates = [] const programs = [...Object.keys(this.commands)].sort() programs.forEach(program => { if (program.startsWith(autocompleteableProgram)) { candidates.push(program) } })

// Autocompletion resolved into multiple results if (this.stdin !== '' && candidates.length > 1) { this.history.push({ // Build table programmatically render: createElement => { const columns = candidates.length < 5 ? candidates.length : 4 const rows = candidates.length < 5 ? 1 : Math.ceil(candidates.length / columns)

    let index = 0
    let table = []
    for (let i = 0; i &lt; rows; i++) {
      let row = []
      for (let j = 0; j &lt; columns; j++) {
        row.push(createElement('td', candidates[index]))
        index++
      }

      table.push(createElement('tr', [row]))
    }

    return createElement('table', { style: { width: '100%' } }, [table])
  }
})

}

// Autocompletion resolved into one result if (candidates.length === 1) { this.stdin = candidates[0] } }

Advanced version with option autocompletion
this.autocompletionResolver = () =&gt; {
  // Preserve cursor position
  const cursor = this.cursor

  // Reverse concatenate autocompletable according to cursor
  let pointer = this.cursor
  let autocompleteableStdin = ''
  while (this.stdin[pointer - 1] !== ' ' &amp;&amp; pointer - 1 &gt; 0) {
    pointer--
    autocompleteableStdin = `${this.stdin[pointer]}${autocompleteableStdin}`
  }

  // Divide by arguments
  const command = this.stdin.split(' ')

  // Autocompleteable is program
  if (command.length === 1) {
    const autocompleteableProgram = command[0]
    // Collect all autocompletion candidates
    const candidates = []
    const programs = [...Object.keys(this.commands)].sort()
    programs.forEach(program =&gt; {
      if (program.startsWith(autocompleteableProgram)) {
        candidates.push(program)
      }
    })

    // Autocompletion resolved into multiple results
    if (this.stdin !== '' &amp;&amp; candidates.length &gt; 1) {
      this.history.push({
        // Build table programmatically
        render: createElement =&gt; {
          const columns = candidates.length &lt; 5 ? candidates.length : 4
          const rows = candidates.length &lt; 5 ? 1 : Math.ceil(candidates.length / columns)

          let index = 0
          const table = []
          for (let i = 0; i &lt; rows; i++) {
            const row = []
            for (let j = 0; j &lt; columns; j++) {
              row.push(createElement('td', candidates[index]))
              index++
            }

            table.push(createElement('tr', [row]))
          }

          return createElement('table', { style: { width: '100%' } }, [table])
        }
      })
    }

    // Autocompletion resolved into one result
    if (candidates.length === 1) {
      // Mutating Stdin mutates the cursor, so we've to wait to push it to the end
      const unwatch = this.$watch(() =&gt; this.cursor, () =&gt; {
        this.cursor = cursor + (candidates[0].length - autocompleteableStdin.length + 0)

        unwatch()
      })

      this.stdin = candidates[0]
    }

    return
  }

  // Check if option might be completed already or option is last tokens
  if ((this.stdin[cursor] !== '' &amp;&amp; this.stdin[cursor] !== ' ') &amp;&amp; typeof this.stdin[cursor] !== 'undefined') {
    return
  }

  // Get the executable
  const program = command[0]

  // Check if any autocompleteable exists
  if (typeof this.options.long[program] === 'undefined' &amp;&amp; typeof this.options.short[program] === 'undefined') {
    return
  }

  // Autocompleteable is long option
  if (autocompleteableStdin.substring(0, 2) === '--') {
    const candidates = []
    this.options.long[program].forEach(option =&gt; {
      // If only dashes are present, user requests all options
      if (`--${option}`.startsWith(autocompleteableStdin) || autocompleteableStdin === '--') {
        candidates.push(option)
      }
    })

    // Autocompletion resolved into one result
    if (candidates.length === 1) {
      const autocompleted = `${this.stdin.substring(0, pointer - 1)} --${candidates[0]}`
      const rest = `${this.stdin.substring(this.cursor)}`

      // Mutating Stdin mutates the cursor, so we've to wait to push it to the end
      const unwatch = this.$watch(() =&gt; this.cursor, () =&gt; {
        this.cursor = cursor + (candidates[0].length - autocompleteableStdin.length + 2)

        unwatch()
      })

      this.stdin = `${autocompleted}${rest}`

      return
    }

    // Autocompletion resolved into multiple result
    if (autocompleteableStdin === '--' || candidates.length &gt; 1) {
      this.history.push({
        // Build table programmatically
        render: createElement =&gt; {
          const columns = candidates.length &lt; 5 ? candidates.length : 4
          const rows = candidates.length &lt; 5 ? 1 : Math.ceil(candidates.length / columns)

          let index = 0
          const table = []
          for (let i = 0; i &lt; rows; i++) {
            const row = []
            for (let j = 0; j &lt; columns; j++) {
              row.push(createElement('td', `--${candidates[index]}`))
              index++
            }

            table.push(createElement('tr', [row]))
          }

          return createElement('table', { style: { width: '100%' } }, [table])
        }
      })
    }

    return
  }

  // Autocompleteable is option
  if (autocompleteableStdin.substring(0, 1) === '-') {
    const candidates = []
    this.options.short[program].forEach(option =&gt; {
      // If only one dash is present, user requests all options
      if (`-${option}`.startsWith(autocompleteableStdin) || autocompleteableStdin === '-') {
        candidates.push(option)
      }
    })

    // Autocompletion resolved into one result
    if (candidates.length === 1) {
      const autocompleted = `${this.stdin.substring(0, pointer - 1)} -${candidates[0]}`
      const rest = `${this.stdin.substring(this.cursor)}`

      // Mutating Stdin mutates the cursor, so we've to wait to push it to the end
      const unwatch = this.$watch(() =&gt; this.cursor, () =&gt; {
        this.cursor = cursor + (candidates[0].length - autocompleteableStdin.length + 1)

        unwatch()
      })

      this.stdin = `${autocompleted}${rest}`

      return
    }

    // Autocompletion resolved into multiple result
    if (autocompleteableStdin === '-' || candidates.length &gt; 1) {
      this.history.push({
        // Build table programmatically
        render: createElement =&gt; {
          const columns = candidates.length &lt; 5 ? candidates.length : 4
          const rows = candidates.length &lt; 5 ? 1 : Math.ceil(candidates.length / columns)

          let index = 0
          const table = []
          for (let i = 0; i &lt; rows; i++) {
            const row = []
            for (let j = 0; j &lt; columns; j++) {
              row.push(createElement('td', `-${candidates[index]}`))
              index++
            }

            table.push(createElement('tr', [row]))
          }

          return createElement('table', { style: { width: '100%' } }, [table])
        }
      })
    }
  }
}

Event listeners

Event listeners trigger terminal behaviour under certain conditions like pressing a button. Pass an array of event listeners you want to bind via the

event-listeners
property. This library provides three event listeners per default which can be imported:
  • Autocompletion: Autocompletion when pressing
  • History: Cycle through history with /
  • Search: Search history with Ctrl and r

An event listener is called with the Vue.js component instance as argument.

Slots

Bar

It's possible to fully customize the terminal bar using slots as shown in the following. Note: If using the bar slot, the properties

hide-bar
and
title
will be ignored.

Prompt

Customize the prompt with the

prompt
slot. Note: If using the prompt slot, the property
prompt
will be ignored and the CSS class
term-ps
has to be manually applied.

Events

| Event | Type | Description | Note |-----------|-------------|-----------------------------------|-----------------------------------------------------| |

input
|
String
| Emits the current input | | |
execute
|
String
| Emits when executing command | Built-in commands have to manually emit this event | |
executed
|
String
| Emits after command execution | Built-in commands have to manually emit this event. All helper methods emit this event |

Browser support

This library uses the

ResizeObserver
to track if the terminals inner height changes. You may need a pollyfill to support your target browser.

Overwriting
executed
functions

To track when the

executed
property has been mutated, this library overwrites the functions
add
,
clear
and
delete
of it. That means if you plan to overwrite the named
Set
functions by yourself, this library won't work.

Projects using vue-command

Chuck Norris API

The Chuck Norris jokes are comming from this API. This library has no relation to Chuck Norris or the services provided by the API.

Author

Julian Claus and contributors. Special thanks to krmax44 for the amazing work!

I apologize to some contributors that are not in the Git history anymore since I had to delete the repository because of problems with semantic-release.

License

MIT

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.