@noffle/cabal
Version:
p2p forum software
314 lines (265 loc) • 9.47 kB
JavaScript
var neatLog = require('neat-log')
var chalk = require('chalk')
var strftime = require('strftime')
var Commander = require('./commands.js')
var views = require('./views')
var collect = require('collect-stream')
const HEADER_ROWS = 6
function NeatScreen (cabal) {
if (!(this instanceof NeatScreen)) return new NeatScreen(cabal)
var self = this
this.cabal = cabal
this.commander = Commander(this, cabal)
this.neat = neatLog(renderApp, {fullscreen: true,
style: function (start, cursor, end) {
if (!cursor) cursor = ' '
return start + chalk.underline(cursor) + end
}}
)
this.neat.input.on('update', () => this.neat.render())
this.neat.input.on('enter', (line) => this.commander.process(line))
this.neat.input.on('tab', () => {
var line = self.neat.input.rawLine()
if (line.length > 1 && line[0] === '/') {
// command completion
var soFar = line.slice(1)
var commands = Object.keys(this.commander.commands)
var matchingCommands = commands.filter(cmd => cmd.startsWith(soFar))
if (matchingCommands.length === 1) {
self.neat.input.set('/' + matchingCommands[0])
}
} else {
// nick completion
var users = Object.keys(self.state.users)
.map(key => self.state.users[key])
.map(user => user.name || user.key.substring(0, 8))
.sort()
var pattern = (/^(\w+)$/)
var match = pattern.exec(line)
if (match) {
users = users.filter(user => user.startsWith(match[0]))
if (users.length > 0) self.neat.input.set(users[0] + ': ')
}
}
})
this.neat.input.on('up', () => {
if (self.commander.history.length) {
var command = self.commander.history.pop()
self.commander.history.unshift(command)
self.neat.input.set(command)
}
})
this.neat.input.on('down', () => {
if (self.commander.history.length) {
var command = self.commander.history.shift()
self.commander.history.push(command)
self.neat.input.set(command)
}
})
// set channel with alt-#
this.neat.input.on('alt-1', () => { setChannelByIndex(0) })
this.neat.input.on('alt-2', () => { setChannelByIndex(1) })
this.neat.input.on('alt-3', () => { setChannelByIndex(2) })
this.neat.input.on('alt-4', () => { setChannelByIndex(3) })
this.neat.input.on('alt-5', () => { setChannelByIndex(4) })
this.neat.input.on('alt-6', () => { setChannelByIndex(5) })
this.neat.input.on('alt-7', () => { setChannelByIndex(6) })
this.neat.input.on('alt-8', () => { setChannelByIndex(7) })
this.neat.input.on('alt-9', () => { setChannelByIndex(8) })
this.neat.input.on('alt-0', () => { setChannelByIndex(9) })
this.neat.input.on('keypress', (ch, key) => {
if (!key || !key.name) return
if (key.name === 'home') this.neat.input.cursor = 0
else if (key.name === 'end') this.neat.input.cursor = this.neat.input.rawLine().length
else return
this.bus.emit('render')
})
// move up/down channels with ctrl+{n,p}
this.neat.input.on('ctrl-p', () => {
var currentIdx = self.state.channels.indexOf(self.commander.channel)
if (currentIdx !== -1) {
currentIdx--
if (currentIdx < 0) currentIdx = self.state.channels.length - 1
setChannelByIndex(currentIdx)
}
})
this.neat.input.on('ctrl-n', () => {
var currentIdx = self.state.channels.indexOf(self.commander.channel)
if (currentIdx !== -1) {
currentIdx++
currentIdx = currentIdx % self.state.channels.length
setChannelByIndex(currentIdx)
}
})
function setChannelByIndex (n) {
if (n < 0 || n >= self.state.channels.length) return
self.commander.channel = self.state.channels[n]
self.loadChannel(self.state.channels[n])
}
this.neat.input.on('ctrl-d', () => process.exit(0))
this.neat.input.on('pageup', () => self.state.scrollback++)
this.neat.input.on('pagedown', () => self.state.scrollback = Math.max(0, self.state.scrollback - 1))
this.neat.use(function (state, bus) {
state.cabal = cabal
state.neat = self.neat
self.state = state
self.bus = bus
self.state.messages = []
self.state.channels = []
self.state.users = {}
self.state.user = null
self.cabal.on('peer-added', function (key) {
var found = false
Object.keys(self.state.users).forEach(function (k) {
if (k === key) {
self.state.users[k].online = true
found = true
}
})
if (!found) {
self.state.users[key] = {
key: key,
online: true
}
}
self.bus.emit('render')
})
self.cabal.on('peer-dropped', function (key) {
Object.keys(self.state.users).forEach(function (k) {
if (k === key) {
self.state.users[k].online = false
self.bus.emit('render')
}
})
})
// TODO: use cabal-core api for all of this
self.cabal.db.ready(function () {
self.cabal.channels.get((err, channels) => {
if (err) return
self.state.channels = channels
self.loadChannel('default')
self.bus.emit('render')
self.cabal.channels.events.on('add', function (channel) {
self.state.channels.push(channel)
self.state.channels.sort()
self.bus.emit('render')
})
})
self.cabal.users.getAll(function (err, users) {
if (err) return
state.users = users
updateLocalKey()
self.cabal.users.events.on('update', function (key) {
// TODO: rate-limit
self.cabal.users.get(key, function (err, user) {
if (err) return
state.users[key] = Object.assign(state.users[key] || {}, user)
if (self.state.user && key === self.state.user.key) self.state.user = state.users[key]
if (!self.state.user) updateLocalKey()
self.bus.emit('render')
})
})
function updateLocalKey () {
self.cabal.getLocalKey(function (err, lkey) {
if (err) return self.bus.emit('render')
Object.keys(users).forEach(function (key) {
if (key === lkey) {
self.state.user = users[key]
self.state.user.local = true
self.state.user.online = true
self.state.user.key = key
}
})
self.bus.emit('render')
})
}
})
})
})
}
function renderApp (state) {
if (process.stdout.columns > 80) return views.big(state)
else return views.small(state)
}
// use to write anything else to the screen, e.g. info messages or emotes
NeatScreen.prototype.writeLine = function (line) {
this.state.messages.push(`${chalk.gray(line)}`)
this.bus.emit('render')
}
NeatScreen.prototype.clear = function () {
this.state.messages = []
this.bus.emit('render')
}
NeatScreen.prototype.loadChannel = function (channel) {
if (this.state.msgListener) {
this.cabal.messages.events.removeListener(this.state.channel, this.state.msgListener)
this.state.msgListener = null
}
var self = this
// This is really cheap, so we could load many more if we wanted to!
var MAX_MESSAGES = process.stdout.rows - HEADER_ROWS + 50
self.state.channel = channel
// clear the old channel state
self.state.scrollback = 0
self.state.messages = []
self.neat.render()
// MISSING: mention beeps
// MISSING: day change messages
var pending = 0
function onMessage () {
if (pending > 0) {
pending++
return
}
pending = 1
// TODO: wrap this up in a nice interface and expose it via cabal-client
var rs = self.cabal.messages.read(channel, {limit: MAX_MESSAGES, lt: '~'})
collect(rs, function (err, msgs) {
if (err) return
msgs.reverse()
self.state.messages = []
msgs.forEach(function (msg) {
self.state.messages.push(self.formatMessage(msg))
})
self.neat.render()
if (pending > 1) {
pending = 0
onMessage()
} else {
pending = 0
}
})
}
self.cabal.messages.events.on(channel, onMessage)
self.state.msgListener = onMessage
onMessage()
}
NeatScreen.prototype.render = function () {
this.bus.emit('render')
}
NeatScreen.prototype.formatMessage = function (msg) {
var self = this
var highlight = false
var user = self.cabal.username
if (!msg.value.type) { msg.type = 'chat/text' }
if (msg.value.content && msg.value.timestamp) {
if (msg.value.content.text.indexOf(user) > -1 && msg.value.author !== user) { highlight = true }
var author
if (this.state.users && this.state.users[msg.key]) author = this.state.users[msg.key].name || this.state.users[msg.key].key.slice(0, 8)
else author = msg.key.slice(0, 8)
var timestamp = `${chalk.gray(formatTime(msg.value.timestamp))}`
var authorText = `${chalk.gray('<')}${chalk.cyan(author)}${chalk.gray('>')}`
var content = msg.value.content.text
var emote = (msg.value.type === 'chat/emote')
if (emote) {
authorText = `${chalk.white(author)}`
content = `${chalk.gray(msg.value.content.text)}`
}
return timestamp + (emote ? ' * ' : ' ') + (highlight ? chalk.bgRed(chalk.black(authorText)) : authorText) + ' ' + content
}
return chalk.cyan('unknown message type: ') + chalk.gray(JSON.stringify(msg.value))
}
function formatTime (t) {
return strftime('%T', new Date(t))
}
module.exports = NeatScreen