xP: clean up
This commit is contained in:
		
							
								
								
									
										356
									
								
								xP/public/xP.js
									
									
									
									
									
								
							
							
						
						
									
										356
									
								
								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),
 | 
			
		||||
		])
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user