cabal-cli
Version:
285 lines (245 loc) • 10.5 kB
JavaScript
var output = require('./output')
var chalk = require('chalk')
var blit = require('txt-blit')
var util = require('./util')
var version = require('./package.json').version
const HEADER_ROWS = 8
const NICK_COLS = 15
const CHAN_COLS = 16
module.exports = { big, small, getPageSize, getChatWidth }
function getPageSize () {
return process.stdout.rows - HEADER_ROWS
}
function getChatWidth () {
if (process.stdout.columns > 80) {
return process.stdout.columns - NICK_COLS - CHAN_COLS - 2 /* 2x vertical dividers */ - 1 /* nick col padding */
}
return process.stdout.columns
}
function small (state) {
var screen = []
var titlebarSize = Math.ceil(linkSize(state) / process.stdout.columns)
// title bar
blit(screen, renderTitlebar(state, process.stdout.columns), 0, titlebarSize - 1)
// chat messages
blit(screen, renderMessages(state, process.stdout.columns, process.stdout.rows - HEADER_ROWS), 0, 3)
// horizontal dividers
blit(screen, renderHorizontalLine('─', process.stdout.columns, chalk.blue), 0, process.stdout.rows - 2)
blit(screen, renderHorizontalLine('─', process.stdout.columns, chalk.blue), 0, titlebarSize + 1)
// user input prompt
blit(screen, renderPrompt(state), 0, process.stdout.rows - 1)
return output(screen.join('\n'))
}
function big (state) {
var screen = []
// title bar
blit(screen, renderTitlebar(state, process.stdout.columns), 0, 0)
if (state.cabals.length > 1) {
// cabals pane
blit(screen, renderCabals(state, 6, process.stdout.rows - HEADER_ROWS), 0, process.stdout.rows - 3)
}
// channels listing
blit(screen, renderChannels(state, CHAN_COLS, process.stdout.rows - HEADER_ROWS), 0, 3)
blit(screen, renderVerticalLine('│', process.stdout.rows - 7, chalk.blue), 16, 3)
// channel topic description
blit(screen, renderChannelTopic(state, process.stdout.columns - 16 - 17, process.stdout.rows - HEADER_ROWS), 17, 3)
// chat messages
blit(screen, renderMessages(state, process.stdout.columns - 17 - 17, process.stdout.rows - HEADER_ROWS), 17, 4)
// nicks pane
blit(screen, renderVerticalLine('│', process.stdout.rows - 7, chalk.blue), process.stdout.columns - 17, 3)
blit(screen, renderNicks(state, NICK_COLS, process.stdout.rows - HEADER_ROWS), process.stdout.columns - 15, 3)
// horizontal dividers
blit(screen, renderHorizontalLine('─', process.stdout.columns, chalk.blue), 0, process.stdout.rows - 4)
blit(screen, renderHorizontalLine('─', process.stdout.columns, chalk.blue), 0, 2)
// user input prompt
blit(screen, renderPrompt(state), 0, process.stdout.rows - 2)
return output(screen.join('\n'))
}
function linkSize (state) {
const moderationKey = util.getModerationKey(state)
if (state.cabal.key) return `cabal://${state.cabal.key.toString('hex')}`.length + moderationKey.length
else return 'cabal://...'
}
function renderPrompt (state) {
var name = util.sanitizeString(state.cabal ? state.cabal.getLocalName() : 'unknown')
var channel = state.cabal.getCurrentChannel()
var channelName = channel
if (state.cabal.isChannelPrivate(channel)) {
const recipient = state.cabal.getUsers()[channelName]
const recipientName = recipient.name || recipient.key.slice(0, 8)
channelName = 'pm with ' + recipientName
}
return [
`[${chalk.cyan(name)}:${channelName}] ${state.neat.input.line()}`
]
}
function renderTitlebar (state, width) {
const moderationKey = chalk.cyan(util.getModerationKey(state))
return [
chalk.bgBlue(util.centerText(chalk.whiteBright.bold(`CABAL@${version}`), width)),
util.rightAlignText(`cabal://${state.cabal.key.toString('hex')}${moderationKey}`, width)
]
}
function renderCabals (state, width, height) {
return ['[' + state.cabals.map(function (cabal, idx) {
var key = cabal
var keyTruncated = key.substring(0, 6)
// if we're dealing with the active/focused cabal
if (state.cabal.key === key) {
if (state.selectedWindowPane === 'cabals') {
return `(${chalk.bgBlue(keyTruncated)})`
} else {
return `(${chalk.cyan(keyTruncated)})`
}
} else {
return chalk.white(keyTruncated)
}
}).join(' ') + ']']
}
function renderChannels (state, width, height) {
const channels = state.cabal.getChannels({ includePM: true, onlyJoined: true })
const numPrefixWidth = String(channels.length).length
const users = state.cabal.getUsers()
return channels
.map((channel, idx) => {
const isPrivate = state.cabal.isChannelPrivate(channel)
var channelTruncated = channel.substring(0, width - 5)
if (isPrivate) {
// if private, `channel` contains the pubkey of who we are chatting with
channelTruncated = `+${getPrintedName(users[channel])}`
}
var unread = channel in state.unreadChannels
var mentioned = channel in state.mentions
const channelIdx = idx + 1
let numPrefix = channelIdx + '. '
const numLength = String(channelIdx).length
if (numLength < numPrefixWidth) {
numPrefix += new Array(numLength).fill(' ').join('')
}
numPrefix = chalk.cyan(numPrefix)
if (state.cabal.getCurrentChannel() === channel) {
var fillWidth = width - channelTruncated.length - 5
var fill = (fillWidth > 0) ? new Array(fillWidth).fill(' ').join('') : ''
if (isPrivate) return ' ' + chalk.whiteBright(chalk.bgMagenta(numPrefix + channelTruncated + fill))
if (state.selectedWindowPane === 'channels') {
return ' ' + chalk.whiteBright(chalk.bgBlue(numPrefix + channelTruncated + fill))
} else {
return ' ' + chalk.bgBlue(numPrefix + channelTruncated + fill)
}
} else {
if (mentioned) return ' ' + numPrefix + '@' + chalk.magenta(channelTruncated)
else if (unread) return ' ' + numPrefix + '*' + chalk.green(channelTruncated)
else if (isPrivate) return ' ' + numPrefix + chalk.cyan(channelTruncated)
else return ' ' + numPrefix + channelTruncated
}
}).slice(0, height)
}
function renderVerticalLine (chr, height, chlk) {
return new Array(height).fill(chlk ? chlk(chr) : chr)
}
function renderHorizontalLine (chr, width, chlk) {
var txt = new Array(width).fill(chr).join('')
if (chlk) txt = chlk(txt)
return [txt]
}
function getPrintedName (user) {
if (user && user.name) return user.name
else return user.key.slice(0, 8)
}
function renderNicks (state, width, height) {
// All known users
var users = state.cabal.getChannelMembers()
const currentChannel = state.cabal.getCurrentChannel()
users = Object.keys(users)
.map(key => users[key])
.sort(util.cmpUser)
// Count how many occurances of same nickname there are
const onlineNickCount = {}
const offlineNickCount = {}
users.forEach(user => {
const name = getPrintedName(user)
if (user.online) onlineNickCount[name] = name in onlineNickCount ? onlineNickCount[name] + 1 : 1
else offlineNickCount[name] = name in offlineNickCount ? offlineNickCount[name] + 1 : 1
})
// Format and colorize names
const seen = {}
const formattedNicks = users
.filter(user => {
const name = getPrintedName(user)
if (seen[name]) return false
seen[name] = true
return true
})
.map(user => {
const name = getPrintedName(user)
let outputName
// Duplicate nick count
const duplicates = user.online ? onlineNickCount[name] : offlineNickCount[name]
const dupecountStr = `(${duplicates})`
const modSigilLength = (user.isAdmin(currentChannel) || user.isModerator(currentChannel) || user.isHidden(currentChannel)) ? 1 : 0
outputName = util.sanitizeString(name).slice(0, width - modSigilLength)
if (duplicates > 1) outputName = outputName.slice(0, width - dupecountStr.length - 2 - modSigilLength)
// Colorize
let colorizedName = outputName.slice()
if (user.isAdmin(currentChannel)) colorizedName = chalk.green('@') + colorizedName
else if (user.isModerator(currentChannel)) colorizedName = chalk.green('%') + colorizedName
else if (user.isHidden(currentChannel)) colorizedName = chalk.green('-') + colorizedName
if (user.online) {
colorizedName = chalk.bold(colorizedName)
}
if (duplicates > 1) colorizedName += ' ' + chalk.green(dupecountStr)
return colorizedName
})
// Scrolling Rendering
state.userScrollback = Math.min(state.userScrollback, formattedNicks.length - height)
if (formattedNicks.length < height) state.userScrollback = 0
var nickBlock = formattedNicks.slice(state.userScrollback, state.userScrollback + height)
return nickBlock
}
function renderChannelTopic (state, width, height) {
var topic = state.topic || state.channel
var line = topic ? '➤ ' + topic : ''
line = line.substring(0, width - 1)
if (line.length === width - 1) {
line = line.substring(0, line.length - 1) + '…'
}
line = line + new Array(width - line.length - 1).fill(' ').join('')
const isPrivate = state.cabal.isChannelPrivate(state.cabal.channel)
// visually distinguish private channel from all other channels
if (isPrivate) { return [chalk.whiteBright(chalk.bgMagenta(line))] } else { return [chalk.whiteBright(chalk.bgBlue(line))] }
}
function renderMessages (state, width, height) {
var msgs = state.messages
// Character-wrap to area edge
var allLines = msgs.reduce(function (accum, msg) {
// Status message
if (!msg.timestamp) {
// TODO(kira): These don't wrap yet & ought to!
accum.push(' * ' + msg.formatted)
return accum
}
const indent = util.strwidth(msg.formattedPrefix)
const lines = util.wrapAnsi(msg.content, width - indent)
if (lines.length === 0) return accum
const firstLine = msg.formattedPrefix + lines[0]
accum.push(firstLine)
const paddedLines = lines.slice(1).map(line => ' '.repeat(indent) + line.trim())
accum = accum.concat(paddedLines)
return accum
}, [])
// Scrollable Content
state.messageScrollback = Math.min(state.messageScrollback, allLines.length - height)
if (allLines.length < height) {
state.messageScrollback = 0
}
var lines = (allLines.length < height)
? allLines.concat(Array(height - allLines.length).fill(''))
: allLines.slice(
allLines.length - height - state.messageScrollback,
allLines.length - state.messageScrollback
)
if (state.messageScrollback > 0) {
lines = lines.slice(0, lines.length - 1).concat(['More messages below...'])
}
return lines
}