2024-11-14 16:27:26 +01:00
|
|
|
|
// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
2022-09-05 22:34:20 +02:00
|
|
|
|
// SPDX-License-Identifier: 0BSD
|
2022-09-15 22:45:14 +02:00
|
|
|
|
import * as Relay from './proto.js'
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
|
|
|
|
// ---- RPC --------------------------------------------------------------------
|
|
|
|
|
|
2022-09-21 14:22:47 +02:00
|
|
|
|
class RelayRPC extends EventTarget {
|
2022-09-06 17:17:32 +02:00
|
|
|
|
constructor(url) {
|
|
|
|
|
super()
|
|
|
|
|
this.url = url
|
|
|
|
|
this.commandSeq = 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connect() {
|
|
|
|
|
// We can't close the connection immediately, as that queues a task.
|
|
|
|
|
if (this.ws !== undefined)
|
|
|
|
|
throw "Already connecting or connected"
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
let ws = this.ws = new WebSocket(this.url)
|
|
|
|
|
ws.onopen = event => {
|
|
|
|
|
this._initialize()
|
|
|
|
|
resolve()
|
|
|
|
|
}
|
|
|
|
|
// It's going to be code 1006 with no further info.
|
|
|
|
|
ws.onclose = event => {
|
|
|
|
|
this.ws = undefined
|
2022-09-06 20:17:23 +02:00
|
|
|
|
reject()
|
2022-09-06 17:17:32 +02:00
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_initialize() {
|
2022-09-15 22:45:14 +02:00
|
|
|
|
this.ws.binaryType = 'arraybuffer'
|
2022-09-06 17:17:32 +02:00
|
|
|
|
this.ws.onopen = undefined
|
|
|
|
|
this.ws.onmessage = event => {
|
|
|
|
|
this._process(event.data)
|
|
|
|
|
}
|
|
|
|
|
this.ws.onerror = event => {
|
|
|
|
|
this.dispatchEvent(new CustomEvent('error'))
|
|
|
|
|
}
|
|
|
|
|
this.ws.onclose = event => {
|
|
|
|
|
let message = "Connection closed: " +
|
2022-09-16 03:18:53 +02:00
|
|
|
|
event.reason + " (" + event.code + ")"
|
2022-09-06 17:17:32 +02:00
|
|
|
|
for (const seq in this.promised)
|
|
|
|
|
this.promised[seq].reject(message)
|
|
|
|
|
|
|
|
|
|
this.ws = undefined
|
|
|
|
|
this.dispatchEvent(new CustomEvent('close', {
|
|
|
|
|
detail: {message, code: event.code, reason: event.reason},
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// Now connect() can be called again.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.promised = {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_process(data) {
|
2022-09-15 22:45:14 +02:00
|
|
|
|
if (typeof data === 'string')
|
|
|
|
|
throw "JSON messages not supported"
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
const r = new Relay.Reader(data)
|
|
|
|
|
while (!r.empty)
|
|
|
|
|
this._processOne(Relay.EventMessage.deserialize(r))
|
|
|
|
|
}
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
_processOne(message) {
|
|
|
|
|
let e = message.data
|
2022-09-06 17:17:32 +02:00
|
|
|
|
switch (e.event) {
|
2022-09-15 22:45:14 +02:00
|
|
|
|
case Relay.Event.Error:
|
2022-09-06 17:17:32 +02:00
|
|
|
|
if (this.promised[e.commandSeq] !== undefined)
|
|
|
|
|
this.promised[e.commandSeq].reject(e.error)
|
|
|
|
|
else
|
2022-09-21 14:22:47 +02:00
|
|
|
|
console.error(`Unawaited error: ${e.error}`)
|
2022-09-06 17:17:32 +02:00
|
|
|
|
break
|
2022-09-15 22:45:14 +02:00
|
|
|
|
case Relay.Event.Response:
|
2022-09-06 17:17:32 +02:00
|
|
|
|
if (this.promised[e.commandSeq] !== undefined)
|
|
|
|
|
this.promised[e.commandSeq].resolve(e.data)
|
|
|
|
|
else
|
|
|
|
|
console.error("Unawaited response")
|
|
|
|
|
break
|
|
|
|
|
default:
|
2022-09-13 06:01:08 +02:00
|
|
|
|
e.eventSeq = message.eventSeq
|
2022-09-14 01:57:02 +02:00
|
|
|
|
this.dispatchEvent(new CustomEvent('event', {detail: e}))
|
2022-09-06 17:17:32 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete this.promised[e.commandSeq]
|
|
|
|
|
for (const seq in this.promised) {
|
|
|
|
|
// We don't particularly care about wraparound issues.
|
|
|
|
|
if (seq >= e.commandSeq)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
this.promised[seq].reject("No response")
|
|
|
|
|
delete this.promised[seq]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
send(params) {
|
|
|
|
|
if (this.ws === undefined)
|
|
|
|
|
throw "Not connected"
|
|
|
|
|
if (typeof params !== 'object')
|
|
|
|
|
throw "Method parameters must be an object"
|
|
|
|
|
|
2022-09-10 16:42:49 +02:00
|
|
|
|
// Left shifts in Javascript convert to a 32-bit signed number.
|
2022-09-06 17:17:32 +02:00
|
|
|
|
let seq = ++this.commandSeq
|
2022-09-10 16:42:49 +02:00
|
|
|
|
if ((seq << 0) != seq)
|
2022-09-06 17:17:32 +02:00
|
|
|
|
seq = this.commandSeq = 0
|
|
|
|
|
|
|
|
|
|
this.ws.send(JSON.stringify({commandSeq: seq, data: params}))
|
2022-09-21 14:22:47 +02:00
|
|
|
|
|
|
|
|
|
// Automagically detect if we want a result.
|
|
|
|
|
let data = undefined
|
|
|
|
|
const promise = new Promise(
|
|
|
|
|
(resolve, reject) => { data = {resolve, reject} })
|
|
|
|
|
promise.then = (...args) => {
|
|
|
|
|
this.promised[seq] = data
|
|
|
|
|
return Promise.prototype.then.call(promise, ...args)
|
|
|
|
|
}
|
|
|
|
|
return promise
|
2022-09-06 17:17:32 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-11 03:38:33 +02:00
|
|
|
|
// ---- Utilities --------------------------------------------------------------
|
|
|
|
|
|
2022-09-11 15:54:39 +02:00
|
|
|
|
function utf8Encode(s) { return new TextEncoder().encode(s) }
|
|
|
|
|
function utf8Decode(s) { return new TextDecoder().decode(s) }
|
|
|
|
|
|
2022-09-11 03:38:33 +02:00
|
|
|
|
function hasShortcutModifiers(event) {
|
2022-09-12 16:43:13 +02:00
|
|
|
|
return (event.altKey || event.escapePrefix) &&
|
|
|
|
|
!event.metaKey && !event.ctrlKey
|
2022-09-11 03:38:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const audioContext = new AudioContext()
|
|
|
|
|
|
|
|
|
|
function beep() {
|
|
|
|
|
let gain = audioContext.createGain()
|
|
|
|
|
gain.gain.value = 0.5
|
|
|
|
|
gain.connect(audioContext.destination)
|
|
|
|
|
|
|
|
|
|
let oscillator = audioContext.createOscillator()
|
|
|
|
|
oscillator.type = "triangle"
|
|
|
|
|
oscillator.frequency.value = 800
|
|
|
|
|
oscillator.connect(gain)
|
|
|
|
|
oscillator.start(audioContext.currentTime)
|
|
|
|
|
oscillator.stop(audioContext.currentTime + 0.1)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-11 19:10:09 +02:00
|
|
|
|
let iconLink = undefined
|
|
|
|
|
let iconState = undefined
|
|
|
|
|
|
|
|
|
|
function updateIcon(highlighted) {
|
|
|
|
|
if (iconState === highlighted)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
iconState = highlighted
|
|
|
|
|
let canvas = document.createElement('canvas')
|
|
|
|
|
canvas.width = 32
|
|
|
|
|
canvas.height = 32
|
|
|
|
|
|
|
|
|
|
let ctx = canvas.getContext('2d')
|
|
|
|
|
ctx.arc(16, 16, 12, 0, 2 * Math.PI)
|
2022-09-12 03:48:12 +02:00
|
|
|
|
ctx.fillStyle = '#000'
|
|
|
|
|
if (highlighted === true)
|
|
|
|
|
ctx.fillStyle = '#ff5f00'
|
|
|
|
|
if (highlighted === false)
|
|
|
|
|
ctx.fillStyle = '#ccc'
|
2022-09-11 19:10:09 +02:00
|
|
|
|
ctx.fill()
|
|
|
|
|
|
|
|
|
|
if (iconLink === undefined) {
|
|
|
|
|
iconLink = document.createElement('link')
|
|
|
|
|
iconLink.type = 'image/png'
|
|
|
|
|
iconLink.rel = 'icon'
|
|
|
|
|
document.getElementsByTagName('head')[0].appendChild(iconLink)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iconLink.href = canvas.toDataURL();
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-06 17:17:32 +02:00
|
|
|
|
// ---- Event processing -------------------------------------------------------
|
|
|
|
|
|
2022-09-21 14:22:47 +02:00
|
|
|
|
let rpc = new RelayRPC(proxy)
|
2022-09-15 22:45:14 +02:00
|
|
|
|
let rpcEventHandlers = new Map()
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
|
|
|
|
let buffers = new Map()
|
2022-09-10 19:33:39 +02:00
|
|
|
|
let bufferLast = undefined
|
2022-09-06 17:17:32 +02:00
|
|
|
|
let bufferCurrent = undefined
|
2022-09-06 23:37:06 +02:00
|
|
|
|
let bufferLog = undefined
|
2022-09-07 13:52:30 +02:00
|
|
|
|
let bufferAutoscroll = true
|
2022-09-06 23:37:06 +02:00
|
|
|
|
|
2022-09-11 20:46:35 +02:00
|
|
|
|
let servers = new Map()
|
|
|
|
|
|
2022-09-10 20:38:32 +02:00
|
|
|
|
function bufferResetStats(b) {
|
2022-09-10 17:37:19 +02:00
|
|
|
|
b.newMessages = 0
|
|
|
|
|
b.newUnimportantMessages = 0
|
|
|
|
|
b.highlighted = false
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-06 21:07:03 +01:00
|
|
|
|
function bufferPopExcessLines(b) {
|
|
|
|
|
// Let "new" messages be, if only because pulling the log file
|
|
|
|
|
// is much more problematic in the web browser than in xC.
|
|
|
|
|
// TODO: Make the limit configurable, or extract general.backlog_limit.
|
|
|
|
|
const old = b.lines.length - b.newMessages - b.newUnimportantMessages
|
|
|
|
|
b.lines.splice(0, old - 1000)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 19:33:39 +02:00
|
|
|
|
function bufferActivate(name) {
|
|
|
|
|
rpc.send({command: 'BufferActivate', bufferName: name})
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-16 02:46:03 +02:00
|
|
|
|
function bufferToggleUnimportant(name) {
|
|
|
|
|
rpc.send({command: 'BufferToggleUnimportant', bufferName: name})
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-10 20:38:32 +02:00
|
|
|
|
function bufferToggleLog() {
|
2022-09-21 07:32:18 +02:00
|
|
|
|
// TODO: Try to restore the previous scroll offset.
|
2022-09-10 20:38:32 +02:00
|
|
|
|
if (bufferLog) {
|
|
|
|
|
setTimeout(() =>
|
|
|
|
|
document.getElementById('input')?.focus())
|
|
|
|
|
|
|
|
|
|
bufferLog = undefined
|
|
|
|
|
m.redraw()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let name = bufferCurrent
|
|
|
|
|
rpc.send({
|
|
|
|
|
command: 'BufferLog',
|
|
|
|
|
bufferName: name,
|
|
|
|
|
}).then(resp => {
|
|
|
|
|
if (bufferCurrent !== name)
|
|
|
|
|
return
|
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
bufferLog = utf8Decode(resp.log)
|
2022-09-10 20:38:32 +02:00
|
|
|
|
m.redraw()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-06 20:17:23 +02:00
|
|
|
|
let connecting = true
|
|
|
|
|
rpc.connect().then(result => {
|
|
|
|
|
buffers.clear()
|
2022-09-10 19:33:39 +02:00
|
|
|
|
bufferLast = undefined
|
2022-09-06 20:17:23 +02:00
|
|
|
|
bufferCurrent = undefined
|
2022-09-06 23:37:06 +02:00
|
|
|
|
bufferLog = undefined
|
2022-09-07 13:52:30 +02:00
|
|
|
|
bufferAutoscroll = true
|
2022-09-06 23:37:06 +02:00
|
|
|
|
|
2022-09-11 20:46:35 +02:00
|
|
|
|
servers.clear()
|
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpc.send({command: 'Hello', version: Relay.version})
|
2022-09-06 20:17:23 +02:00
|
|
|
|
connecting = false
|
|
|
|
|
m.redraw()
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
connecting = false
|
|
|
|
|
m.redraw()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
rpc.addEventListener('close', event => {
|
|
|
|
|
m.redraw()
|
|
|
|
|
})
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-14 01:57:02 +02:00
|
|
|
|
rpc.addEventListener('event', event => {
|
2022-09-15 22:45:14 +02:00
|
|
|
|
const handler = rpcEventHandlers.get(event.detail.event)
|
2022-09-14 01:57:02 +02:00
|
|
|
|
if (handler !== undefined) {
|
|
|
|
|
handler(event.detail)
|
2022-09-15 22:45:14 +02:00
|
|
|
|
if (bufferCurrent !== undefined ||
|
|
|
|
|
event.detail.event !== Relay.Event.BufferLine)
|
2022-09-14 01:57:02 +02:00
|
|
|
|
m.redraw()
|
|
|
|
|
}
|
2022-09-08 02:33:44 +02:00
|
|
|
|
})
|
|
|
|
|
|
2022-09-21 12:13:30 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.Ping, e => {
|
2022-09-14 01:57:02 +02:00
|
|
|
|
rpc.send({command: 'PingResponse', eventSeq: e.eventSeq})
|
2022-09-21 12:13:30 +02:00
|
|
|
|
})
|
2022-09-14 01:57:02 +02:00
|
|
|
|
|
2022-09-11 20:46:35 +02:00
|
|
|
|
// ~~~ Buffer events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
2022-09-21 12:13:30 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.BufferLine, e => {
|
|
|
|
|
let b = buffers.get(e.bufferName), line = {...e}
|
|
|
|
|
delete line.event
|
|
|
|
|
delete line.eventSeq
|
|
|
|
|
delete line.leakToActive
|
|
|
|
|
if (b === undefined)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
// Initial sync: skip all other processing, let highlights be.
|
|
|
|
|
if (bufferCurrent === undefined) {
|
|
|
|
|
b.lines.push(line)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let visible = document.visibilityState !== 'hidden' &&
|
|
|
|
|
bufferLog === undefined &&
|
|
|
|
|
bufferAutoscroll &&
|
|
|
|
|
(e.bufferName == bufferCurrent || e.leakToActive)
|
|
|
|
|
b.lines.push({...line})
|
|
|
|
|
if (!(visible || e.leakToActive) ||
|
|
|
|
|
b.newMessages || b.newUnimportantMessages) {
|
2022-09-28 16:25:16 +02:00
|
|
|
|
if (line.isUnimportant || e.leakToActive)
|
2022-09-21 12:13:30 +02:00
|
|
|
|
b.newUnimportantMessages++
|
|
|
|
|
else
|
|
|
|
|
b.newMessages++
|
|
|
|
|
}
|
2024-01-06 23:27:22 +01:00
|
|
|
|
|
|
|
|
|
// XXX: In its unkeyed diff algorithm, Mithril.js can only efficiently
|
|
|
|
|
// deal with common prefixes, i.e., indefinitely growing buffers.
|
|
|
|
|
// But we don't want to key all children of Buffer,
|
|
|
|
|
// so only trim buffers while they are, or once they become invisible.
|
|
|
|
|
if (e.bufferName != bufferCurrent)
|
|
|
|
|
bufferPopExcessLines(b)
|
2022-09-21 12:13:30 +02:00
|
|
|
|
|
|
|
|
|
if (e.leakToActive) {
|
|
|
|
|
let bc = buffers.get(bufferCurrent)
|
|
|
|
|
bc.lines.push({...line, leaked: true})
|
|
|
|
|
if (!visible || bc.newMessages || bc.newUnimportantMessages) {
|
|
|
|
|
if (line.isUnimportant)
|
|
|
|
|
bc.newUnimportantMessages++
|
|
|
|
|
else
|
|
|
|
|
bc.newMessages++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (line.isHighlight || (!visible && !line.isUnimportant &&
|
|
|
|
|
b.kind === Relay.BufferKind.PrivateMessage)) {
|
|
|
|
|
beep()
|
|
|
|
|
if (!visible)
|
|
|
|
|
b.highlighted = true
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.BufferUpdate, e => {
|
2022-09-14 01:57:02 +02:00
|
|
|
|
let b = buffers.get(e.bufferName)
|
2022-09-06 17:17:32 +02:00
|
|
|
|
if (b === undefined) {
|
2022-09-11 02:54:39 +02:00
|
|
|
|
buffers.set(e.bufferName, (b = {
|
|
|
|
|
lines: [],
|
|
|
|
|
history: [],
|
|
|
|
|
historyAt: 0,
|
|
|
|
|
}))
|
2022-09-10 20:38:32 +02:00
|
|
|
|
bufferResetStats(b)
|
2022-09-06 17:17:32 +02:00
|
|
|
|
}
|
2022-09-11 20:46:35 +02:00
|
|
|
|
|
2022-09-10 18:58:55 +02:00
|
|
|
|
b.hideUnimportant = e.hideUnimportant
|
2022-09-11 20:46:35 +02:00
|
|
|
|
b.kind = e.context.kind
|
|
|
|
|
b.server = servers.get(e.context.serverName)
|
2022-09-21 12:13:30 +02:00
|
|
|
|
b.topic = e.context.topic
|
2022-09-21 16:32:08 +02:00
|
|
|
|
b.modes = e.context.modes
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.BufferStats, e => {
|
2022-09-14 01:57:02 +02:00
|
|
|
|
let b = buffers.get(e.bufferName)
|
2022-09-10 17:37:19 +02:00
|
|
|
|
if (b === undefined)
|
|
|
|
|
return
|
|
|
|
|
|
2024-01-06 21:07:03 +01:00
|
|
|
|
b.newMessages = e.newMessages
|
2022-09-10 17:37:19 +02:00
|
|
|
|
b.newUnimportantMessages = e.newUnimportantMessages
|
|
|
|
|
b.highlighted = e.highlighted
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-10 17:37:19 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.BufferRename, e => {
|
2022-09-06 17:17:32 +02:00
|
|
|
|
buffers.set(e.new, buffers.get(e.bufferName))
|
|
|
|
|
buffers.delete(e.bufferName)
|
2023-08-25 21:20:50 +02:00
|
|
|
|
|
|
|
|
|
if (e.bufferName === bufferCurrent)
|
|
|
|
|
bufferCurrent = e.new
|
|
|
|
|
if (e.bufferName === bufferLast)
|
|
|
|
|
bufferLast = e.new
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
|
2022-09-06 17:17:32 +02:00
|
|
|
|
buffers.delete(e.bufferName)
|
2022-09-10 19:33:39 +02:00
|
|
|
|
if (e.bufferName === bufferLast)
|
|
|
|
|
bufferLast = undefined
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.BufferActivate, e => {
|
2022-09-06 23:17:47 +02:00
|
|
|
|
let old = buffers.get(bufferCurrent)
|
2024-01-06 23:27:22 +01:00
|
|
|
|
if (old !== undefined) {
|
2022-09-10 20:38:32 +02:00
|
|
|
|
bufferResetStats(old)
|
2024-01-06 23:27:22 +01:00
|
|
|
|
bufferPopExcessLines(old)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initial sync: trim all buffers to our limit, just for consistency.
|
|
|
|
|
if (bufferCurrent === undefined) {
|
|
|
|
|
for (let b of buffers.values())
|
|
|
|
|
bufferPopExcessLines(b)
|
|
|
|
|
}
|
2022-09-07 19:42:18 +02:00
|
|
|
|
|
2022-09-10 19:33:39 +02:00
|
|
|
|
bufferLast = bufferCurrent
|
2022-09-14 01:57:02 +02:00
|
|
|
|
let b = buffers.get(e.bufferName)
|
2022-09-06 17:17:32 +02:00
|
|
|
|
bufferCurrent = e.bufferName
|
2022-09-06 23:37:06 +02:00
|
|
|
|
bufferLog = undefined
|
2022-09-07 13:52:30 +02:00
|
|
|
|
bufferAutoscroll = true
|
2022-09-12 03:48:12 +02:00
|
|
|
|
if (b !== undefined && document.visibilityState !== 'hidden')
|
|
|
|
|
b.highlighted = false
|
2022-09-06 23:17:47 +02:00
|
|
|
|
|
|
|
|
|
let textarea = document.getElementById('input')
|
|
|
|
|
if (textarea === null)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
textarea.focus()
|
2022-09-11 01:01:16 +02:00
|
|
|
|
if (old !== undefined) {
|
2022-09-06 23:17:47 +02:00
|
|
|
|
old.input = textarea.value
|
2022-09-11 01:01:16 +02:00
|
|
|
|
old.inputStart = textarea.selectionStart
|
|
|
|
|
old.inputEnd = textarea.selectionEnd
|
|
|
|
|
old.inputDirection = textarea.selectionDirection
|
2022-09-11 02:54:39 +02:00
|
|
|
|
// Note that we effectively overwrite the newest line
|
|
|
|
|
// with the current textarea contents, and jump there.
|
|
|
|
|
old.historyAt = old.history.length
|
2022-09-11 01:01:16 +02:00
|
|
|
|
}
|
2022-09-06 23:17:47 +02:00
|
|
|
|
|
2022-09-11 01:01:16 +02:00
|
|
|
|
textarea.value = ''
|
|
|
|
|
if (b !== undefined && b.input !== undefined) {
|
|
|
|
|
textarea.value = b.input
|
|
|
|
|
textarea.setSelectionRange(b.inputStart, b.inputEnd, b.inputDirection)
|
|
|
|
|
}
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-30 17:16:16 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.BufferInput, e => {
|
|
|
|
|
let b = buffers.get(e.bufferName)
|
|
|
|
|
if (b === undefined)
|
|
|
|
|
return
|
|
|
|
|
if (b.historyAt == b.history.length)
|
|
|
|
|
b.historyAt++
|
|
|
|
|
b.history.push(e.text)
|
|
|
|
|
})
|
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.BufferClear, e => {
|
2022-09-14 01:57:02 +02:00
|
|
|
|
let b = buffers.get(e.bufferName)
|
2022-09-06 17:17:32 +02:00
|
|
|
|
if (b !== undefined)
|
|
|
|
|
b.lines.length = 0
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-05 22:34:20 +02:00
|
|
|
|
|
2022-09-11 20:46:35 +02:00
|
|
|
|
// ~~~ Server events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.ServerUpdate, e => {
|
2022-09-14 01:57:02 +02:00
|
|
|
|
let s = servers.get(e.serverName)
|
2022-09-11 20:46:35 +02:00
|
|
|
|
if (s === undefined)
|
|
|
|
|
servers.set(e.serverName, (s = {}))
|
2022-09-20 17:14:55 +02:00
|
|
|
|
s.data = e.data
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-11 20:46:35 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.ServerRename, e => {
|
2022-09-11 20:46:35 +02:00
|
|
|
|
servers.set(e.new, servers.get(e.serverName))
|
|
|
|
|
servers.delete(e.serverName)
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-11 20:46:35 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
rpcEventHandlers.set(Relay.Event.ServerRemove, e => {
|
2022-09-11 20:46:35 +02:00
|
|
|
|
servers.delete(e.serverName)
|
2022-09-15 22:45:14 +02:00
|
|
|
|
})
|
2022-09-11 20:46:35 +02:00
|
|
|
|
|
2022-09-05 22:34:20 +02:00
|
|
|
|
// --- 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}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- UI ---------------------------------------------------------------------
|
|
|
|
|
|
2022-09-10 17:18:08 +02:00
|
|
|
|
let linkRE = [
|
|
|
|
|
/https?:\/\//,
|
|
|
|
|
/([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/,
|
2022-09-23 09:01:52 +02:00
|
|
|
|
/([^\[\](){}<>"'\s,.:]|\([^\[\](){}<>"'\s]*\))/,
|
2022-09-10 17:18:08 +02:00
|
|
|
|
].map(r => r.source).join('')
|
|
|
|
|
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
let BufferList = {
|
|
|
|
|
view: vnode => {
|
2022-09-11 19:10:09 +02:00
|
|
|
|
let highlighted = false
|
2022-09-06 23:37:06 +02:00
|
|
|
|
let items = Array.from(buffers, ([name, b]) => {
|
2022-09-07 19:42:18 +02:00
|
|
|
|
let classes = [], displayName = name
|
|
|
|
|
if (name == bufferCurrent) {
|
|
|
|
|
classes.push('current')
|
2022-09-12 03:48:12 +02:00
|
|
|
|
} else if (b.newMessages) {
|
|
|
|
|
classes.push('activity')
|
|
|
|
|
displayName += ` (${b.newMessages})`
|
|
|
|
|
}
|
|
|
|
|
if (b.highlighted) {
|
|
|
|
|
classes.push('highlighted')
|
|
|
|
|
highlighted = true
|
2022-09-07 19:42:18 +02:00
|
|
|
|
}
|
2023-04-05 23:08:49 +02:00
|
|
|
|
// The role makes it selectable in VIM-like browser extensions.
|
|
|
|
|
return m('.item[role=tab]', {
|
2022-09-10 19:33:39 +02:00
|
|
|
|
onclick: event => bufferActivate(name),
|
2023-04-13 04:25:58 +02:00
|
|
|
|
onauxclick: event => {
|
|
|
|
|
if (event.button == 1)
|
|
|
|
|
rpc.send({
|
|
|
|
|
command: 'BufferInput',
|
|
|
|
|
bufferName: name,
|
|
|
|
|
text: '/buffer close',
|
|
|
|
|
})
|
|
|
|
|
},
|
2022-09-07 19:42:18 +02:00
|
|
|
|
class: classes.join(' '),
|
|
|
|
|
}, displayName)
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
})
|
2022-09-11 19:10:09 +02:00
|
|
|
|
|
2022-09-12 03:48:12 +02:00
|
|
|
|
updateIcon(rpc.ws === undefined ? null : highlighted)
|
2023-04-05 23:08:49 +02:00
|
|
|
|
return m('.list[role=tablist]', {}, items)
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Content = {
|
2022-09-07 14:07:14 +02:00
|
|
|
|
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.backgroundColor = palette[bg]
|
2022-09-21 06:23:41 +02:00
|
|
|
|
for (const _ in style)
|
2022-09-07 14:07:14 +02:00
|
|
|
|
return style
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
linkify: (text, attrs) => {
|
2022-09-10 17:18:08 +02:00
|
|
|
|
let re = new RegExp(linkRE, 'g'), a = [], end = 0, match
|
2022-09-07 14:07:14 +02:00
|
|
|
|
while ((match = re.exec(text)) !== null) {
|
|
|
|
|
if (end < match.index)
|
|
|
|
|
a.push(m('span', attrs, text.substring(end, match.index)))
|
2024-03-04 16:12:43 +01:00
|
|
|
|
a.push(m('a[target=_blank][rel=noreferrer]',
|
|
|
|
|
{href: match[0], ...attrs}, match[0]))
|
2022-09-07 14:07:14 +02:00
|
|
|
|
end = re.lastIndex
|
|
|
|
|
}
|
|
|
|
|
if (end < text.length)
|
|
|
|
|
a.push(m('span', attrs, text.substring(end)))
|
|
|
|
|
return a
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
makeMark: line => {
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
switch (line.rendition) {
|
2022-09-15 22:45:14 +02:00
|
|
|
|
case Relay.Rendition.Indent: return m('span.mark', {}, '')
|
|
|
|
|
case Relay.Rendition.Status: return m('span.mark', {}, '–')
|
|
|
|
|
case Relay.Rendition.Error: return m('span.mark.error', {}, '⚠')
|
|
|
|
|
case Relay.Rendition.Join: return m('span.mark.join', {}, '→')
|
|
|
|
|
case Relay.Rendition.Part: return m('span.mark.part', {}, '←')
|
|
|
|
|
case Relay.Rendition.Action: return m('span.mark.action', {}, '✶')
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
}
|
2022-09-15 22:45:14 +02:00
|
|
|
|
},
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
|
2022-09-15 22:45:14 +02:00
|
|
|
|
view: vnode => {
|
|
|
|
|
let line = vnode.children[0]
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
let classes = new Set()
|
|
|
|
|
let flip = c => {
|
|
|
|
|
if (classes.has(c))
|
|
|
|
|
classes.delete(c)
|
|
|
|
|
else
|
|
|
|
|
classes.add(c)
|
|
|
|
|
}
|
2022-09-15 22:45:14 +02:00
|
|
|
|
|
2022-09-05 22:34:20 +02:00
|
|
|
|
let fg = -1, bg = -1, inverse = false
|
2022-09-15 22:45:14 +02:00
|
|
|
|
return m('.content', vnode.attrs, [
|
|
|
|
|
Content.makeMark(line),
|
|
|
|
|
line.items.flatMap(item => {
|
|
|
|
|
switch (item.kind) {
|
|
|
|
|
case Relay.Item.Text:
|
|
|
|
|
return Content.linkify(item.text, {
|
|
|
|
|
class: Array.from(classes.keys()).join(' '),
|
|
|
|
|
style: Content.applyColor(fg, bg, inverse),
|
|
|
|
|
})
|
|
|
|
|
case Relay.Item.Reset:
|
|
|
|
|
classes.clear()
|
|
|
|
|
fg = bg = -1
|
|
|
|
|
inverse = false
|
|
|
|
|
break
|
|
|
|
|
case Relay.Item.FgColor:
|
|
|
|
|
fg = item.color
|
|
|
|
|
break
|
|
|
|
|
case Relay.Item.BgColor:
|
|
|
|
|
bg = item.color
|
|
|
|
|
break
|
|
|
|
|
case Relay.Item.FlipInverse:
|
|
|
|
|
inverse = !inverse
|
|
|
|
|
break
|
|
|
|
|
case Relay.Item.FlipBold:
|
|
|
|
|
flip('b')
|
|
|
|
|
break
|
|
|
|
|
case Relay.Item.FlipItalic:
|
|
|
|
|
flip('i')
|
|
|
|
|
break
|
|
|
|
|
case Relay.Item.FlipUnderline:
|
|
|
|
|
flip('u')
|
|
|
|
|
break
|
|
|
|
|
case Relay.Item.FlipCrossedOut:
|
|
|
|
|
flip('s')
|
|
|
|
|
break
|
|
|
|
|
case Relay.Item.FlipMonospace:
|
|
|
|
|
flip('m')
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
])
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-21 12:13:30 +02:00
|
|
|
|
let Topic = {
|
|
|
|
|
view: vnode => {
|
|
|
|
|
let b = buffers.get(bufferCurrent)
|
|
|
|
|
if (b !== undefined && b.topic !== undefined)
|
|
|
|
|
return m(Content, {}, {items: b.topic})
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
let Buffer = {
|
2022-09-06 22:30:23 +02:00
|
|
|
|
onupdate: vnode => {
|
2022-09-10 18:09:46 +02:00
|
|
|
|
if (bufferAutoscroll)
|
|
|
|
|
vnode.dom.scrollTop = vnode.dom.scrollHeight
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
oncreate: vnode => {
|
|
|
|
|
Buffer.onupdate(vnode)
|
2022-09-28 21:05:02 +02:00
|
|
|
|
|
|
|
|
|
vnode.state.controller = new AbortController()
|
2022-09-10 18:09:46 +02:00
|
|
|
|
window.addEventListener('resize', event => Buffer.onupdate(vnode),
|
2022-09-28 21:05:02 +02:00
|
|
|
|
{signal: vnode.state.controller.signal})
|
2023-01-24 08:02:08 +01:00
|
|
|
|
|
|
|
|
|
Buffer.setDateChangeTimeout(vnode)
|
2022-09-28 21:05:02 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onremove: vnode => {
|
|
|
|
|
vnode.state.controller.abort()
|
2023-01-24 08:02:08 +01:00
|
|
|
|
clearTimeout(vnode.state.dateChangeTimeout)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setDateChangeTimeout: vnode => {
|
|
|
|
|
let midnight = new Date()
|
|
|
|
|
midnight.setHours(24, 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
// Note that this doesn't handle time zone changes correctly.
|
|
|
|
|
vnode.state.dateChangeTimeout = setTimeout(() => {
|
|
|
|
|
m.redraw()
|
|
|
|
|
Buffer.setDateChangeTimeout(vnode)
|
|
|
|
|
}, midnight - new Date())
|
2022-09-06 22:30:23 +02:00
|
|
|
|
},
|
|
|
|
|
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
view: vnode => {
|
|
|
|
|
let lines = []
|
|
|
|
|
let b = buffers.get(bufferCurrent)
|
|
|
|
|
if (b === undefined)
|
2022-09-10 18:09:46 +02:00
|
|
|
|
return m('.buffer')
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
|
|
|
|
|
let lastDateMark = undefined
|
2022-09-11 21:30:51 +02:00
|
|
|
|
let squashing = false
|
2022-09-07 19:42:18 +02:00
|
|
|
|
let markBefore = b.lines.length
|
|
|
|
|
- b.newMessages - b.newUnimportantMessages
|
|
|
|
|
b.lines.forEach((line, i) => {
|
2022-09-10 18:58:55 +02:00
|
|
|
|
if (i == markBefore)
|
|
|
|
|
lines.push(m('.unread'))
|
2022-09-11 21:30:51 +02:00
|
|
|
|
|
|
|
|
|
if (!line.isUnimportant || !b.hideUnimportant) {
|
|
|
|
|
squashing = false
|
|
|
|
|
} else if (squashing) {
|
2022-09-10 18:58:55 +02:00
|
|
|
|
return
|
2022-09-11 21:30:51 +02:00
|
|
|
|
} else {
|
|
|
|
|
squashing = true
|
|
|
|
|
}
|
2022-09-10 18:58:55 +02:00
|
|
|
|
|
2022-09-06 14:38:09 +02:00
|
|
|
|
let date = new Date(line.when)
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
let dateMark = date.toLocaleDateString()
|
|
|
|
|
if (dateMark !== lastDateMark) {
|
|
|
|
|
lines.push(m('.date', {}, dateMark))
|
|
|
|
|
lastDateMark = dateMark
|
|
|
|
|
}
|
2022-09-11 21:30:51 +02:00
|
|
|
|
if (squashing) {
|
|
|
|
|
lines.push(m('.time.hidden'))
|
|
|
|
|
lines.push(m('.content'))
|
|
|
|
|
return
|
|
|
|
|
}
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
|
2022-09-07 15:33:38 +02:00
|
|
|
|
let attrs = {}
|
|
|
|
|
if (line.leaked)
|
|
|
|
|
attrs.class = 'leaked'
|
|
|
|
|
|
|
|
|
|
lines.push(m('.time', {...attrs}, date.toLocaleTimeString()))
|
|
|
|
|
lines.push(m(Content, {...attrs}, line))
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
})
|
2022-09-07 19:23:17 +02:00
|
|
|
|
|
|
|
|
|
let dateMark = new Date().toLocaleDateString()
|
2022-09-07 19:42:18 +02:00
|
|
|
|
if (dateMark !== lastDateMark && lastDateMark !== undefined)
|
2022-09-07 19:23:17 +02:00
|
|
|
|
lines.push(m('.date', {}, dateMark))
|
2022-09-21 07:32:18 +02:00
|
|
|
|
return m('.buffer', {onscroll: event => {
|
|
|
|
|
const dom = event.target
|
|
|
|
|
bufferAutoscroll =
|
2022-09-23 08:57:10 +02:00
|
|
|
|
dom.scrollTop + dom.clientHeight + 1 >= dom.scrollHeight
|
2024-07-28 03:42:41 +02:00
|
|
|
|
|
|
|
|
|
let b = buffers.get(bufferCurrent)
|
2024-07-28 13:42:15 +02:00
|
|
|
|
if (b !== undefined && b.highlighted && !bufferAutoscroll)
|
2024-07-28 03:42:41 +02:00
|
|
|
|
b.highlighted = false
|
2022-09-21 07:32:18 +02:00
|
|
|
|
}}, lines)
|
2022-09-06 23:37:06 +02:00
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-07 14:07:14 +02:00
|
|
|
|
let Log = {
|
|
|
|
|
oncreate: vnode => {
|
2022-09-10 20:38:32 +02:00
|
|
|
|
vnode.dom.scrollTop = vnode.dom.scrollHeight
|
|
|
|
|
vnode.dom.focus()
|
2022-09-07 13:52:30 +02:00
|
|
|
|
},
|
|
|
|
|
|
2022-09-10 17:18:08 +02:00
|
|
|
|
linkify: text => {
|
|
|
|
|
let re = new RegExp(linkRE, 'g'), a = [], end = 0, match
|
|
|
|
|
while ((match = re.exec(text)) !== null) {
|
|
|
|
|
if (end < match.index)
|
|
|
|
|
a.push(text.substring(end, match.index))
|
2024-03-04 16:12:43 +01:00
|
|
|
|
a.push(m('a[target=_blank][rel=noreferrer]',
|
|
|
|
|
{href: match[0]}, match[0]))
|
2022-09-10 17:18:08 +02:00
|
|
|
|
end = re.lastIndex
|
|
|
|
|
}
|
|
|
|
|
if (end < text.length)
|
|
|
|
|
a.push(text.substring(end))
|
|
|
|
|
return a
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-07 14:07:14 +02:00
|
|
|
|
view: vnode => {
|
2022-09-10 17:18:08 +02:00
|
|
|
|
return m(".log", {}, Log.linkify(bufferLog))
|
2022-09-06 23:37:06 +02:00
|
|
|
|
},
|
2022-09-07 14:07:14 +02:00
|
|
|
|
}
|
2022-09-06 23:37:06 +02:00
|
|
|
|
|
2022-09-18 05:53:44 +02:00
|
|
|
|
let Completions = {
|
|
|
|
|
entries: [],
|
|
|
|
|
|
|
|
|
|
reset: list => {
|
|
|
|
|
Completions.entries = list || []
|
|
|
|
|
m.redraw()
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
view: vnode => {
|
|
|
|
|
if (!Completions.entries.length)
|
|
|
|
|
return
|
|
|
|
|
return m('.completions', {},
|
|
|
|
|
Completions.entries.map(option => m('.completion', {}, option)))
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-07 14:07:14 +02:00
|
|
|
|
let BufferContainer = {
|
2022-09-06 23:37:06 +02:00
|
|
|
|
view: vnode => {
|
2022-09-07 14:07:14 +02:00
|
|
|
|
return m('.buffer-container', {}, [
|
|
|
|
|
m('.filler'),
|
|
|
|
|
bufferLog !== undefined ? m(Log) : m(Buffer),
|
2022-09-18 05:53:44 +02:00
|
|
|
|
m(Completions),
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-21 07:32:18 +02:00
|
|
|
|
let Toolbar = {
|
2022-09-23 09:37:23 +02:00
|
|
|
|
format: formatting => {
|
2022-09-23 08:57:10 +02:00
|
|
|
|
let textarea = document.getElementById('input')
|
2022-09-23 09:37:23 +02:00
|
|
|
|
if (textarea !== null)
|
|
|
|
|
Input.format(textarea, formatting)
|
2022-09-23 08:57:10 +02:00
|
|
|
|
},
|
|
|
|
|
|
2022-09-21 07:32:18 +02:00
|
|
|
|
view: vnode => {
|
2022-09-23 09:37:23 +02:00
|
|
|
|
let indicators = []
|
2022-09-23 08:57:10 +02:00
|
|
|
|
if (bufferLog === undefined && !bufferAutoscroll)
|
2022-09-23 09:37:23 +02:00
|
|
|
|
indicators.push(m('.indicator', {}, '⇩'))
|
|
|
|
|
if (Input.formatting)
|
|
|
|
|
indicators.push(m('.indicator', {}, '#'))
|
2022-09-21 07:32:18 +02:00
|
|
|
|
return m('.toolbar', {}, [
|
2022-09-23 09:37:23 +02:00
|
|
|
|
indicators,
|
|
|
|
|
m('button', {onclick: event => Toolbar.format('\u0002')},
|
2022-09-23 08:57:10 +02:00
|
|
|
|
m('b', {}, 'B')),
|
2022-09-23 09:37:23 +02:00
|
|
|
|
m('button', {onclick: event => Toolbar.format('\u001D')},
|
2022-09-23 08:57:10 +02:00
|
|
|
|
m('i', {}, 'I')),
|
2022-09-23 09:37:23 +02:00
|
|
|
|
m('button', {onclick: event => Toolbar.format('\u001F')},
|
2022-09-23 08:57:10 +02:00
|
|
|
|
m('u', {}, 'U')),
|
2022-09-21 07:32:18 +02:00
|
|
|
|
m('button', {onclick: event => bufferToggleLog()},
|
|
|
|
|
bufferLog === undefined ? 'Log' : 'Hide log'),
|
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-11 20:46:35 +02:00
|
|
|
|
let Status = {
|
|
|
|
|
view: vnode => {
|
|
|
|
|
let b = buffers.get(bufferCurrent)
|
|
|
|
|
if (b === undefined)
|
|
|
|
|
return m('.status', {}, 'Synchronizing...')
|
|
|
|
|
|
|
|
|
|
let status = `${bufferCurrent}`
|
2022-09-21 16:32:08 +02:00
|
|
|
|
if (b.modes)
|
|
|
|
|
status += `(+${b.modes})`
|
2022-09-11 20:46:35 +02:00
|
|
|
|
if (b.hideUnimportant)
|
|
|
|
|
status += `<H>`
|
2022-09-21 07:32:18 +02:00
|
|
|
|
return m('.status', {}, [status, m(Toolbar)])
|
2022-09-19 03:16:34 +02:00
|
|
|
|
},
|
|
|
|
|
}
|
2022-09-15 22:45:14 +02:00
|
|
|
|
|
2022-09-19 03:16:34 +02:00
|
|
|
|
let Prompt = {
|
|
|
|
|
view: vnode => {
|
|
|
|
|
let b = buffers.get(bufferCurrent)
|
|
|
|
|
if (b === undefined || b.server === undefined)
|
|
|
|
|
return
|
|
|
|
|
|
2022-09-20 17:14:55 +02:00
|
|
|
|
if (b.server.data.user !== undefined) {
|
|
|
|
|
let user = b.server.data.user
|
2022-09-21 16:32:08 +02:00
|
|
|
|
if (b.server.data.userModes)
|
|
|
|
|
user += `(${b.server.data.userModes})`
|
2022-09-20 17:14:55 +02:00
|
|
|
|
return m('.prompt', {}, `${user}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This might certainly be done more systematically.
|
|
|
|
|
let state = b.server.data.state
|
2022-09-19 03:16:34 +02:00
|
|
|
|
for (const s in Relay.ServerState)
|
2022-09-20 17:14:55 +02:00
|
|
|
|
if (Relay.ServerState[s] == state) {
|
2022-09-19 03:16:34 +02:00
|
|
|
|
state = s
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
return m('.prompt', {}, `(${state})`)
|
2022-09-11 20:46:35 +02:00
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-07 14:07:14 +02:00
|
|
|
|
let Input = {
|
2022-09-07 15:09:43 +02:00
|
|
|
|
counter: 0,
|
|
|
|
|
stamp: textarea => {
|
|
|
|
|
return [Input.counter,
|
|
|
|
|
textarea.selectionStart, textarea.selectionEnd, textarea.value]
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-18 00:38:31 +02:00
|
|
|
|
complete: (b, textarea) => {
|
2022-09-06 17:17:32 +02:00
|
|
|
|
if (textarea.selectionStart !== textarea.selectionEnd)
|
2022-09-07 14:07:14 +02:00
|
|
|
|
return false
|
|
|
|
|
|
2022-09-07 15:09:43 +02:00
|
|
|
|
// Cancel any previous autocomplete, and ensure applicability.
|
|
|
|
|
Input.counter++
|
|
|
|
|
let state = Input.stamp(textarea)
|
2022-09-06 17:17:32 +02:00
|
|
|
|
rpc.send({
|
|
|
|
|
command: 'BufferComplete',
|
|
|
|
|
bufferName: bufferCurrent,
|
|
|
|
|
text: textarea.value,
|
2022-09-11 15:54:39 +02:00
|
|
|
|
position: utf8Encode(
|
|
|
|
|
textarea.value.slice(0, textarea.selectionEnd)).length,
|
2022-09-06 23:37:06 +02:00
|
|
|
|
}).then(resp => {
|
2022-09-07 15:09:43 +02:00
|
|
|
|
if (!Input.stamp(textarea).every((v, k) => v === state[k]))
|
|
|
|
|
return
|
|
|
|
|
|
2022-09-11 15:54:39 +02:00
|
|
|
|
let preceding = utf8Encode(textarea.value).slice(0, resp.start)
|
|
|
|
|
let start = utf8Decode(preceding).length
|
2022-09-18 05:53:44 +02:00
|
|
|
|
if (resp.completions.length > 0) {
|
2022-09-06 23:37:06 +02:00
|
|
|
|
textarea.setRangeText(resp.completions[0],
|
2022-09-11 15:54:39 +02:00
|
|
|
|
start, textarea.selectionEnd, 'end')
|
|
|
|
|
}
|
2022-09-18 05:53:44 +02:00
|
|
|
|
|
|
|
|
|
if (resp.completions.length == 1) {
|
2022-09-06 17:17:32 +02:00
|
|
|
|
textarea.setRangeText(' ',
|
|
|
|
|
textarea.selectionStart, textarea.selectionEnd, 'end')
|
2022-09-18 05:53:44 +02:00
|
|
|
|
} else {
|
|
|
|
|
beep()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (resp.completions.length > 1)
|
|
|
|
|
Completions.reset(resp.completions.slice(1))
|
2022-09-06 17:17:32 +02:00
|
|
|
|
})
|
2022-09-07 14:07:14 +02:00
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-18 00:38:31 +02:00
|
|
|
|
submit: (b, textarea) => {
|
2022-09-06 17:17:32 +02:00
|
|
|
|
rpc.send({
|
|
|
|
|
command: 'BufferInput',
|
|
|
|
|
bufferName: bufferCurrent,
|
|
|
|
|
text: textarea.value,
|
|
|
|
|
})
|
2022-09-11 02:54:39 +02:00
|
|
|
|
|
|
|
|
|
// b.history[b.history.length] is virtual, and is represented
|
|
|
|
|
// either by textarea contents when it's currently being edited,
|
|
|
|
|
// or by b.input in all other cases.
|
|
|
|
|
b.history.push(textarea.value)
|
|
|
|
|
b.historyAt = b.history.length
|
2022-09-06 17:17:32 +02:00
|
|
|
|
textarea.value = ''
|
2022-09-07 14:07:14 +02:00
|
|
|
|
return true
|
|
|
|
|
},
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-18 01:46:00 +02:00
|
|
|
|
backward: textarea => {
|
2022-09-18 01:09:41 +02:00
|
|
|
|
if (textarea.selectionStart !== textarea.selectionEnd)
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
let point = textarea.selectionStart
|
|
|
|
|
if (point < 1)
|
|
|
|
|
return false
|
|
|
|
|
while (point && /\s/.test(textarea.value.charAt(--point))) {}
|
|
|
|
|
while (point-- && !/\s/.test(textarea.value.charAt(point))) {}
|
|
|
|
|
point++
|
|
|
|
|
textarea.setSelectionRange(point, point)
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-18 01:46:00 +02:00
|
|
|
|
forward: textarea => {
|
2022-09-18 01:09:41 +02:00
|
|
|
|
if (textarea.selectionStart !== textarea.selectionEnd)
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
let point = textarea.selectionStart, len = textarea.value.length
|
|
|
|
|
if (point + 1 > len)
|
|
|
|
|
return false
|
|
|
|
|
while (point < len && /\s/.test(textarea.value.charAt(point))) point++
|
|
|
|
|
while (point < len && !/\s/.test(textarea.value.charAt(point))) point++
|
|
|
|
|
textarea.setSelectionRange(point, point)
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-18 01:46:00 +02:00
|
|
|
|
modifyWord: (textarea, cb) => {
|
|
|
|
|
let start = textarea.selectionStart
|
|
|
|
|
let end = textarea.selectionEnd
|
|
|
|
|
if (start === end) {
|
|
|
|
|
let len = textarea.value.length
|
|
|
|
|
while (start < len && /\s/.test(textarea.value.charAt(start)))
|
|
|
|
|
start++;
|
|
|
|
|
end = start
|
|
|
|
|
while (end < len && !/\s/.test(textarea.value.charAt(end)))
|
|
|
|
|
end++;
|
|
|
|
|
}
|
|
|
|
|
if (start === end)
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
const text = textarea.value, modified = cb(text.substring(start, end))
|
|
|
|
|
textarea.value = text.slice(0, start) + modified + text.slice(end)
|
|
|
|
|
end = start + modified.length
|
|
|
|
|
textarea.setSelectionRange(end, end)
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
downcase: textarea => {
|
|
|
|
|
return Input.modifyWord(textarea, text => text.toLowerCase())
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
upcase: textarea => {
|
|
|
|
|
return Input.modifyWord(textarea, text => text.toUpperCase())
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
capitalize: textarea => {
|
|
|
|
|
return Input.modifyWord(textarea, text => {
|
|
|
|
|
const cps = Array.from(text.toLowerCase())
|
|
|
|
|
return cps[0].toUpperCase() + cps.slice(1).join('')
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-18 00:38:31 +02:00
|
|
|
|
first: (b, textarea) => {
|
|
|
|
|
if (b.historyAt <= 0)
|
2022-09-11 02:54:39 +02:00
|
|
|
|
return false
|
|
|
|
|
|
2022-09-18 00:38:31 +02:00
|
|
|
|
if (b.historyAt == b.history.length)
|
|
|
|
|
b.input = textarea.value
|
|
|
|
|
textarea.value = b.history[(b.historyAt = 0)]
|
2022-09-11 02:54:39 +02:00
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-18 00:38:31 +02:00
|
|
|
|
last: (b, textarea) => {
|
|
|
|
|
if (b.historyAt >= b.history.length)
|
2022-09-11 02:54:39 +02:00
|
|
|
|
return false
|
|
|
|
|
|
2022-09-18 00:38:31 +02:00
|
|
|
|
b.historyAt = b.history.length
|
|
|
|
|
textarea.value = b.input
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
previous: (b, textarea) => {
|
|
|
|
|
if (b.historyAt <= 0)
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
if (b.historyAt == b.history.length)
|
|
|
|
|
b.input = textarea.value
|
|
|
|
|
textarea.value = b.history[--b.historyAt]
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
next: (b, textarea) => {
|
|
|
|
|
if (b.historyAt >= b.history.length)
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
if (++b.historyAt == b.history.length)
|
|
|
|
|
textarea.value = b.input
|
|
|
|
|
else
|
|
|
|
|
textarea.value = b.history[b.historyAt]
|
2022-09-11 02:54:39 +02:00
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-23 09:37:23 +02:00
|
|
|
|
formatting: false,
|
|
|
|
|
|
|
|
|
|
format: (textarea, formatting) => {
|
|
|
|
|
const [start, end] = [textarea.selectionStart, textarea.selectionEnd]
|
|
|
|
|
if (start === end) {
|
|
|
|
|
textarea.setRangeText(formatting)
|
|
|
|
|
textarea.setSelectionRange(
|
|
|
|
|
start + formatting.length, end + formatting.length)
|
|
|
|
|
} else {
|
|
|
|
|
textarea.setRangeText(
|
|
|
|
|
formatting + textarea.value.substr(start, end) + formatting)
|
|
|
|
|
}
|
|
|
|
|
textarea.focus()
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-07 14:07:14 +02:00
|
|
|
|
onKeyDown: event => {
|
|
|
|
|
// TODO: And perhaps on other actions, too.
|
|
|
|
|
rpc.send({command: 'Active'})
|
|
|
|
|
|
2022-09-18 00:38:31 +02:00
|
|
|
|
let b = buffers.get(bufferCurrent)
|
2024-07-04 19:16:48 +02:00
|
|
|
|
if (b === undefined || event.isComposing)
|
2022-09-18 00:38:31 +02:00
|
|
|
|
return
|
|
|
|
|
|
2022-09-07 14:07:14 +02:00
|
|
|
|
let textarea = event.currentTarget
|
|
|
|
|
let handled = false
|
2022-09-18 00:38:31 +02:00
|
|
|
|
let success = true
|
2022-09-23 09:37:23 +02:00
|
|
|
|
if (Input.formatting) {
|
|
|
|
|
Input.formatting = false
|
|
|
|
|
|
|
|
|
|
// Like process_formatting_escape() within xC.
|
|
|
|
|
handled = true
|
|
|
|
|
switch (event.key) {
|
|
|
|
|
case 'b': Input.format(textarea, '\u0002'); break
|
|
|
|
|
case 'c': Input.format(textarea, '\u0003'); break
|
|
|
|
|
case 'q':
|
|
|
|
|
case 'm': Input.format(textarea, '\u0011'); break
|
|
|
|
|
case 'v': Input.format(textarea, '\u0016'); break
|
|
|
|
|
case 'i':
|
|
|
|
|
case ']': Input.format(textarea, '\u001D'); break
|
|
|
|
|
case 's':
|
|
|
|
|
case 'x':
|
|
|
|
|
case '^': Input.format(textarea, '\u001E'); break
|
|
|
|
|
case 'u':
|
|
|
|
|
case '_': Input.format(textarea, '\u001F'); break
|
|
|
|
|
case 'r':
|
|
|
|
|
case 'o': Input.format(textarea, '\u000F'); break
|
|
|
|
|
default: success = false
|
|
|
|
|
}
|
|
|
|
|
} else if (hasShortcutModifiers(event)) {
|
2022-09-18 00:38:31 +02:00
|
|
|
|
handled = true
|
2022-09-11 02:54:39 +02:00
|
|
|
|
switch (event.key) {
|
2022-09-18 01:46:00 +02:00
|
|
|
|
case 'b': success = Input.backward(textarea); break
|
|
|
|
|
case 'f': success = Input.forward(textarea); break
|
|
|
|
|
case 'l': success = Input.downcase(textarea); break
|
|
|
|
|
case 'u': success = Input.upcase(textarea); break
|
|
|
|
|
case 'c': success = Input.capitalize(textarea); break
|
2022-09-18 00:38:31 +02:00
|
|
|
|
case '<': success = Input.first(b, textarea); break
|
|
|
|
|
case '>': success = Input.last(b, textarea); break
|
|
|
|
|
case 'p': success = Input.previous(b, textarea); break
|
|
|
|
|
case 'n': success = Input.next(b, textarea); break
|
2022-09-23 09:37:23 +02:00
|
|
|
|
case 'm': success = Input.formatting = true; break
|
2022-09-18 00:38:31 +02:00
|
|
|
|
default: handled = false
|
2022-09-11 02:54:39 +02:00
|
|
|
|
}
|
|
|
|
|
} else if (!event.altKey && !event.ctrlKey && !event.metaKey &&
|
|
|
|
|
!event.shiftKey) {
|
2022-09-18 00:38:31 +02:00
|
|
|
|
handled = true
|
2023-07-22 23:47:33 +02:00
|
|
|
|
switch (event.key) {
|
|
|
|
|
case 'PageUp':
|
|
|
|
|
Array.from(document.getElementsByClassName('buffer'))
|
|
|
|
|
.forEach(b => b.scrollBy(0, -b.clientHeight))
|
|
|
|
|
break
|
|
|
|
|
case 'PageDown':
|
|
|
|
|
Array.from(document.getElementsByClassName('buffer'))
|
|
|
|
|
.forEach(b => b.scrollBy(0, +b.clientHeight))
|
|
|
|
|
break
|
|
|
|
|
case 'Tab':
|
|
|
|
|
success = Input.complete(b, textarea);
|
|
|
|
|
break
|
|
|
|
|
case 'Enter':
|
|
|
|
|
success = Input.submit(b, textarea);
|
|
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
handled = false
|
2022-09-11 02:54:39 +02:00
|
|
|
|
}
|
2022-09-07 14:07:14 +02:00
|
|
|
|
}
|
2022-09-18 00:38:31 +02:00
|
|
|
|
if (!success)
|
|
|
|
|
beep()
|
2022-09-07 14:07:14 +02:00
|
|
|
|
if (handled)
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
},
|
2022-09-06 17:17:32 +02:00
|
|
|
|
|
2022-09-23 09:37:23 +02:00
|
|
|
|
onStateChange: event => {
|
|
|
|
|
Completions.reset()
|
|
|
|
|
Input.formatting = false
|
|
|
|
|
},
|
|
|
|
|
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
view: vnode => {
|
2022-09-18 05:53:44 +02:00
|
|
|
|
return m('textarea#input', {
|
|
|
|
|
rows: 1,
|
|
|
|
|
onkeydown: Input.onKeyDown,
|
2022-09-23 09:37:23 +02:00
|
|
|
|
oninput: Input.onStateChange,
|
2022-09-18 05:53:44 +02:00
|
|
|
|
// Sadly only supported in Firefox as of writing.
|
2022-09-23 09:37:23 +02:00
|
|
|
|
onselectionchange: Input.onStateChange,
|
2022-09-18 05:53:44 +02:00
|
|
|
|
// The list of completions is scrollable without receiving focus.
|
2022-09-23 09:37:23 +02:00
|
|
|
|
onblur: Input.onStateChange,
|
2022-09-18 05:53:44 +02:00
|
|
|
|
})
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Main = {
|
|
|
|
|
view: vnode => {
|
2022-09-16 03:18:53 +02:00
|
|
|
|
let overlay = undefined
|
2022-09-06 20:17:23 +02:00
|
|
|
|
if (connecting)
|
2022-09-16 03:18:53 +02:00
|
|
|
|
overlay = m('.overlay', {}, "Connecting...")
|
2022-09-06 20:17:23 +02:00
|
|
|
|
else if (rpc.ws === undefined)
|
2022-09-16 03:18:53 +02:00
|
|
|
|
overlay = m('.overlay', {}, [
|
|
|
|
|
m('', {}, "Disconnected"),
|
|
|
|
|
m('', {}, m('small', {}, "Reload page to reconnect.")),
|
|
|
|
|
])
|
2022-09-06 20:17:23 +02:00
|
|
|
|
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
return m('.xP', {}, [
|
2022-09-16 03:18:53 +02:00
|
|
|
|
overlay,
|
2022-09-21 12:13:30 +02:00
|
|
|
|
m('.title', {}, [m('b', {}, `xP`), m(Topic)]),
|
2022-09-07 14:07:14 +02:00
|
|
|
|
m('.middle', {}, [m(BufferList), m(BufferContainer)]),
|
2022-09-11 20:46:35 +02:00
|
|
|
|
m(Status),
|
2022-09-19 03:16:34 +02:00
|
|
|
|
m('.input', {}, [m(Prompt), m(Input)]),
|
Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options,
it was decided to implement an XDR-like protocol code generator
in portable AWK. It now has two backends, per each of:
- xF, the X11 frontend, is in C, and is meant to be the primary
user interface in the future.
- xP, the web frontend, relies on a protocol proxy written in Go,
and is meant for use on-the-go (no pun intended).
They are very much work-in-progress proofs of concept right now,
and the relay protocol is certain to change.
2022-08-08 04:39:20 +02:00
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-06 21:27:14 +02:00
|
|
|
|
window.addEventListener('load', () => m.mount(document.body, Main))
|
2022-09-10 19:22:53 +02:00
|
|
|
|
|
2022-09-12 03:48:12 +02:00
|
|
|
|
document.addEventListener('visibilitychange', event => {
|
|
|
|
|
let b = buffers.get(bufferCurrent)
|
|
|
|
|
if (b !== undefined && document.visibilityState !== 'hidden') {
|
|
|
|
|
b.highlighted = false
|
|
|
|
|
m.redraw()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2022-09-12 16:43:13 +02:00
|
|
|
|
// On macOS, the Alt/Option key transforms characters, which basically breaks
|
|
|
|
|
// all event.altKey shortcuts, so implement Escape prefixing on that system.
|
|
|
|
|
// This method of detection only works with Blink browsers, as of writing.
|
|
|
|
|
let lastWasEscape = false
|
2022-09-10 19:22:53 +02:00
|
|
|
|
document.addEventListener('keydown', event => {
|
2022-09-12 16:43:13 +02:00
|
|
|
|
event.escapePrefix = lastWasEscape
|
|
|
|
|
if (lastWasEscape) {
|
2022-10-04 20:14:52 +02:00
|
|
|
|
// https://www.w3.org/TR/uievents-key/#keys-modifier
|
|
|
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1232918
|
|
|
|
|
if (["Alt", "AltGraph", "CapsLock", "Control", "Fn", "FnLock",
|
|
|
|
|
"Meta", "NumLock", "ScrollLock", "Shift", "Symbol", "SymbolLock",
|
|
|
|
|
"Hyper", "Super", "OS"].indexOf(event.key) != -1)
|
|
|
|
|
return
|
|
|
|
|
|
2022-09-12 16:43:13 +02:00
|
|
|
|
lastWasEscape = false
|
|
|
|
|
} else if (event.code == 'Escape' &&
|
|
|
|
|
navigator.userAgentData?.platform === 'macOS') {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
lastWasEscape = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-11 02:54:39 +02:00
|
|
|
|
if (rpc.ws == undefined || !hasShortcutModifiers(event))
|
2022-09-10 19:22:53 +02:00
|
|
|
|
return
|
|
|
|
|
|
2022-09-13 03:18:12 +02:00
|
|
|
|
// Rotate names so that the current buffer comes first.
|
|
|
|
|
let names = [...buffers.keys()]
|
|
|
|
|
names.push.apply(names,
|
|
|
|
|
names.splice(0, names.findIndex(name => name == bufferCurrent)))
|
|
|
|
|
|
2022-09-11 02:54:39 +02:00
|
|
|
|
switch (event.key) {
|
|
|
|
|
case 'h':
|
2022-09-10 20:38:32 +02:00
|
|
|
|
bufferToggleLog()
|
2022-09-11 02:54:39 +02:00
|
|
|
|
break
|
2022-09-16 02:46:03 +02:00
|
|
|
|
case 'H':
|
|
|
|
|
if (bufferCurrent !== undefined)
|
|
|
|
|
bufferToggleUnimportant(bufferCurrent)
|
|
|
|
|
break
|
2022-09-11 02:54:39 +02:00
|
|
|
|
case 'a':
|
2022-09-13 03:18:12 +02:00
|
|
|
|
for (const name of names.slice(1))
|
|
|
|
|
if (buffers.get(name).newMessages) {
|
2022-09-10 19:33:39 +02:00
|
|
|
|
bufferActivate(name)
|
2022-09-10 19:22:53 +02:00
|
|
|
|
break
|
|
|
|
|
}
|
2022-09-11 02:54:39 +02:00
|
|
|
|
break
|
|
|
|
|
case '!':
|
2022-09-13 03:18:12 +02:00
|
|
|
|
for (const name of names.slice(1))
|
|
|
|
|
if (buffers.get(name).highlighted) {
|
2022-09-10 19:33:39 +02:00
|
|
|
|
bufferActivate(name)
|
2022-09-10 19:22:53 +02:00
|
|
|
|
break
|
|
|
|
|
}
|
2022-09-11 02:54:39 +02:00
|
|
|
|
break
|
2022-09-11 21:47:24 +02:00
|
|
|
|
case 'Tab':
|
|
|
|
|
if (bufferLast !== undefined)
|
|
|
|
|
bufferActivate(bufferLast)
|
|
|
|
|
break
|
|
|
|
|
case 'PageUp':
|
2022-09-13 03:18:12 +02:00
|
|
|
|
if (names.length > 1)
|
|
|
|
|
bufferActivate(names.at(-1))
|
2022-09-11 21:47:24 +02:00
|
|
|
|
break
|
|
|
|
|
case 'PageDown':
|
2022-09-13 03:18:12 +02:00
|
|
|
|
if (names.length > 1)
|
|
|
|
|
bufferActivate(names.at(+1))
|
2022-09-11 21:47:24 +02:00
|
|
|
|
break
|
2022-09-11 02:54:39 +02:00
|
|
|
|
default:
|
2022-09-10 19:33:39 +02:00
|
|
|
|
return
|
2022-09-11 02:54:39 +02:00
|
|
|
|
}
|
2022-09-10 19:33:39 +02:00
|
|
|
|
|
|
|
|
|
event.preventDefault()
|
2022-09-12 16:43:13 +02:00
|
|
|
|
}, true)
|