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,151 +226,8 @@ 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 BufferList = { - view: vnode => { - let items = Array.from(buffers, ([name, b]) => { - let attrs = { - onclick: event => - rpc.send({command: 'BufferActivate', bufferName: name}), - } - if (name == bufferCurrent) - attrs.class = 'active' - return 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 = { - oncreate: vnode => { - if (vnode.dom === undefined || - bufferLog !== undefined || !bufferAutoscroll) - return - - let el = vnode.dom.children[1] - if (el !== null) - el.scrollTop = el.scrollHeight - }, - - onupdate: vnode => { - Buffer.oncreate(vnode) - }, - - 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'), - bufferLog !== undefined - ? m(".log", {}, bufferLog) - : m('.buffer', {}, lines), - ]) - }, -} - let Toolbar = { toggleAutoscroll: () => { bufferAutoscroll = !bufferAutoscroll @@ -401,17 +258,173 @@ let Toolbar = { }, } -function onKeyDown(event) { - // TODO: And perhaps on other actions, too. - rpc.send({command: 'Active'}) +let BufferList = { + activate: name => { + rpc.send({command: 'BufferActivate', bufferName: name}) + }, - // TODO: Cancel any current autocomplete. + view: vnode => { + let items = Array.from(buffers, ([name, b]) => { + let attrs = {onclick: event => BufferList.activate(name)} + if (name == bufferCurrent) + attrs.class = 'active' + return m('.item', attrs, name) + }) + return m('.list', {}, items) + }, +} - let textarea = event.currentTarget - switch (event.keyCode) { - case 9: - if (textarea.selectionStart !== textarea.selectionEnd) +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 mark = undefined + switch (line.rendition) { + 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() + let flip = c => { + if (classes.has(c)) + classes.delete(c) + else + classes.add(c) + } + let fg = -1, bg = -1, inverse = false + return m('.content', {}, [mark, line.items.flatMap(item => { + switch (item.kind) { + case 'Text': + return Content.linkify(item.text, { + class: Array.from(classes.keys()).join(' '), + style: Content.applyColor(fg, bg, inverse), + }) + 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 + } + })]) + }, +} + +let Buffer = { + oncreate: vnode => { + if (vnode.dom !== undefined && bufferAutoscroll) + vnode.dom.scrollTop = vnode.dom.scrollHeight + }, + + onupdate: vnode => { + Buffer.oncreate(vnode) + }, + + 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', {}, lines) + }, +} + +let Log = { + oncreate: vnode => { + if (vnode.dom !== undefined) + vnode.dom.scrollTop = vnode.dom.scrollHeight + }, + + view: vnode => { + return m(".log", {}, bufferLog) + }, +} + +let BufferContainer = { + view: vnode => { + return m('.buffer-container', {}, [ + m('.filler'), + bufferLog !== undefined ? m(Log) : m(Buffer), + ]) + }, +} + +let Input = { + complete: textarea => { + if (textarea.selectionStart !== textarea.selectionEnd) + 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), ])