28

I would like to be able to open Vim from node.js program running in the terminal, create some content, save and exit Vim, and then grab the contents of the file.

I'm trying to do something like this:

filename = '/tmp/tmpfile-' + process.pid

editor = process.env['EDITOR'] ? 'vi'
spawn editor, [filename], (err, stdout, stderr) ->

  text = fs.readFileSync filename
  console.log text

However, when this runs, it just hangs the terminal.

I've also tried it with exec and got the same result.

Update:

This is complicated by the fact that this process is launched from a command typed at a prompt with readline running. I completely extracted the relevant parts of my latest version out to a file. Here is it in its entirety:

{spawn} = require 'child_process'
fs = require 'fs'
tty = require 'tty'
rl = require 'readline'

cli = rl.createInterface process.stdin, process.stdout, null
cli.prompt()

filename = '/tmp/tmpfile-' + process.pid

proc = spawn 'vim', [filename]

#cli.pause()
process.stdin.resume()

indata = (c) ->
    proc.stdin.write c
process.stdin.on 'data', indata

proc.stdout.on 'data', (c) ->
    process.stdout.write c

proc.on 'exit', () ->
    tty.setRawMode false
    process.stdin.removeListener 'data', indata

    # Grab content from the temporary file and display it
    text = fs.readFile filename, (err, data) ->
        throw err if err?  
        console.log data.toString()

        # Try to resume readline prompt
        cli.prompt()

The way it works as show above, is that it shows a prompt for a couple of seconds, and then launches in to Vim, but the TTY is messed up. I can edit, and save the file, and the contents are printed correctly. There is a bunch of junk printed to terminal on exit as well, and Readline functionality is broken afterward (no Up/Down arrow, no Tab completion).

If I uncomment the cli.pause() line, then the TTY is OK in Vim, but I'm stuck in insert mode, and the Esc key doesn't work. If I hit Ctrl-C it quits the child and parent process.

mkopala
  • 1,262
  • 3
  • 12
  • 15
  • Can you shed some light on the use case? Do you want to interact with Vim by running its commands or just write a file to disk? – Devin M Feb 03 '12 at 00:55
  • Is the intention to ask a user to use the editor to create the content? Or are you intending to drive `vim` completely from within `node.js`? – sarnold Feb 03 '12 at 00:59
  • I want to be able to open `vim` so that I can create content, and when finished, exit, and have node grab the contents of the temporary file, which I will then send as part of an HTTP request from node. – mkopala Feb 03 '12 at 01:11
  • 1
    Can you try with raw mode on/off? http://nodejs.org/docs/latest/api/tty.html#tty.setRawMode – Andrey Sidorov Feb 03 '12 at 01:44
  • 1
    I seem to be using the wrong tool for the job :-) This would be one line in `bash`. It works decently well having the rest of the app in node.js though, and I can move code between the server API and this client code easier. A good learning experience, I suppose. – mkopala Feb 03 '12 at 19:13
  • 1
    Yeah, this does seem like an odd thing to be doing in node, but it is kind of fun to figure out how to do it. I'll see if I can fix your readline issue in like an hour. – loganfsmyth Feb 03 '12 at 22:15

3 Answers3

49

You can inherit stdio from the main process.

const child_process = require('child_process')
var editor = process.env.EDITOR || 'vi';

var child = child_process.spawn(editor, ['/tmp/somefile.txt'], {
    stdio: 'inherit'
});

child.on('exit', function (e, code) {
    console.log("finished");
});

More options here: http://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options

edi9999
  • 19,701
  • 13
  • 88
  • 127
Ilia Sidorenko
  • 2,157
  • 3
  • 26
  • 30
12

Update: My answer applied at the time it was created, but for modern versions of Node, look at this other answer.

First off, your usage of spawn isn't correct. Here are the docs. http://nodejs.org/docs/latest/api/child_processes.html#child_process.spawn

Your sample code makes it seem like you expect vim to automatically pop up and take over the terminal, but it won't. The important thing to remember is that even though you may spawn a process, it is up to you to make sure that the data from the process makes it through to your terminal for display.

In this case, you need to take data from stdin and send it to vim, and you need to take data output by vim and set it to your terminal, otherwise you won't see anything. You also need to set the tty into raw mode, otherwise node will intercept some of the key sequences, so vim will not behave properly.

Next, don't do readFileSync. If you come upon a case where you think you need to use a sync method, then chances are, you are doing something wrong.

Here's a quick example I put together. I can't vouch for it working in every single case, but it should cover most cases.

var tty = require('tty');
var child_process = require('child_process');
var fs = require('fs');

function spawnVim(file, cb) {
  var vim = child_process.spawn( 'vim', [file])

  function indata(c) {
    vim.stdin.write(c);
  }
  function outdata(c) {
    process.stdout.write(c);
  }

  process.stdin.resume();
  process.stdin.on('data', indata);
  vim.stdout.on('data', outdata);
  tty.setRawMode(true);

  vim.on('exit', function(code) {
    tty.setRawMode(false);
    process.stdin.pause();
    process.stdin.removeListener('data', indata);
    vim.stdout.removeListener('data', outdata);

    cb(code);
  });
}

var filename = '/tmp/somefile.txt';

spawnVim(filename, function(code) {
  if (code == 0) {
    fs.readFile(filename, function(err, data) {
      if (!err) {
        console.log(data.toString());
      }
    });
  }
});

Update

I seeee. I don't think readline is as compatible with all of this as you would like unfortunately. The issue is that when you createInterface, node kind of assumes that it will have full control over that stream from that point forward. When we redirect that data to vim, readline is still there processing keypresses, but vim is also doing the same thing.

The only way around this that I see is to manually disable everything from the cli interface before you start vim.

Just before you spawn the process, we need to close the interface, and unfortunately manually remove the keypress listener because, at least at the moment, node does not remove it automatically.

process.stdin.removeAllListeners 'keypress'
cli.close()
tty.setRawMode true

Then in the process 'exit' callback, you will need to call createInterface again.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • 1
    This is almost working. First problem is it takes a couple of seconds for Vim to appear though, and it's only half-screen in the terminal. Second, the TTY is also meesed up. After type `iThis is a test of the editor` it shows `TThi i tes o th editor`. Exiting and displaying the file contents shows the correct string. – mkopala Feb 03 '12 at 07:54
  • 1
    The program I'm using this in is a single-user command line client to an API, so that's why I used `readFileSync`. Simplifies the code a bit by dropping a callback. In fact, it would probably simpler to have the all of the code synchronous ... but this is what I've got for now. – mkopala Feb 03 '12 at 08:01
  • 1
    Still, readFileSync is bad form. If you're going to use node, you might as well do it right. I don't know why it is showing up so strangely for you. Can you try using a simpler editor, like nano? MY vim definitely works better than yours, but my backspace key doesn't work and I can't seem to get it working, but it works in nano. Are you getting that issue with the exact code I posted, or did you integrate it into your code? Maybe post more of the code you are using? – loganfsmyth Feb 03 '12 at 14:14
  • 1
    Ok, guilty as charged ;-) I only tried to integrate your code before. I have no idea why I didn't just copy & paste and run it. I did that now, and it works great (no backspace issue here). This led me to discover that my use of **readline** is apparently causing a problem. I updated the original question. Thanks so much for your help! – mkopala Feb 03 '12 at 18:58
  • That solved it. Vim still takes a couple of seconds to load, which is annoying, but it's working. Thanks! – mkopala Feb 05 '12 at 06:02
  • 1
    I didn't read the lower answer about inheritance and found that this code (1) no longer works (node 6.9.1) and (2) it can be fixed by removing `var tty = require('tty');` and replacing `tty.setRawMode` with `process.stdin.setRawMode`. Just FYI, but the inherit solution is better. – devtanc Jul 20 '17 at 17:56
  • 1
    @tanc Good call, added a note to the top of my answer linking to the other answer. – loganfsmyth Jul 20 '17 at 18:01
  • 1
    one of the keys here is the call to setRawMode(true), i had to do process.stdin.setRawMode(true) – Fernando Gabrieli Jul 10 '18 at 01:10
0

I tried to do something like this using Node's repl library - https://nodejs.org/api/repl.html - but nothing worked. I tried launching vscode and TextEdit, but on the Mac there didn't seem to be a way to wait for those programs to close. Using execSync with vim, nano, and micro all acted strangely or hung the terminal.

Finally I switched to using the readline library using the example given here https://nodejs.org/api/readline.html#readline_example_tiny_cli - and it worked using micro, e.g.

import { execSync } from 'child_process'
...
case 'edit':
  const cmd = `micro foo.txt`
  const result = execSync(cmd).toString()
  console.log({ result })
  break

It switches to micro in a Scratch buffer - hit ctrl-q when done, and it returns the buffer contents in result.

Brian Burns
  • 20,575
  • 8
  • 83
  • 77