SBoudrias / Inquirer.js

A collection of common interactive command line user interfaces.
MIT License
20.03k stars 1.3k forks source link

Using transformer and moving the caret cursor #669

Open gentunian opened 6 years ago

gentunian commented 6 years ago

Hi there :)

I'm using the transformer function to visually append hinted data such as:

transformer: (input, answer) => {
  return `${a}.mydomain.com`;
}

works as expected, but how could I play with the caret cursor so it's positioned where the user is typing:

out

Full example:

var inquirer = require('inquirer');

inquirer.prompt({
  name: 'hostname',
  type: 'input',
  message: 'hostname:',
  transformer: function(a,b) {
    return `${a}.mydomain.com`
  }
});

Is it possible?

SBoudrias commented 6 years ago

It would be possible with a custom prompt - but that's not a feature of the input prompt right now.

Maybe a solution is to have an extra property, like suffix append that after the cursor. I don't think there's a way this could work with the transformer function.

gentunian commented 6 years ago

Hey @SBoudrias :)

I was reading your code and trying to hack it. I was only reading the parts of interests and this hack seems to work only for suffixes because prompt is based on length:

--- a/lib/prompts/input.js
+++ b/lib/prompts/input.js
@@ -55,7 +55,7 @@ class InputPrompt extends Base {
       bottomContent = chalk.red('>> ') + error;
     }

-    this.screen.render(message, bottomContent);
+    this.screen.render(message, bottomContent, this.getQuestion() + this.rl.line);
   }
--- a/lib/utils/screen-manager.js
+++ b/lib/utils/screen-manager.js
@@ -22,7 +22,7 @@ class ScreenManager {
     this.rl = rl;
   }

-  render(content, bottomContent) {
+  render(content, bottomContent, changedPrompt) {
     this.rl.output.unmute();
     this.clean(this.extraLinesUnderPrompt);

@@ -30,7 +30,7 @@ class ScreenManager {
      * Write message to screen and setPrompt to control backspace
      */

-    var promptLine = lastLine(content);
+    var promptLine = changedPrompt? changedPrompt: lastLine(content);
     var rawPromptLine = stripAnsi(promptLine);

I will give it another review for working in most cases.

Sample test:

var inquirer = require('inquirer');

inquirer.prompt([{
  name: 'suffix',
  type: 'input',
  message: 'suffix:',
  transformer: function (a, b) {
    return `${a}.suffix`
  }
}, {
  name: 'middle',
  type: 'input',
  message: 'middle:',
  transformer: function (a, b) {
    return `test==>${a}<==my-input`
  }
}, {
  name: 'prefix',
  type: 'input',
  message: 'prefix:',
  transformer: function (a, b) {
    return `prefix==>${a}`
  }
}]);
gentunian commented 6 years ago

After giving this a little thought and understanding of the code, this seems unlikely to be implemented.

My impressions are that this feature should be implemented on the transformation as it's the only point where the user takes control of the readline output buffer. The transformation function basically sticks in the middle so it seems there's no way to know where the user is typing just because the user can transform the output as the way he/she may like.

From the above, it follows that the transformation (if exists) should inform where the user want the cursor to be positioned. This is possible to do, but not elegant, this is my example:

out2

For achieving that behaviour, the transform function should provide the prompt information. As I said, not elegant at all:

var inquirer = require('inquirer');

inquirer.prompt([{
  name: 'suffix',
  type: 'input',
  message: 'suffix:',
  transformer: function (a, b) {
    return `${a}.mydomain.com`
  }
}, {
  name: 'middle',
  type: 'input',
  message: 'middle:',
  transformer: function (a, b) {
    return {
      text: `test==>${a}<==my-input`,
      caret: `test==>`
    }
  }
}, {
  name: 'prefix',
  type: 'input',
  message: 'prefix:',
  transformer: function (a, b) {
    return {
      text: `test==>${a}`,
      caret: `test==>`
    }
  }
}, {
  name: 'complex',
  type: 'input',
  message: 'complex:',
  transformer: function (a, b) {
    return {
      text: `${a} <--> ${a}`,
      caret: `${a} <--> `
    }
  }
}]);

For this to work, the inquirer.js library needs this patch:

diff --git a/lib/prompts/input.js b/lib/prompts/input.js
index 4fd765e..da77b38 100644
--- a/lib/prompts/input.js
+++ b/lib/prompts/input.js
@@ -42,11 +42,20 @@ class InputPrompt extends Base {
     var bottomContent = '';
     var message = this.getQuestion();
     var transformer = this.opt.transformer;
+    var caretOffset = '';

     if (this.status === 'answered') {
       message += chalk.cyan(this.answer);
     } else if (transformer) {
-      message += transformer(this.rl.line, this.answers);
+      var transform = transformer(this.rl.line, this.answers);
+      if (typeof transform === 'string') {
+        message += transform;
+      } else if (transform.caret && transform.text) {
+        message += transform.text;
+        caretOffset = transform.caret;
+      } else {
+
+      }
     } else {
       message += this.rl.line;
     }
@@ -55,7 +64,7 @@ class InputPrompt extends Base {
       bottomContent = chalk.red('>> ') + error;
     }

-    this.screen.render(message, bottomContent);
+    this.screen.render(message, bottomContent, this.getQuestion() + caretOffset);
   }

   /**
diff --git a/lib/utils/screen-manager.js b/lib/utils/screen-manager.js
index f19126e..d33257a 100644
--- a/lib/utils/screen-manager.js
+++ b/lib/utils/screen-manager.js
@@ -22,7 +22,7 @@ class ScreenManager {
     this.rl = rl;
   }

-  render(content, bottomContent) {
+  render(content, bottomContent, changedPrompt) {
     this.rl.output.unmute();
     this.clean(this.extraLinesUnderPrompt);

@@ -30,17 +30,13 @@ class ScreenManager {
      * Write message to screen and setPrompt to control backspace
      */

-    var promptLine = lastLine(content);
+    var promptLine = changedPrompt? changedPrompt: lastLine(content);
     var rawPromptLine = stripAnsi(promptLine);

     // Remove the rl.line from our prompt. We can't rely on the content of
     // rl.line (mainly because of the password prompt), so just rely on it's
     // length.
-    var prompt = rawPromptLine;
-    if (this.rl.line.length) {
-      prompt = prompt.slice(0, -this.rl.line.length);
-    }
-    this.rl.setPrompt(prompt);
+    this.rl.setPrompt(rawPromptLine);

     // SetPrompt will change cursor position, now we can get correct value
     var cursorPos = this.rl._getCursorPos();

lib/prompts/input.js was quickly and dirty modified to test the functionality and show the idea. Those if statements are a bad idea.

SBoudrias commented 6 years ago

Hum, I wonder if we could provide the caret in a way like this:

inquirer.prompt([{
  name: 'suffix',
  type: 'input',
  message: 'suffix:',
  transformer: function (a, b) {
    return `${a}${inquirer.CARET}.mydomain.com`
  }
}]);

inquirer.CARET could be a Symbol or a unique enough string we'd search for and when found use to position the caret maybe?

gentunian commented 6 years ago

Nice tip. I found a solution for that :)

I've chosen a custom string for specifying the caret. Not sure how could I reach the inquirer namespace inside input.js without including inquirer.js.

It could be placed in a custom file just like constants.js or something like that. So client code will use inquirer.CARET and library code will use internally that file.

gentunian commented 6 years ago

@SBoudrias this is the commit, can you take a look? :)

EDIT: I'm testing this thing out, but the main idea is not so bad.

Here are the results: out3

I've detected only one minor issue when not specifying the cursor position. The cursor will be 1 character to the left than where it should be.

Sample code:

var inquirer = require('inquirer');

inquirer.prompt([{
  name: 'suffix',
  type: 'input',
  message: 'suffix transform:',
  transformer: function (a, b) {
    return `${a}${inquirer.CARET}.mydomain.com`
  }
}, {
  name: 'middle',
  type: 'input',
  message: 'in the middle CARET:',
  transformer: function (a, b) {
    return `optional---> ${a}${inquirer.CARET} <---optional`;
  }
}, {
  name: 'prefix',
  type: 'input',
  message: 'prefix transform:',
  transformer: function (a, b) {
    return `append-me: ${a}${inquirer.CARET}`;
  }
}, {
  name: 'complex',
  type: 'input',
  message: 'complex and mixed:',
  transformer: function (a, b) {
    return `${a} <--> ${a}${inquirer.CARET}`;
  }
}, {
  name: 'nocaret',
  type: 'input',
  message: 'transformer without CARET:',
  transformer: function (a, b) {
    return `${a} ----NO CARET ----`;
  }
}, {
  name: 'normal',
  type: 'input',
  message: 'this is normal:'
}]);
SBoudrias commented 6 years ago

Seems good, wanna send a PR?

twang-rs commented 6 years ago

Why not optionally allow a tuple return instead? You can place the cursor position at the end of the string if the transformer returns a simple string; otherwise, place the cursor after the first element in the tuple and render the rest of the tuple after the cursor.

gentunian commented 6 years ago

@twang-rs Sounds good but you will need to refactor more code if you plan to apply this logic into screen.render. I think the solution should maintain the current interface and methods.

That is, in order to maintain the interfaces you should process this tuple before calling screen.render.