xK/xP/public/xP.js

257 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
// --- Colours -----------------------------------------------------------------
let palette = [
'#000', '#800', '#080', '#880', '#008', '#808', '#088', '#ccc',
'#888', '#f00', '#0f0', '#ff0', '#00f', '#f0f', '#0ff', '#fff',
]
palette.length = 256
for (let i = 0; i < 216; i++) {
let r = i / 36 >> 0, g = (i / 6 >> 0) % 6, b = i % 6
r = !r ? '00' : (55 + 40 * r).toString(16)
g = !g ? '00' : (55 + 40 * g).toString(16)
b = !b ? '00' : (55 + 40 * b).toString(16)
palette[16 + i] = `#${r}${g}${b}`
}
for (let i = 0; i < 24; i++) {
let g = ('0' + (8 + i * 10).toString(16)).slice(-2)
palette[232 + i] = `#${g}${g}${g}`
}
function applyColor(fg, bg, inverse) {
if (inverse)
[fg, bg] = [bg >= 0 ? bg : 15, fg >= 0 ? fg : 0]
let style = ''
if (fg >= 0)
style += `color: ${palette[fg]};`
if (bg >= 0)
style += `background-color: ${palette[bg]};`
if (style)
return style
}
// ---- Event processing -------------------------------------------------------
// TODO: Probably reset state on disconnect, and indicate to user.
let socket = new WebSocket(proxy)
let commandSeq = 0
function send(command) {
socket.send(JSON.stringify({commandSeq: ++commandSeq, data: command}))
}
socket.onopen = function(event) {
send({command: 'Hello', version: 1})
}
let buffers = new Map()
let bufferCurrent = undefined
socket.onmessage = function(event) {
console.log(event.data)
let e = JSON.parse(event.data).data
switch (e.event) {
case 'BufferUpdate':
{
let b = buffers.get(e.bufferName)
if (b === undefined) {
b = {lines: []}
buffers.set(e.bufferName, b)
}
// TODO: Update any buffer properties.
break
}
case 'BufferRename':
buffers.set(e.new, buffers.get(e.bufferName))
buffers.delete(e.bufferName)
break
case 'BufferRemove':
buffers.delete(e.bufferName)
break
case 'BufferActivate':
bufferCurrent = e.bufferName
// TODO: Somehow scroll to the end of it immediately.
// TODO: Focus the textarea.
break
case 'BufferLine':
{
let b = buffers.get(e.bufferName)
if (b !== undefined)
b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
break
}
case 'BufferClear':
{
let b = buffers.get(e.bufferName)
if (b !== undefined)
b.lines.length = 0
break
}
}
m.redraw()
}
// ---- UI ---------------------------------------------------------------------
let BufferList = {
view: vnode => {
let items = []
buffers.forEach((b, name) => {
let attrs = {
onclick: e => {
send({command: 'BufferActivate', bufferName: name})
},
}
if (name == bufferCurrent)
attrs.class = 'active'
items.push(m('.item', attrs, name))
})
return m('.list', {}, items)
},
}
function linkify(text, attrs, a) {
let re = new RegExp([
/https?:\/\//,
/([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/,
/[^\[\](){}<>"'\s,.:]/,
].map(r => r.source).join(''), 'g')
let end = 0, match
while ((match = re.exec(text)) !== null) {
if (end < match.index)
a.push(m('span', attrs, text.substring(end, match.index)))
a.push(m('a', {href: match[0], ...attrs}, match[0]))
end = re.lastIndex
}
if (end < text.length)
a.push(m('span', attrs, text.substring(end)))
}
let Content = {
view: vnode => {
let line = vnode.children[0]
let content = []
switch (line.rendition) {
case 'Indent': content.push(m('span.mark', {}, '')); break
case 'Status': content.push(m('span.mark', {}, '')); break
case 'Error': content.push(m('span.mark.error', {}, '⚠')); break
case 'Join': content.push(m('span.mark.join', {}, '→')); break
case 'Part': content.push(m('span.mark.part', {}, '←')); break
case 'Action': content.push(m('span.mark.action', {}, '✶')); break
}
let classes = new Set()
let flip = c => {
if (classes.has(c))
classes.delete(c)
else
classes.add(c)
}
let fg = -1, bg = -1, inverse = false
line.items.forEach(item => {
switch (item.kind) {
case 'Text':
linkify(item.text, {
class: Array.from(classes.keys()).join(' '),
style: applyColor(fg, bg, inverse),
}, content)
break
case 'Reset':
classes.clear()
fg = bg = -1
inverse = false
break
case 'FgColor':
fg = item.color
break
case 'BgColor':
bg = item.color
break
case 'FlipInverse':
inverse = !inverse
break
case 'FlipBold': flip('b'); break
case 'FlipItalic': flip('i'); break
case 'FlipUnderline': flip('u'); break
case 'FlipCrossedOut': flip('s'); break
case 'FlipMonospace': flip('m'); break
}
})
return m('.content', {}, content)
},
}
let Buffer = {
view: vnode => {
let lines = []
let b = buffers.get(bufferCurrent)
if (b === undefined)
return
let lastDateMark = undefined
b.lines.forEach(line => {
let date = new Date(line.when)
let dateMark = date.toLocaleDateString()
if (dateMark !== lastDateMark) {
lines.push(m('.date', {}, dateMark))
lastDateMark = dateMark
}
lines.push(m('.time', {}, date.toLocaleTimeString()))
lines.push(m(Content, {}, line))
})
return m('.buffer-container', {}, [
m('.filler'),
m('.buffer', {}, lines),
])
},
}
// TODO: This should be remembered across buffer switches,
// and we'll probably have to intercept /all/ key presses.
let Input = {
view: vnode => {
return m('textarea', {
rows: 1,
onkeydown: e => {
// TODO: And perhaps on other actions, too.
send({command: 'Active'})
if (e.keyCode !== 13)
return
send({
command: 'BufferInput',
bufferName: bufferCurrent,
text: e.currentTarget.value,
})
e.preventDefault()
e.currentTarget.value = ''
},
})
},
}
let Main = {
view: vnode => {
return m('.xP', {}, [
m('.title', {}, "xP"),
m('.middle', {}, [m(BufferList), m(Buffer)]),
m('.status', {}, bufferCurrent),
m(Input),
])
},
}
// TODO: Buffer names should work as routes.
window.addEventListener('load', () => {
m.route(document.body, '/', {
'/': Main,
})
})