diff options
-rw-r--r-- | xP/public/xP.js | 252 |
1 files changed, 139 insertions, 113 deletions
diff --git a/xP/public/xP.js b/xP/public/xP.js index 4d2c439..abef57a 100644 --- a/xP/public/xP.js +++ b/xP/public/xP.js @@ -226,28 +226,46 @@ for (let i = 0; i < 24; i++) { 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.backgroundColor = palette[bg] - if (style) - return style -} - // ---- UI --------------------------------------------------------------------- +let Toolbar = { + toggleAutoscroll: () => { + bufferAutoscroll = !bufferAutoscroll + }, + + toggleLog: () => { + if (bufferLog) { + bufferLog = undefined + return + } + + rpc.send({ + command: 'BufferLog', + bufferName: bufferCurrent, + }).then(resp => { + bufferLog = rpc.base64decode(resp.log) + m.redraw() + }) + }, + + view: vnode => { + return m('.toolbar', {}, [ + m('button', {onclick: Toolbar.toggleAutoscroll}, + bufferAutoscroll ? 'Pause autoscroll' : 'Unpause autoscroll'), + m('button', {onclick: Toolbar.toggleLog}, + bufferLog === undefined ? 'Show log' : 'Hide log'), + ]) + }, +} + let BufferList = { + activate: name => { + rpc.send({command: 'BufferActivate', bufferName: name}) + }, + view: vnode => { let items = Array.from(buffers, ([name, b]) => { - let attrs = { - onclick: event => - rpc.send({command: 'BufferActivate', bufferName: name}), - } + let attrs = {onclick: event => BufferList.activate(name)} if (name == bufferCurrent) attrs.class = 'active' return m('.item', attrs, name) @@ -256,35 +274,49 @@ let BufferList = { }, } -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 = { + 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] + if (style) + return style + }, + + linkify: (text, attrs) => { + let re = new RegExp([ + /https?:\/\//, + /([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/, + /[^\[\](){}<>"'\s,.:]/, + ].map(r => r.source).join(''), 'g') + + let a = [], 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))) + return a + }, + view: vnode => { let line = vnode.children[0] - let content = [] + let mark = undefined 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 + case 'Indent': mark = m('span.mark', {}, ''); break + case 'Status': mark = m('span.mark', {}, '–'); break + case 'Error': mark = m('span.mark.error', {}, '⚠'); break + case 'Join': mark = m('span.mark.join', {}, '→'); break + case 'Part': mark = m('span.mark.part', {}, '←'); break + case 'Action': mark = m('span.mark.action', {}, '✶'); break } let classes = new Set() @@ -295,14 +327,13 @@ let Content = { classes.add(c) } let fg = -1, bg = -1, inverse = false - line.items.forEach(item => { + return m('.content', {}, [mark, line.items.flatMap(item => { switch (item.kind) { case 'Text': - linkify(item.text, { + return Content.linkify(item.text, { class: Array.from(classes.keys()).join(' '), - style: applyColor(fg, bg, inverse), - }, content) - break + style: Content.applyColor(fg, bg, inverse), + }) case 'Reset': classes.clear() fg = bg = -1 @@ -317,26 +348,30 @@ let Content = { 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 + 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 = { oncreate: vnode => { - if (vnode.dom === undefined || - bufferLog !== undefined || !bufferAutoscroll) - return - - let el = vnode.dom.children[1] - if (el !== null) - el.scrollTop = el.scrollHeight + if (vnode.dom !== undefined && bufferAutoscroll) + vnode.dom.scrollTop = vnode.dom.scrollHeight }, onupdate: vnode => { @@ -361,57 +396,35 @@ let Buffer = { lines.push(m('.time', {}, date.toLocaleTimeString())) lines.push(m(Content, {}, line)) }) - - return m('.buffer-container', {}, [ - m('.filler'), - bufferLog !== undefined - ? m(".log", {}, bufferLog) - : m('.buffer', {}, lines), - ]) + return m('.buffer', {}, lines) }, } -let Toolbar = { - toggleAutoscroll: () => { - bufferAutoscroll = !bufferAutoscroll +let Log = { + oncreate: vnode => { + if (vnode.dom !== undefined) + vnode.dom.scrollTop = vnode.dom.scrollHeight }, - toggleLog: () => { - if (bufferLog) { - bufferLog = undefined - return - } - - rpc.send({ - command: 'BufferLog', - bufferName: bufferCurrent, - }).then(resp => { - bufferLog = rpc.base64decode(resp.log) - m.redraw() - }) + view: vnode => { + return m(".log", {}, bufferLog) }, +} +let BufferContainer = { view: vnode => { - return m('.toolbar', {}, [ - m('button', {onclick: Toolbar.toggleAutoscroll}, - bufferAutoscroll ? 'Pause autoscroll' : 'Unpause autoscroll'), - m('button', {onclick: Toolbar.toggleLog}, - bufferLog === undefined ? 'Show log' : 'Hide log'), + return m('.buffer-container', {}, [ + m('.filler'), + bufferLog !== undefined ? m(Log) : m(Buffer), ]) }, } -function onKeyDown(event) { - // TODO: And perhaps on other actions, too. - rpc.send({command: 'Active'}) - - // TODO: Cancel any current autocomplete. - - let textarea = event.currentTarget - switch (event.keyCode) { - case 9: +let Input = { + complete: textarea => { if (textarea.selectionStart !== textarea.selectionEnd) - return + return false + rpc.send({ command: 'BufferComplete', bufferName: bufferCurrent, @@ -426,28 +439,41 @@ function onKeyDown(event) { textarea.setRangeText(' ', textarea.selectionStart, textarea.selectionEnd, 'end') }) - break; - case 13: + return true + }, + + submit: textarea => { rpc.send({ command: 'BufferInput', bufferName: bufferCurrent, text: textarea.value, }) textarea.value = '' - break; - default: - return - } + return true + }, - event.preventDefault() -} + onKeyDown: event => { + // TODO: And perhaps on other actions, too. + rpc.send({command: 'Active'}) + + // TODO: Cancel any current autocomplete. + + let textarea = event.currentTarget + let handled = false + switch (event.keyCode) { + case 9: + handled = Input.complete(textarea) + break + case 13: + handled = Input.submit(textarea) + break + } + if (handled) + event.preventDefault() + }, -let Input = { view: vnode => { - return m('textarea#input', { - rows: 1, - onkeydown: onKeyDown, - }) + return m('textarea#input', {rows: 1, onkeydown: Input.onKeyDown}) }, } @@ -461,7 +487,7 @@ let Main = { return m('.xP', {}, [ m('.title', {}, [`xP (${state})`, m(Toolbar)]), - m('.middle', {}, [m(BufferList), m(Buffer)]), + m('.middle', {}, [m(BufferList), m(BufferContainer)]), m('.status', {}, bufferCurrent), m(Input), ]) |