OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / lib / program.js

92 lines · javascript ref:
import { Screen } from './screen.js'

export class Program {
  constructor (model, opts = {}) {
    this.model = model
    this.screen = new Screen()
    this.running = false
    this._pollInterval = opts.pollInterval || 2000
    this._pollTimer = null
    this._onPoll = opts.onPoll || null
  }

  run () {
    this.running = true

    this.screen.enterAltScreen()
    this.screen.hideCursor()
    this.screen.enableRawMode()

    this.screen.on('key', (key) => this.dispatch({ type: 'key', key }))
    this.screen.on('resize', (size) => {
      this.dispatch({ type: 'resize', ...size })
    })

    // Initial size into model
    this.dispatch({
      type: 'resize',
      width: this.screen.width,
      height: this.screen.height
    })

    this.render()
    this._startPoll()
  }

  dispatch (msg) {
    if (!this.running) return

    // Ctrl+C always quits
    if (msg.type === 'key' && msg.key.name === 'c' && msg.key.ctrl) {
      this.quit()
      return
    }

    const result = this.model.update(msg)
    if (result && result.quit) {
      this.quit()
      return
    }

    this.render()
  }

  render () {
    if (!this.running) return
    const w = this.screen.width
    const h = this.screen.height
    const lines = this.model.view(w, h)
    this.screen.render(lines)
  }

  quit () {
    if (!this.running) return
    this.running = false
    this._stopPoll()
    this.screen.destroy()
    Bare.exit(0)
  }

  _startPoll () {
    if (!this._onPoll) return
    const tick = async () => {
      if (!this.running) return
      try {
        await this._onPoll(this)
        this.render()
      } catch {}
      if (this.running) {
        this._pollTimer = setTimeout(tick, this._pollInterval)
      }
    }
    this._pollTimer = setTimeout(tick, 100)
  }

  _stopPoll () {
    if (this._pollTimer) {
      clearTimeout(this._pollTimer)
      this._pollTimer = null
    }
  }
}