snippet.js 4.42 KB
'use strict';

const colors = require('ansi-colors');
const interpolate = require('../interpolate');
const Prompt = require('../prompt');

class SnippetPrompt extends Prompt {
  constructor(options) {
    super(options);
    this.cursorHide();
    this.reset(true);
  }

  async initialize() {
    this.interpolate = await interpolate(this);
    await super.initialize();
  }

  async reset(first) {
    this.state.keys = [];
    this.state.invalid = new Map();
    this.state.missing = new Set();
    this.state.completed = 0;
    this.state.values = {};

    if (first !== true) {
      await this.initialize();
      await this.render();
    }
  }

  moveCursor(n) {
    let item = this.getItem();
    this.cursor += n;
    item.cursor += n;
  }

  dispatch(ch, key) {
    if (!key.code && !key.ctrl && ch != null && this.getItem()) {
      this.append(ch, key);
      return;
    }
    this.alert();
  }

  append(ch, key) {
    let item = this.getItem();
    let prefix = item.input.slice(0, this.cursor);
    let suffix = item.input.slice(this.cursor);
    this.input = item.input = `${prefix}${ch}${suffix}`;
    this.moveCursor(1);
    this.render();
  }

  delete() {
    let item = this.getItem();
    if (this.cursor <= 0 || !item.input) return this.alert();
    let suffix = item.input.slice(this.cursor);
    let prefix = item.input.slice(0, this.cursor - 1);
    this.input = item.input = `${prefix}${suffix}`;
    this.moveCursor(-1);
    this.render();
  }

  increment(i) {
    return i >= this.state.keys.length - 1 ? 0 : i + 1;
  }

  decrement(i) {
    return i <= 0 ? this.state.keys.length - 1 : i - 1;
  }

  first() {
    this.state.index = 0;
    this.render();
  }

  last() {
    this.state.index = this.state.keys.length - 1;
    this.render();
  }

  right() {
    if (this.cursor >= this.input.length) return this.alert();
    this.moveCursor(1);
    this.render();
  }

  left() {
    if (this.cursor <= 0) return this.alert();
    this.moveCursor(-1);
    this.render();
  }

  prev() {
    this.state.index = this.decrement(this.state.index);
    this.getItem();
    this.render();
  }

  next() {
    this.state.index = this.increment(this.state.index);
    this.getItem();
    this.render();
  }

  up() {
    this.prev();
  }

  down() {
    this.next();
  }

  format(value) {
    let color = this.state.completed < 100 ? this.styles.warning : this.styles.success;
    if (this.state.submitted === true && this.state.completed !== 100) {
      color = this.styles.danger;
    }
    return color(`${this.state.completed}% completed`);
  }

  async render() {
    let { index, keys = [], submitted, size } = this.state;

    let newline = [this.options.newline, '\n'].find(v => v != null);
    let prefix = await this.prefix();
    let separator = await this.separator();
    let message = await this.message();

    let prompt = [prefix, message, separator].filter(Boolean).join(' ');
    this.state.prompt = prompt;

    let header = await this.header();
    let error = (await this.error()) || '';
    let hint = (await this.hint()) || '';
    let body = submitted ? '' : await this.interpolate(this.state);

    let key = this.state.key = keys[index] || '';
    let input = await this.format(key);
    let footer = await this.footer();
    if (input) prompt += ' ' + input;
    if (hint && !input && this.state.completed === 0) prompt += ' ' + hint;

    this.clear(size);
    let lines = [header, prompt, body, footer, error.trim()];
    this.write(lines.filter(Boolean).join(newline));
    this.restore();
  }

  getItem(name) {
    let { items, keys, index } = this.state;
    let item = items.find(ch => ch.name === keys[index]);
    if (item && item.input != null) {
      this.input = item.input;
      this.cursor = item.cursor;
    }
    return item;
  }

  async submit() {
    if (typeof this.interpolate !== 'function') await this.initialize();
    await this.interpolate(this.state, true);

    let { invalid, missing, output, values } = this.state;
    if (invalid.size) {
      let err = '';
      for (let [key, value] of invalid) err += `Invalid ${key}: ${value}\n`;
      this.state.error = err;
      return super.submit();
    }

    if (missing.size) {
      this.state.error = 'Required: ' + [...missing.keys()].join(', ');
      return super.submit();
    }

    let lines = colors.unstyle(output).split('\n');
    let result = lines.map(v => v.slice(1)).join('\n');
    this.value = { values, result };
    return super.submit();
  }
}

module.exports = SnippetPrompt;