@@ -0,0 +1,449 @@
|
||||
/** TEA IRC TUI. Cell buffer; protocol lives in irc-client.js. */
|
||||
|
||||
function bareIrcFmtClock(ms) {
|
||||
var d = new Date(typeof ms === 'number' ? ms : Date.now())
|
||||
function z(n) {
|
||||
return (n < 10 ? '0' : '') + n
|
||||
}
|
||||
return z(d.getHours()) + ':' + z(d.getMinutes())
|
||||
}
|
||||
|
||||
function bareIrcCreateTuiApp(ctx, client) {
|
||||
var tui = ctx.tui
|
||||
var size0 = tui && typeof tui.size === 'function' ? tui.size() : {}
|
||||
return {
|
||||
client: client,
|
||||
width: size0.width || 80,
|
||||
height: size0.height || 24,
|
||||
help: false,
|
||||
quitConfirm: false,
|
||||
filterOpen: false,
|
||||
status: 'connecting…',
|
||||
input: tui.textinput.create({ prompt: '', focused: true, charLimit: 450 }),
|
||||
viewport: tui.viewport.create({
|
||||
width: size0.width || 80,
|
||||
height: Math.max(6, (size0.height || 24) - 6)
|
||||
}),
|
||||
init: function () {
|
||||
var self = this
|
||||
client.on('registered', function () {
|
||||
if (tui && typeof tui.send === 'function')
|
||||
tui.send({ type: 'irc.paint' })
|
||||
})
|
||||
client.on('privmsg', function () {
|
||||
if (tui && typeof tui.send === 'function')
|
||||
tui.send({ type: 'irc.paint' })
|
||||
})
|
||||
client.on('notice', function () {
|
||||
if (tui && typeof tui.send === 'function')
|
||||
tui.send({ type: 'irc.paint' })
|
||||
})
|
||||
client.on('join', function () {
|
||||
if (tui && typeof tui.send === 'function')
|
||||
tui.send({ type: 'irc.paint' })
|
||||
})
|
||||
client.on('sasl', function (p) {
|
||||
self.status = p && p.ok ? 'sasl ok' : 'sasl failed'
|
||||
if (tui && typeof tui.send === 'function')
|
||||
tui.send({ type: 'irc.paint' })
|
||||
})
|
||||
client.on('error', function (p) {
|
||||
self.status =
|
||||
'error: ' +
|
||||
((p && (p.text || (p.error && p.error.message))) || 'error')
|
||||
if (tui && typeof tui.send === 'function')
|
||||
tui.send({ type: 'irc.paint' })
|
||||
})
|
||||
client.on('close', function () {
|
||||
self.status = 'disconnected'
|
||||
if (tui && typeof tui.send === 'function')
|
||||
tui.send({ type: 'irc.paint' })
|
||||
})
|
||||
this._sync()
|
||||
return null
|
||||
},
|
||||
_current: function () {
|
||||
var st = this.client.state
|
||||
return (
|
||||
st.buffers[st.currentId] || bareIrcEnsureBuffer(st, 'server', '*server')
|
||||
)
|
||||
},
|
||||
_sync: function () {
|
||||
var buf = this._current()
|
||||
buf.unread = 0
|
||||
buf.highlight = false
|
||||
var cols = Math.max(40, this.width || 80)
|
||||
var nickW = cols >= 100 ? 16 : 0
|
||||
this.viewport.width = Math.max(20, cols - nickW - 2)
|
||||
this.viewport.height = Math.max(4, (this.height || 24) - 6)
|
||||
var lines = []
|
||||
for (var i = 0; i < buf.lines.length; i++) {
|
||||
var rec = buf.lines[i]
|
||||
var clock = bareIrcFmtClock(rec.atMs)
|
||||
if (rec.kind === 'action') {
|
||||
lines.push(clock + ' * ' + rec.from + ' ' + rec.text)
|
||||
} else if (rec.kind === 'notice') {
|
||||
lines.push(clock + ' -' + rec.from + '- ' + rec.text)
|
||||
} else if (rec.kind === 'privmsg') {
|
||||
lines.push(clock + ' <' + rec.from + '> ' + rec.text)
|
||||
} else {
|
||||
lines.push(clock + ' ' + (rec.text || rec.command || ''))
|
||||
}
|
||||
}
|
||||
this.viewport.setContent(lines.join('\n'))
|
||||
this.viewport.gotoBottom()
|
||||
},
|
||||
_nextBuffer: function (dir) {
|
||||
var list = bareIrcListBuffers(this.client.state)
|
||||
if (!list.length) return
|
||||
var cur = 0
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].id === this.client.state.currentId) cur = i
|
||||
}
|
||||
var n = (cur + dir + list.length) % list.length
|
||||
this.client.state.currentId = list[n].id
|
||||
this._sync()
|
||||
},
|
||||
_completeNick: function () {
|
||||
var buf = this._current()
|
||||
var val = this.input.value || ''
|
||||
var m = /(?:^|\s)(\S+)$/.exec(val)
|
||||
var prefix = m ? m[1] : ''
|
||||
if (!prefix) return
|
||||
var mapping = bareIrcCasemapping(this.client.state.isupport)
|
||||
var keys = Object.keys(buf.members || {})
|
||||
var hit = ''
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var mem = buf.members[keys[i]]
|
||||
var nick = mem && mem.nick ? mem.nick : keys[i]
|
||||
if (
|
||||
bareIrcCasemap(nick, mapping).indexOf(
|
||||
bareIrcCasemap(prefix, mapping)
|
||||
) === 0
|
||||
) {
|
||||
hit = nick
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hit) return
|
||||
this.input.setValue(val.slice(0, val.length - prefix.length) + hit)
|
||||
},
|
||||
_runSlash: function (line) {
|
||||
var parts = line.slice(1).split(/\s+/)
|
||||
var cmd = (parts[0] || '').toLowerCase()
|
||||
var rest = line.slice(1 + (parts[0] || '').length).trim()
|
||||
var buf = this._current()
|
||||
if (cmd === 'join' && parts[1]) this.client.join(parts[1], parts[2])
|
||||
else if (cmd === 'part')
|
||||
this.client.part(parts[1] || buf.name, rest.replace(/^\S+\s*/, ''))
|
||||
else if (cmd === 'query' && parts[1]) {
|
||||
var q = bareIrcEnsureBuffer(this.client.state, 'query', parts[1])
|
||||
this.client.state.currentId = q.id
|
||||
if (parts.slice(2).join(' ')) {
|
||||
this.client.sendPrivmsg(parts[1], parts.slice(2).join(' '))
|
||||
}
|
||||
} else if ((cmd === 'msg' || cmd === 'privmsg') && parts[1]) {
|
||||
this.client.sendPrivmsg(parts[1], parts.slice(2).join(' '))
|
||||
} else if (cmd === 'notice' && parts[1]) {
|
||||
this.client.sendRaw('NOTICE', [parts[1], parts.slice(2).join(' ')])
|
||||
} else if (cmd === 'nick' && parts[1])
|
||||
this.client.sendRaw('NICK', [parts[1]])
|
||||
else if (cmd === 'whois' && parts[1])
|
||||
this.client.sendRaw('WHOIS', [parts[1]])
|
||||
else if (cmd === 'who' && parts[1]) this.client.sendRaw('WHO', [parts[1]])
|
||||
else if (cmd === 'topic') {
|
||||
if (rest) this.client.sendRaw('TOPIC', [buf.name, rest])
|
||||
else this.client.sendRaw('TOPIC', [buf.name])
|
||||
} else if (cmd === 'me' && rest) {
|
||||
this.client.sendPrivmsg(buf.name, '\x01ACTION ' + rest + '\x01')
|
||||
} else if (cmd === 'away') this.client.sendRaw('AWAY', rest ? [rest] : [])
|
||||
else if (cmd === 'quit') {
|
||||
this.client.quit(rest || 'Leaving')
|
||||
return tui.quit
|
||||
} else if (cmd === 'quote' && rest) {
|
||||
var qparts = rest.split(/\s+/)
|
||||
this.client.sendRaw(qparts[0], qparts.slice(1))
|
||||
} else if (cmd === 'disconnect') this.client.quit('Disconnect')
|
||||
else if (cmd === 'reconnect') {
|
||||
try {
|
||||
if (typeof ctx.bareOsTlsConnect === 'function' && this.client.opts) {
|
||||
var spec = this.client.opts
|
||||
var sock =
|
||||
spec.tls === false
|
||||
? ctx.bareOsTelnetConnect(spec.host, spec.port, {})
|
||||
: ctx.bareOsTlsConnect(spec.host, spec.port, {
|
||||
servername: spec.host,
|
||||
rejectUnauthorized: true
|
||||
})
|
||||
this.client.attach(sock)
|
||||
this.status = 'reconnecting\u2026'
|
||||
}
|
||||
} catch (err) {
|
||||
this.status = 'reconnect failed'
|
||||
}
|
||||
} else if (cmd === 'win' || cmd === 'buffer') {
|
||||
var n = parseInt(parts[1], 10)
|
||||
var list = bareIrcListBuffers(this.client.state)
|
||||
if (n >= 1 && n <= list.length) {
|
||||
this.client.state.currentId = list[n - 1].id
|
||||
}
|
||||
} else if (cmd === 'p2p' && parts[1] === 'open' && parts[2]) {
|
||||
var name = String(parts[2])
|
||||
if (name.indexOf('[p2p]') < 0) name = name + ' [p2p]'
|
||||
var pb = bareIrcEnsureBuffer(this.client.state, 'p2p', name)
|
||||
this.client.state.currentId = pb.id
|
||||
bareIrcPushLine(this.client.state, pb, {
|
||||
atMs: Date.now(),
|
||||
kind: 'info',
|
||||
text: 'Shadow room — not Libera. Messages stay on the swarm sideband.'
|
||||
})
|
||||
if (typeof ctx.bareOsIrcShadowOpen === 'function') {
|
||||
try {
|
||||
ctx.bareOsIrcShadowOpen(parts[2])
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} else if (cmd === 'help') {
|
||||
this.help = true
|
||||
} else {
|
||||
bareIrcPushLine(this.client.state, buf, {
|
||||
atMs: Date.now(),
|
||||
kind: 'info',
|
||||
text: 'unknown command /' + cmd
|
||||
})
|
||||
}
|
||||
this._sync()
|
||||
return null
|
||||
},
|
||||
update: function (msg) {
|
||||
if (msg && msg.type === 'resize') {
|
||||
this.width = msg.width || this.width
|
||||
this.height = msg.height || this.height
|
||||
this._sync()
|
||||
return [this, null]
|
||||
}
|
||||
if (msg && msg.type === 'irc.paint') {
|
||||
this.status = this.client.state.registered
|
||||
? this.client.state.saslOk
|
||||
? 'sasl ok'
|
||||
: 'registered'
|
||||
: this.client.state.connection
|
||||
this._sync()
|
||||
return [this, null]
|
||||
}
|
||||
if (this.help) {
|
||||
if (msg && msg.type === 'key') this.help = false
|
||||
return [this, null]
|
||||
}
|
||||
if (this.quitConfirm) {
|
||||
if (tui.key.matches(msg, 'y', 'Y')) {
|
||||
this.client.quit('Quit')
|
||||
return [this, tui.quit]
|
||||
}
|
||||
if (msg && msg.type === 'key') this.quitConfirm = false
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'ctrl+c')) {
|
||||
this.quitConfirm = true
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'f10')) {
|
||||
this.client.quit('Quit')
|
||||
return [this, tui.quit]
|
||||
}
|
||||
if (tui.key.matches(msg, '?')) {
|
||||
this.help = true
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'ctrl+n', 'alt+right')) {
|
||||
this._nextBuffer(1)
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'ctrl+p', 'alt+left')) {
|
||||
this._nextBuffer(-1)
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'tab')) {
|
||||
this._completeNick()
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'pageup', 'pagedown', 'up', 'down')) {
|
||||
var vp = this.viewport.update(msg)
|
||||
this.viewport = vp[0]
|
||||
return [this, vp[1]]
|
||||
}
|
||||
if (tui.key.matches(msg, 'enter')) {
|
||||
var line = String(this.input.value || '').trim()
|
||||
this.input.reset()
|
||||
if (!line) return [this, null]
|
||||
if (line.charAt(0) === '/') {
|
||||
var cmd = this._runSlash(line)
|
||||
return [this, cmd]
|
||||
}
|
||||
var cur = this._current()
|
||||
if (cur.kind === 'server') {
|
||||
bareIrcPushLine(this.client.state, cur, {
|
||||
atMs: Date.now(),
|
||||
kind: 'info',
|
||||
text: 'join a channel or /query someone first'
|
||||
})
|
||||
} else if (cur.kind === 'p2p') {
|
||||
bareIrcPushLine(this.client.state, cur, {
|
||||
atMs: Date.now(),
|
||||
kind: 'privmsg',
|
||||
from: this.client.state.nick || 'me',
|
||||
text: line,
|
||||
local: true
|
||||
})
|
||||
if (typeof ctx.bareOsChatSend === 'function') {
|
||||
try {
|
||||
ctx.bareOsChatSend(
|
||||
'[bare-os-irc-shadow] ' + cur.name + ' ' + line
|
||||
)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.client.sendPrivmsg(cur.name, line)
|
||||
}
|
||||
this._sync()
|
||||
return [this, null]
|
||||
}
|
||||
var pair = this.input.update(msg)
|
||||
this.input = pair[0]
|
||||
return [this, pair[1]]
|
||||
},
|
||||
view: function () {
|
||||
var cols = Math.max(40, this.width || 80)
|
||||
var rows = Math.max(12, this.height || 24)
|
||||
var st = tui.style
|
||||
var buf = this._current()
|
||||
var net = this.client.state.network || 'irc'
|
||||
var tls = this.client.state.tls ? 'tls' : 'plain'
|
||||
var titleRaw =
|
||||
' ' +
|
||||
net +
|
||||
' ' +
|
||||
buf.name +
|
||||
(buf.unread ? ' ● ' + buf.unread : '') +
|
||||
' ' +
|
||||
tls +
|
||||
(this.client.state.saslOk ? ' sasl' : '') +
|
||||
' ' +
|
||||
this.status +
|
||||
' ' +
|
||||
cols +
|
||||
'x' +
|
||||
rows +
|
||||
' '
|
||||
var title = st
|
||||
? st()
|
||||
.foreground('brightwhite')
|
||||
.background('blue')
|
||||
.width(cols)
|
||||
.render(titleRaw)
|
||||
: titleRaw
|
||||
var tabs = bareIrcListBuffers(this.client.state)
|
||||
.map(function (b) {
|
||||
var mark = b.id === buf.id ? '[' + b.name + ']' : b.name
|
||||
if (b.unread) mark += '(' + b.unread + ')'
|
||||
return mark
|
||||
})
|
||||
.join(' ')
|
||||
var tabBar = st ? st().dim().width(cols).render(tabs) : tabs
|
||||
var nickW = cols >= 100 ? 16 : 0
|
||||
var body = this.viewport.view()
|
||||
if (nickW) {
|
||||
var nicks = Object.keys(buf.members || {})
|
||||
.map(function (k) {
|
||||
var m = buf.members[k]
|
||||
var pref =
|
||||
m && m.modes && m.modes.indexOf('o') >= 0
|
||||
? '@'
|
||||
: m && m.modes && m.modes.indexOf('v') >= 0
|
||||
? '+'
|
||||
: ' '
|
||||
return pref + (m && m.nick ? m.nick : k)
|
||||
})
|
||||
.slice(0, this.viewport.height || 10)
|
||||
while (nicks.length < (this.viewport.height || 0)) nicks.push('')
|
||||
var left = String(body).split('\n')
|
||||
var joined = []
|
||||
var h = Math.max(left.length, nicks.length)
|
||||
for (var r = 0; r < h; r++) {
|
||||
var L = st
|
||||
? st.truncate(left[r] || '', cols - nickW - 1)
|
||||
: (left[r] || '').slice(0, cols - nickW - 1)
|
||||
var R = st
|
||||
? st.truncate(nicks[r] || '', nickW)
|
||||
: (nicks[r] || '').slice(0, nickW)
|
||||
joined.push(L + ' ' + R)
|
||||
}
|
||||
body = joined.join('\n')
|
||||
}
|
||||
var prompt =
|
||||
(buf.kind === 'p2p' ? '#p2p ' : '') +
|
||||
'[' +
|
||||
buf.name +
|
||||
'] ' +
|
||||
(this.input.view ? this.input.view() : this.input.value || '')
|
||||
var footRaw =
|
||||
'Enter send /join /query /quit Ctrl+n/p buffers Tab nick ? help'
|
||||
var foot = st ? st().dim().width(cols).render(footRaw) : footRaw
|
||||
var rule = st
|
||||
? st()
|
||||
.dim()
|
||||
.width(cols)
|
||||
.render('\u2500'.repeat(Math.min(cols, 120)))
|
||||
: ''
|
||||
var lines = [title, tabBar, rule]
|
||||
.concat(String(body).split('\n'))
|
||||
.concat([rule, prompt, foot])
|
||||
while (lines.length < rows) lines.push('')
|
||||
var out = []
|
||||
for (var i = 0; i < rows; i++) {
|
||||
out.push(
|
||||
st
|
||||
? st.truncate(lines[i] || '', cols)
|
||||
: String(lines[i] || '').slice(0, cols)
|
||||
)
|
||||
}
|
||||
return out.join('\n')
|
||||
},
|
||||
overlay: function (size) {
|
||||
if (!this.help && !this.quitConfirm) return null
|
||||
var cols = (size && size.width) || this.width || 80
|
||||
var rows = (size && size.height) || this.height || 24
|
||||
var st = tui.style
|
||||
var body = this.quitConfirm
|
||||
? 'Disconnect and quit?\n\nY yes any other key cancel'
|
||||
: 'Bare OS irc\n\n' +
|
||||
'/join #chan /part /query nick /msg nick text\n' +
|
||||
'/nick /whois /me /away /quote /quit\n' +
|
||||
'/p2p open #chan shadow room (not Libera)\n' +
|
||||
'Ctrl+n / Ctrl+p next buffer Tab complete\n\n' +
|
||||
'Default network: irc.libera.chat:6697 TLS\n' +
|
||||
'Press any key to close.'
|
||||
var boxed = st
|
||||
? st()
|
||||
.border(st.borders.rounded)
|
||||
.padding(1, 2)
|
||||
.background('black')
|
||||
.render(body)
|
||||
: body
|
||||
var h = st ? st.height(boxed) : boxed.split('\n').length
|
||||
var w = st ? st.width(boxed) : 48
|
||||
return {
|
||||
row: Math.max(0, Math.floor((rows - h) / 2)),
|
||||
col: Math.max(0, Math.floor((cols - w) / 2)),
|
||||
text: boxed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bareIrcTuiRunOpts() {
|
||||
return { buffer: 'cell' }
|
||||
}
|
||||
Reference in New Issue
Block a user