<commit_before>const _ = require('lodash');
const readline = require('readline');
const chalk = require('chalk');
const MuteStream = require('mute-stream');
const runAsync = require('run-async');
const spinner = require('cli-spinners').dots;
const ScreenManager = require('./lib/screen-manager');

const defaultState = {
  validate: () => true,
  filter: val => val,
  transformer: val => val
};

const defaultMapStateToValue = state => {
  if (!state.value) {
    return state.default;
  }
  return state.value;
};

class StateManager {
  constructor(configFactory, initialState, render) {
    this.initialState = initialState;
    this.render = render;
    this.currentState = {
      loadingIncrement: 0,
      value: '',
      status: 'idle'
    };

    // Default `input` to stdin
    const input = process.stdin;

    // Add mute capabilities to the output
    const output = new MuteStream();
    output.pipe(process.stdout);

    this.rl = readline.createInterface({
      terminal: true,
      input,
      output
    });
    this.screen = new ScreenManager(this.rl);

    let config = configFactory;
    if (_.isFunction(configFactory)) {
      config = configFactory(this.rl);
    }
    this.config = config;

    this.onKeypress = this.onKeypress.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.startLoading = this.startLoading.bind(this);
    this.onLoaderTick = this.onLoaderTick.bind(this);
    this.setState = this.setState.bind(this);
  }

  async execute(cb) {
    let { message } = this.getState();
    this.cb = cb;

    // Load asynchronous properties
    const showLoader = setTimeout(this.startLoading, 500);
    if (_.isFunction(message)) {
      message = await runAsync(message)();
    }
    this.setState({ message, status: 'idle' });

    // Disable the loader if it didn't launch
    clearTimeout(showLoader);

    // Setup event listeners once we're done fetching the configs
    this.rl.input.on('keypress', this.onKeypress);
    this.rl.on('line', this.onSubmit);
  }

  onKeypress(value, key) {
    const { onKeypress = _.noop } = this.config;
    // Ignore enter keypress. The "line" event is handling those.
    if (key.name === 'enter' || key.name === 'return') {
      return;
    }

    this.setState({ value: this.rl.line, error: null });
    onKeypress(this.rl.line, key, this.getState(), this.setState);
  }

  startLoading() {
    this.setState({ loadingIncrement: 0, status: 'loading' });
    setTimeout(this.onLoaderTick, spinner.interval);
  }

  onLoaderTick() {
    const { status, loadingIncrement } = this.getState();
    if (status === 'loading') {
      this.setState({ loadingIncrement: loadingIncrement + 1 });
      setTimeout(this.onLoaderTick, spinner.interval);
    }
  }

  async onSubmit() {
    const state = this.getState();
    const { validate, filter } = state;

    const { mapStateToValue = defaultMapStateToValue } = this.config;
    let value = mapStateToValue(state);

    const showLoader = setTimeout(this.startLoading, 500);
    this.rl.pause();
    try {
      const filteredValue = await runAsync(filter)(value);
      const isValid = await runAsync(validate)(filteredValue);

      if (isValid === true) {
        this.onDone(filteredValue);
      } else {
        this.onError(isValid);
      }
    } catch (err) {
      this.onError(err.message);
    }
    clearTimeout(showLoader);
    this.rl.resume();
  }

  onError(error) {
    this.setState({
      status: 'idle',
      error: error || 'You must provide a valid value'
    });
  }

  onDone(value) {
    this.setState({ status: 'done' });
    this.rl.removeListener('keypress', this.onKeypress);<commit_msg>Fix hanging prompts after close<commit_after>const _ = require('lodash');
const readline = require('readline');
const chalk = require('chalk');
const MuteStream = require('mute-stream');
const runAsync = require('run-async');
const spinner = require('cli-spinners').dots;
const ScreenManager = require('./lib/screen-manager');

const defaultState = {
  validate: () => true,
  filter: val => val,
  transformer: val => val
};

const defaultMapStateToValue = state => {
  if (!state.value) {
    return state.default || '';
  }
  return state.value;
};

class StateManager {
  constructor(configFactory, initialState, render) {
    this.initialState = initialState;
    this.render = render;
    this.currentState = {
      loadingIncrement: 0,
      value: '',
      status: 'idle'
    };

    // Default `input` to stdin
    const input = process.stdin;

    // Add mute capabilities to the output
    const output = new MuteStream();
    output.pipe(process.stdout);

    this.rl = readline.createInterface({
      terminal: true,
      input,
      output
    });
    this.screen = new ScreenManager(this.rl);

    let config = configFactory;
    if (_.isFunction(configFactory)) {
      config = configFactory(this.rl);
    }
    this.config = config;

    this.onKeypress = this.onKeypress.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.startLoading = this.startLoading.bind(this);
    this.onLoaderTick = this.onLoaderTick.bind(this);
    this.setState = this.setState.bind(this);
  }

  async execute(cb) {
    let { message } = this.getState();
    this.cb = cb;

    // Load asynchronous properties
    const showLoader = setTimeout(this.startLoading, 500);
    if (_.isFunction(message)) {
      message = await runAsync(message)();
    }
    this.setState({ message, status: 'idle' });

    // Disable the loader if it didn't launch
    clearTimeout(showLoader);

    // Setup event listeners once we're done fetching the configs
    this.rl.input.on('keypress', this.onKeypress);
    this.rl.on('line', this.onSubmit);
  }

  onKeypress(value, key) {
    const { onKeypress = _.noop } = this.config;
    // Ignore enter keypress. The "line" event is handling those.
    if (key.name === 'enter' || key.name === 'return') {
      return;
    }

    this.setState({ value: this.rl.line, error: null });
    onKeypress(this.rl.line, key, this.getState(), this.setState);
  }

  startLoading() {
    this.setState({ loadingIncrement: 0, status: 'loading' });
    setTimeout(this.onLoaderTick, spinner.interval);
  }

  onLoaderTick() {
    const { status, loadingIncrement } = this.getState();
    if (status === 'loading') {
      this.setState({ loadingIncrement: loadingIncrement + 1 });
      setTimeout(this.onLoaderTick, spinner.interval);
    }
  }

  async onSubmit() {
    const state = this.getState();
    const { validate, filter } = state;

    const { mapStateToValue = defaultMapStateToValue } = this.config;
    let value = mapStateToValue(state);

    const showLoader = setTimeout(this.startLoading, 500);
    this.rl.pause();
    try {
      const filteredValue = await runAsync(filter)(value);
      const isValid = await runAsync(validate)(filteredValue);

      if (isValid === true) {
        this.onDone(filteredValue);
        clearTimeout(showLoader);
        return;
      }

      this.onError(isValid);
    } catch (err) {
      this.onError(err.message);
    }
    clearTimeout(showLoader);
    this.rl.resume();
  }

  onError(error) {
    this.setState({
      status: 'idle',
      error: error || 'You must provide a valid value'
    });
  }

  onDone(value) {
    this.setState({ status: 'done' });
    this.rl.input.removeListener('keypress', this.onKeypress);
{'fraction_non_alphanumeric': 0.13962621885157098, 'fraction_numerical': 0.002437703141928494, 'mean_word_length': 3.053238199780461, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1965384', 'n_tokens_mistral': 2303, 'n_tokens_neox': 2262, 'n_words': 514}