Giter VIP home page Giter VIP logo

local-echo's Introduction

๐Ÿ“ข local-echo Travis (.org) Try it in codepen.io

A fully functional local echo controller for xterm.js

You will be surprised how difficult it is to implement a fully functional local-echo controller for xterm.js (or any other terminal emulator). This project takes this burden off your hands.

Features

The local echo controller tries to replicate most of the bash-like user experience primitives, such as:

  • Arrow navigation: Use left and right arrows to navigate in your input
  • Word-boundary navigation: Use alt+left and alt+right to jump between words
  • Word-boundary deletion: Use alt+backspace to delete a word
  • Multi-line continuation: Break command to multiple lines if they contain incomplete quotation marks, boolean operators (&& or ||), pipe operator (|), or new-line escape sequence (\).
  • Full-navigation on multi-line command: You are not limited only on the line you are editing, you can navigate and edit all of your lines.
  • Local History: Just like bash, access the commands you previously typed using the up and down arrows.
  • Tab-Completion: Provides support for registering your own tab-completion callbacks.

Usage

As ES6 Module

  1. Install it using npm:

    npm install --save wavesoft/local-echo

    Or yarn:

    yarn add wavesoft/local-echo
  2. Use it like so:

    import { Terminal } from 'xterm';
    import LocalEchoController from 'local-echo';
    
    // Start an xterm.js instance
    const term = new Terminal();
    term.open(document.getElementById('terminal'));
    
    // Create a local echo controller (xterm.js v3)
    const localEcho = new LocalEchoController(term);
    // Create a local echo controller (xterm.js >=v4)
    const localEcho = new LocalEchoController();
    term.loadAddon(localEcho);
    
    // Read a single line from the user
    localEcho.read("~$ ")
        .then(input => alert(`User entered: ${input}`))
        .catch(error => alert(`Error reading: ${error}`));

Directly in the browser

  1. Download local-echo.js from the latest release

  2. Include it in your HTML:

    <script src="/js/local-echo.js"></script>
    
  3. Use it like so:

    // Start an xterm.js instance
    const term = new Terminal();
    term.open(document.getElementById('terminal'));
    
    // Create a local echo controller (xterm.js v3)
    const localEcho = new LocalEchoController(term);
    // Create a local echo controller (xterm.js >=v4)
    const localEcho = new LocalEchoController();
    term.loadAddon(localEcho);
    
    // Read a single line from the user
    localEcho.read("~$ ")
        .then(input => alert(`User entered: ${input}`))
        .catch(error => alert(`Error reading: ${error}`));

API Reference

constructor(term, [options])

The constructor accepts an xterm.js instance as the first argument and an object with possible options. The options can be:

{
    // The maximum number of entries to keep in history
    historySize: 10,
    // The maximum number of auto-complete entries, after which the user
    // will have to confirm before the entries are displayed.
    maxAutocompleteEntries: 100
}

.read(prompt, [continuationPrompt]) -> Promise

Reads a single line from the user, using local-echo. Returns a promise that will be resolved with the user input when completed.

localEcho.read("~$", "> ")
        .then(input => alert(`User entered: ${input}`))
        .catch(error => alert(`Error reading: ${error}`));

.readChar(prompt) -> Promise

Reads a single character from the user, without echoing anything. Returns a promise that will be resolved with the user input when completed.

This input can be active in parallel with a .read prompt. A character typed will be handled in priority by this function.

This is particularly helpful if you want to prompt the user for something amidst an input operation. For example, prompting to confirm an expansion of a large number of auto-complete candidates during tab completion.

localEcho.readChar("Display all 1000 possibilities? (y or n)")
        .then(yn => {
            if (yn === 'y' || yn === 'Y') {
                localEcho.print("lots of stuff!");
            }
        })
        .catch(error => alert(`Error reading: ${error}`));

.abortRead([reason])

Aborts a currently active .read. This function will reject the promise returned from .read, passing the reason as the rejection reason.

localEcho.read("~$", "> ")
        .then(input => {})
        .catch(error => alert(`Error reading: ${error}`));

localEcho.abortRead("aborted because the server responded");

.print([message])

.println([message])

Print a message (and change line) to the terminal. These functions are tailored for writing plain-text messages, performing the appropriate conversions.

For example all new-lines are normalized to \r\n, in order for them to appear correctly on the terminal.

.printWide(strings)

Prints an array of strings, occupying the full terminal width. For example:

localEcho.printWide(["first", "second", "third", "fourth", "fifth", "sixth"]);

Will display the following, according to the current width of your terminal:

first  second  third  fourth
fifth  sixth

.addAutocompleteHandler(callback, [args...])

Registers an auto-complete handler that will be used by the local-echo controller when the user hits TAB.

The callback has the following signature:

function (index: Number, tokens: Array[String], [args ...]): Array[String] 

Where:

  • index: represents the current token in the user command that an auto-complete is requested for.
  • tokens : an array with all the tokens in the user command
  • args... : one or more arguments, as given when the callback was registered.

The function should return an array of possible auto-complete expressions for the current state of the user input.

For example:

// Auto-completes common commands
function autocompleteCommonCommands(index, tokens) {
    if (index == 0) return ["cp", "mv", "ls", "chown"];
    return [];
}

// Auto-completes known files
function autocompleteCommonFiles(index, tokens) {
    if (index == 0) return [];
    return [ ".git", ".gitignore", "package.json" ];
}

// Register the handlers
localEcho.addAutocompleteHandler(autocompleteCommonCommands);
localEcho.addAutocompleteHandler(autocompleteCommonFiles);

local-echo's People

Contributors

coolreader18 avatar m4l3vich avatar rangermauve avatar wavesoft avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

local-echo's Issues

question/idea about future and pty/libreadline compat

Your lib is a really nice write up of the missing pieces to get a local REPL with xterm.js working without any OS layer in between. We already had some requests/questions regarding this and had to point out, that it would only work with a fake pty/libreadline roundtrip+functionality in between. Your lib kinda bridges that gap.

What are your plans with this lib? Do you plan to further standardize it (in regards of pty and libreadline compatibility)? This way it could act as a shim for porting nodejs apps to in-browser variants with little effort and vice versa. Note that emscripten (+ wasm) already delivers shims for that, so the effort to do that might be questionable. Imho still worth to think about it.

Feature Request: Compatibility With Node's `process.std{in,out}`

Reason

Make it possible to create terminal programs that run both in a browser and a terminal.

Wanted Changes

Something like:

// ./lib/LocalEchoController
...
handleTermData(data) { // data may be Buffer!
  data = data.toString(); // Against https://eslint.org/docs/rules/no-param-reassign.
...

Test

import LocalEchoController from 'local-echo';

(async () => {

  const window = globalThis.window;
  const term = !window
    ? (() => {

        process.stdin.setRawMode(true);
        return process.stdin;
      })()
    : await new Promise((resolve) => {
        const term = new Terminal({
          // rendererType: 'dom',
        });
        term.open(document.getElementById('terminal'));
        resolve(term);
      });

  term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ');

  term.on('data', (data) => {

    // In the "raw" mode we must handle ctrl+c manually.
    if(data.toString().charAt(0) === '\x03') {
      (window ? console.log('We can\'t close the window.') : process.exit());
    }
  });
  // Create a local echo controller
  const controller = new LocalEchoController(term);
  while(true) {
    await controller.read("~$ ")
      .then(input => controller.println(`User entered: ${input}`))
      .catch(error => console.log(`Error reading: ${error}`));
  }

})();

Multiline editing doesn't fix cursor when user hits enter with cursor halfway up

So I'm working on a little terminal-based REPL for my programming language Phoo and I found this for the input part. So far works great (though I did have to apply the patch described in #24 to be able to use color prompts and a similar patch as the one in #31 for custom is-complete handlers).

One big problem is when a multiline edit is triggered, the user can move their cursor up, but it isn't moved back down again when they press enter and the command is already complete.

Screenshot 2022-03-22 at 23-17-07 Phoo

In the screenshot I just had the terminal set up to prompt in blue (-> and ..), and then echo the output back again in red.

I typed the first few lines, hit enter with the cursor at the bottom, and it worked fine.
I hit 'up', moved the cursor to the middle of the b's, and hit enter from there, and you can see it clobbered over the input prompt and text!

How do I fix this?

Tests fail with "Cannot find module 'jsesc'"

I get the following error when I run npm test.

$ npm test

> [email protected] test /home/pparkkin/src/local-echo
> jest

 FAIL  lib/Utils.test.js
  โ— Test suite failed to run

    Cannot find module 'jsesc'

      at Object.<anonymous> (node_modules/@babel/generator/lib/generators/types.js:25:37)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.06s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

I ran npm install first.

Node and npm versions.

$ npm --version
6.13.6
$ node --version
v10.15.2

Allow to move cursor up and down in multiline editing?

In the Python prompt_toolkit (which is used by IPython) multiline editing mode, the history entry is only changed when the user presses 'up' with the cursor on the first line, or 'down' with the cursor on the last line. Anywhere else it just moves the cursor up or down. Can this be added?

The likely fix area would be somewhere around here:

case "[A": // Up arrow
if (this.history) {
let value = this.history.getPrevious();
if (value) {
this.setInput(value);
this.setCursor(value.length);
}
}
break;
case "[B": // Down arrow
if (this.history) {
let value = this.history.getNext();
if (!value) value = "";
this.setInput(value);
this.setCursor(value.length);
}
break;

Feature Request: Support of .mjs

The main field of your package.json points to index.js but it's internally is in ES6 format. I guess it will break npm expecting main to point to commonjs packages. Instead module field is proposed for ES6 modules and the extension is .mjs, so index.mjs.

I experiment with modules in Node v12 and currently this code:

// node --experimental-modules this-file.mjs
import LocalEchoController from 'local-echo';

throws:

.../node_modules/local-echo/index.js:1
import LocalEchoController from "./lib/LocalEchoController";
       ^^^^^^^^^^^^^^^^^^^

SyntaxError: Unexpected identifier
    at Module._compile (internal/modules/cjs/loader.js:718:23)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:785:10)
    at Module.load (internal/modules/cjs/loader.js:641:32)
    at Function.Module._load (internal/modules/cjs/loader.js:556:12)
    at internal/modules/esm/translators.js:87:15
    at Object.meta.done (internal/modules/esm/create_dynamic_module.js:45:9)
    at file:///home/ilyaigpetrov/Projects/subterm/node_modules/local-echo/index.js:10:13                                                                     
    at ModuleJob.run (internal/modules/esm/module_job.js:111:37)
    at async Loader.import (internal/modules/esm/loader.js:128:24)

History

If you have entered several commands, then press up once you will get the last entry from the history.
If you send that command and press up again you don't get the last command again, you get the command you have entered before. If you press down you get the command you would have expected.

Make isIncompleteInput pluggable

Hi, thanks for this library! I'm using it at https://babashka.org/xterm-sci/ to host a Clojure REPL.
In Clojure single quotes should not be balanced, but are treated as a way to mark forms as literal expressions instead of evaluating them.

E.g.:

'(+ 1 2 3)

means: I want the list (+ 1 2 3) and not the number 6.

Is it possible to opt out of the need that quotes need to be balanced before seeing the input that the user typed by providing our own function? I am handling the reading of incomplete output myself right now also for other cases (e.g. parens aren't balanced).

The source code is at https://github.com/babashka/xterm-sci.

Fixed with PR #31.

Using color code moves cursor to the right

If you use for example \u001b[32m to colorize you prompt the cursor will be moved to the right (by the count of the hidden chars) if you have entered at least one character:

image

"Cannot use import statement outside a module" when using require

After installing local-echo as a node module using npm install, I tried to import local-echo using require within an electron app but encountered this error:

var LocalEchoController = require('local-echo')

Exception thrown in developer console:

Uncaught D:\electron-app\node_modules\local-echo\index.js:1
(function (exports, require, module, __filename, __dirname, process, global, Buffer) { return function (exports, require, module, __filename, __dirname) { import LocalEchoController from "./lib/LocalEchoController";
                                                                                                                                                           ^^^^^^

SyntaxError: Cannot use import statement outside a module
    at new Script (vm.js:84:7)
    at createScript (vm.js:258:10)
    at Object.runInThisContext (vm.js:306:10)
    at Module._compile (internal/modules/cjs/loader.js:884:26)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:986:10)
    at Module.load (internal/modules/cjs/loader.js:816:32)
    at Module._load (internal/modules/cjs/loader.js:728:14)
    at Module._load (electron/js2c/asar.js:717:26)
    at Function.Module._load (electron/js2c/asar.js:717:26)
    at Module.require (internal/modules/cjs/loader.js:853:19)

Can you fix this error? Or at least provide a solution on how to fix this error? Thanks

Add method to remove listeners

I'd like to use the echo controller to do some initial configuration with the terminal and then remove it so I can connect a different controller.

Would a PR that adds support for something like a close() method be welcome?

Autocompletehandler pasting full command instead of completing the written command

Hello

as the title says the autocomplete is pasting the whole command into the input instead of completing it.
Example:

My commands:

defaultCommands: [
      "print test",
      "print pip list",
    ],

My Autocomplete:

 this.localEcho.addAutocompleteHandler((index, tokens) => {
        if (index !== 0) {
          let possible = this.defaultCommands;
          for (let command of tokens) {
            possible = possible.filter(element => element.includes(command));
          }
          return possible;
        }
        return this.defaultCommands;
      });

The text in the input:

print p **TAB TAB**

Results in:

print p print pip list

Is there a nice and simple solution for that?

Allow asynchronous autocomplete handlers

Sometimes, e.g. when autocompleting filenames from a sandboxed filesystem or a remote server, it would be useful to be able to return autocomplete entries asynchronously.

ReferenceError: term is not defined

Hi

Im receiving this error.

value LocalEchoController.js:217:9 value LocalEchoController.js:229:25 value LocalEchoController.js:370:9 value LocalEchoController.js:571:11 value LocalEchoController.js:421:11 value self-hosted:974:17 [18]</EventEmitter</EventEmitter.prototype.emit EventEmitter.ts:74 [14]</Terminal</Terminal.prototype.handler Terminal.ts:1807 [14]</Terminal</Terminal.prototype._keyDown Terminal.ts:1569 [14]</Terminal</Terminal.prototype._bindKeys/< Terminal.ts:622

any idea how to fix?

Allow program to print something during a prompt without clobbering over input

I have not encountered this problem yet, but I don't see anything in the code to protect against it...

The terminal can still be written to while a prompt is going, and if the user's cursor is not at the bottom, it will clobber over their input. The print and println methods should make sure that doesn't happen, by clearing the input and prompt, printing whatever, and restoring the input and prompt.

Publish to npm?

Are you planning on publishing this to npm so that it can be managed with version locks etc?

Thanks, btw, this does everything I was looking for (after #3 was manually accounted for).

paste text to current input

I use this code to localEcho.handleCursorInsert(text);, everything looks good.
But the comment saids the functionhandleCursorInsert() is Internal API, I wonder if this is the right way to handle paste event.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.