prompt.js

/*
 * prompt.js: Simple prompt for prompting information from the command line 
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var events = require('events'),
    async = require('async'),
    colors = require('colors'),
    winston = require('winston'),
    stdio = process.binding('stdio');

@private function capitalize (str)

str {string} String to capitalize

Capitalizes the string supplied.

function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

var prompt = module.exports = Object.create(events.EventEmitter.prototype);

var logger = prompt.logger = new winston.Logger({
  transports: [
    new (winston.transports.Console)()
  ]
});
    
prompt.started    = false;
prompt.paused     = false;
prompt.allowEmpty = false; 

var stdin, stdout;

Create an empty object for the properties known to prompt

prompt.properties = {};

Setup the default winston logger to use the cli levels and colors.

logger.cli();

function start (options)

@options {Object} Optional Options to consume by prompt

Starts the prompt by listening to the appropriate events on options.stdin and options.stdout. If no streams are supplied, then process.stdin and process.stdout are used, respectively.

prompt.start = function (options) {
  if (prompt.started) {
    return;
  }
  
  options = options        || {};
  stdin   = options.stdin  || process.openStdin();
  stdout  = options.stdout || process.stdout;
  
  prompt.allowEmpty = options.allowEmpty || false;
  
  process.on('SIGINT', function () {
    stdout.write('\n');
    process.exit(1);
  })
  
  prompt.emit('start');
  prompt.started = true;
  return prompt;
};

function pause ()

Pauses input coming in from stdin

prompt.pause = function () {
  if (!prompt.started || prompt.paused) {
    return;
  }
  
  stdin.pause();
  prompt.emit('pause');
  prompt.paused = true;
  return prompt;
};

function resume ()

Resumes input coming in from stdin

prompt.resume = function () {
  if (!prompt.started || !prompt.paused) {
    return;
  }
  
  stdin.resume();
  prompt.emit('resume');
  prompt.paused = false;
  return prompt;
};

function get (msg, [validator,] callback)

@msg {Array|Object|string} Set of variables to get input for.

@callback {function} Continuation to pass control to when complete.

Gets input from the user via stdin for the specified message(s) msg.

prompt.get = function (msg, callback) {
  var vars = !Array.isArray(msg) ? [msg] : msg,
      result = {};
  
  vars = vars.map(function (v) {
    if (typeof v === 'string') {
      v = v.toLowerCase();
    }
    
    return prompt.properties[v] || v;
  });
  
  function get(target, next) {
    prompt.getInput(target, function (err, line) {
      if (err) {
        return next(err);
      }
      
      var name = target.name || target;
      result[name] = line;
      next();
    });
  }
  
  async.forEachSeries(vars, get, function (err) {
    return err ? callback(err) : callback(null, result);
  });
  
  return prompt;
};

function getInput (msg, validator, callback)

@msg {Object|string} Variable to get input for.

@callback {function} Continuation to pass control to when complete.

Gets input from the user via stdin for the specified message msg.

prompt.getInput = function (prop, callback) {
  var name   = prop.message || prop.name || prop,
      raw    = ['prompt', ': ' + name.grey, ': '.grey],
      read   = prop.hidden ? prompt.readLineHidden : prompt.readLine,
      length, msg;
  
  if (prop.default) {
    raw.splice(2, -1, ' (' + prop.default + ')');
  }
  

Calculate the raw length and colorize the prompt

  length = raw.join('').length;
  raw[0] = raw[0];
  msg = raw.join('');
  
  if (prop.help) {
    prop.help.forEach(function (line) {
      logger.help(line);
    });
  }
  
  stdout.write(msg); 
  prompt.emit('prompt', prop);
  
  read.call(null, function (err, line) {
    if (err) {
      return callback(err);
    }
  
    if (!line || line === '') {
      line = prop.default || line;
    }
    
    if (prop.validator || prop.empty === false) {
      var valid;
      
      if (prop.validator) {
        valid = prop.validator.test 
         ? prop.validator.test(line)
         : prop.validator(line);
      }
      
      if (prop.empty === false && valid) {
        valid = line.length > 0;
        prop.warning = prop.warning || 'You must supply a value.';
      }
      
      if (!valid) {
        logger.error('Invalid input for ' + name.grey);
        if (prop.warning) {
          logger.error(prop.warning);
        }
        
        prompt.emit('invalid', prop, line);
        return prompt.getInput(prop, callback);
      }
    }
        
    logger.input(line.yellow);
    callback(null, line);
  });

  return prompt;
};

function addProperties (obj, properties, callback)

@obj {Object} Object to add properties to

@properties {Array} List of properties to get values for

@callback {function} Continuation to pass control to when complete.

Prompts the user for values each of the properties if obj does not already have a value for the property. Responds with the modified object.

prompt.addProperties = function (obj, properties, callback) {
  properties = properties.filter(function (prop) {
    return typeof obj[prop] === 'undefined';
  });
  
  if (properties.length === 0) {
    return callback(obj);
  }
  
  prompt.get(properties, function (err, results) {
    if (err) {
      return callback(err);
    }
    else if (!results) {
      return callback(null, obj);
    }
    
    function putNested (obj, path, value) {
      var last = obj, key; 
      
      while (path.length > 1) {
        key = path.shift();
        if (!last[key]) {
          last[key] = {};
        }
        
        last = last[key];
      }
      
      last[path.shift()] = value;
    }
    
    Object.keys(results).forEach(function (key) {
      putNested(obj, key.split('.'), results[key]);
    });
    
    callback(null, obj);
  });
  
  return prompt;
};

function readLine (callback)

@callback {function} Continuation to respond to when complete

Gets a single line of input from the user.

prompt.readLine = function (callback) {
  var value = '', buffer = '';
  prompt.resume();
  stdin.setEncoding('utf8');
  stdin.on('error', callback);
  stdin.on('data', function data (chunk) {
    value += buffer + chunk;
    buffer = '';
    value = value.replace(/\r/g, '');
    if (value.indexOf('\n') !== -1) {
      if (value !== '\n') {
        value = value.replace(/^\n+/, '');
      }
      
      buffer = value.substr(value.indexOf('\n'));
      val = value.substr(0, value.indexOf('\n'));
      prompt.pause();
      stdin.removeListener('data', data);
      stdin.removeListener('error', callback);
      value = value.trim();
      callback(null, value);
    }
  });
  
  return prompt;
};

function readLineHidden (callback)

@callback {function} Continuation to respond to when complete

Gets a single line of hidden input (i.e. rawMode = true) from the user.

prompt.readLineHidden = function (callback) {
  var value = '', buffer = '';
  stdio.setRawMode(true);
  prompt.resume();
  stdin.on('error', callback);
  stdin.on('data', function data (c) {
    c = '' + c;
    switch (c) {
      case '\n': case '\r': case '\r\n': case '\u0004':
        stdio.setRawMode(false);
        stdin.removeListener('data', data);
        stdin.removeListener('error', callback);
        value = value.trim();
        stdout.write('\n');
        stdout.flush();
        prompt.pause();
        return callback(null, value)
      case '\u0003': case '\0':
        stdout.write('\n');
        process.exit(1);
        break;
      default:
        value += buffer + c
        buffer = '';
        break;
    }
  });
  
  return prompt;
};