|
|
|
@ -8,6 +8,8 @@ var vm = require('vm') |
|
|
|
|
var EventManager = require('ethereum-remix').lib.EventManager |
|
|
|
|
var Web3 = require('web3') |
|
|
|
|
|
|
|
|
|
var executionContext = require('../../execution-context') |
|
|
|
|
|
|
|
|
|
var css = csjs` |
|
|
|
|
.panel { |
|
|
|
|
position : relative; |
|
|
|
@ -15,11 +17,12 @@ var css = csjs` |
|
|
|
|
flex-direction : column; |
|
|
|
|
font-size : 12px; |
|
|
|
|
font-family : monospace; |
|
|
|
|
color : white; |
|
|
|
|
background-color : grey; |
|
|
|
|
color : black; |
|
|
|
|
background-color : lightgrey; |
|
|
|
|
margin-top : auto; |
|
|
|
|
height : 100%; |
|
|
|
|
min-height : 1.7em; |
|
|
|
|
overflow : hidden; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.bar { |
|
|
|
@ -52,8 +55,7 @@ var css = csjs` |
|
|
|
|
overflow-y : auto; |
|
|
|
|
font-family : monospace; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.log { |
|
|
|
|
.journal { |
|
|
|
|
margin-top : auto; |
|
|
|
|
font-family : monospace; |
|
|
|
|
} |
|
|
|
@ -63,7 +65,6 @@ var css = csjs` |
|
|
|
|
line-height : 2ch; |
|
|
|
|
margin : 1ch; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.cli { |
|
|
|
|
line-height : 1.7em; |
|
|
|
|
font-family : monospace; |
|
|
|
@ -77,14 +78,26 @@ var css = csjs` |
|
|
|
|
outline : none; |
|
|
|
|
font-family : monospace; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.error { |
|
|
|
|
color : red; |
|
|
|
|
} |
|
|
|
|
.info { |
|
|
|
|
color : blue; |
|
|
|
|
} |
|
|
|
|
.default { |
|
|
|
|
color : white; |
|
|
|
|
.log { |
|
|
|
|
color : black; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.dragbarHorizontal { |
|
|
|
|
position : absolute; |
|
|
|
|
top : 0; |
|
|
|
|
height : 0.5em; |
|
|
|
|
right : 0; |
|
|
|
|
left : 0; |
|
|
|
|
cursor : ns-resize; |
|
|
|
|
z-index : 999; |
|
|
|
|
border-top : 2px solid hsla(215, 81%, 79%, .3); |
|
|
|
|
} |
|
|
|
|
.ghostbar { |
|
|
|
|
position : absolute; |
|
|
|
@ -159,11 +172,12 @@ class Terminal { |
|
|
|
|
} |
|
|
|
|
self.event = new EventManager() |
|
|
|
|
self._api = opts.api |
|
|
|
|
self._view = { panel: null, bar: null, input: null, term: null, log: null, cli: null } |
|
|
|
|
self._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null } |
|
|
|
|
self._templates = {} |
|
|
|
|
self._templates.default = self._blocksRenderer('default') |
|
|
|
|
self._templates.error = self._blocksRenderer('error') |
|
|
|
|
self._templates.info = self._blocksRenderer('info') |
|
|
|
|
self.logger = {} |
|
|
|
|
;['log', 'info', 'error'].forEach(typename => { |
|
|
|
|
self.registerType(typename, self._blocksRenderer(typename)) |
|
|
|
|
}) |
|
|
|
|
self._jsSandboxContext = {} |
|
|
|
|
self._jsSandbox = vm.createContext(self._jsSandboxContext) |
|
|
|
|
if (opts.shell) self._shell = opts.shell |
|
|
|
@ -171,56 +185,161 @@ class Terminal { |
|
|
|
|
} |
|
|
|
|
render () { |
|
|
|
|
var self = this |
|
|
|
|
if (self._view.panel) return self._view.panel |
|
|
|
|
self._view.log = yo`<div class=${css.log}></div>` |
|
|
|
|
if (self._view.el) return self._view.el |
|
|
|
|
self._view.journal = yo`<div class=${css.journal}></div>` |
|
|
|
|
self._view.input = yo` |
|
|
|
|
<span class=${css.input} contenteditable="true" onkeydown=${change}></span> |
|
|
|
|
` |
|
|
|
|
self._view.cli = yo` |
|
|
|
|
<div class=${css.cli} onclick=${e => self._view.input.focus()}> |
|
|
|
|
<div class=${css.cli}> |
|
|
|
|
<span class=${css.prompt}>${'>'}</span> |
|
|
|
|
${self._view.input} |
|
|
|
|
</div> |
|
|
|
|
` |
|
|
|
|
self._view.icon = yo`<i onmouseenter=${hover} onmouseleave=${hover} onmousedown=${minimize} class="${css.minimize} fa fa-angle-double-down"></i>` |
|
|
|
|
self._view.dragbar = yo`<div onmousedown=${mousedown} class=${css.dragbarHorizontal}></div>` |
|
|
|
|
self._view.bar = yo` |
|
|
|
|
<div class=${css.bar} onmousedown=${mousedown}> |
|
|
|
|
<div class=${css.bar}> |
|
|
|
|
${self._view.dragbar} |
|
|
|
|
${self._view.icon} |
|
|
|
|
</div> |
|
|
|
|
` |
|
|
|
|
self._view.term = yo` |
|
|
|
|
<div class=${css.terminal} onscroll=${reattach}> |
|
|
|
|
${self._view.log} |
|
|
|
|
<div class=${css.terminal} onscroll=${throttle(reattach, 50)} onclick=${focusinput}> |
|
|
|
|
${self._view.journal} |
|
|
|
|
${self._view.cli} |
|
|
|
|
</div> |
|
|
|
|
` |
|
|
|
|
self._view.panel = yo` |
|
|
|
|
self._view.el = yo` |
|
|
|
|
<div class=${css.panel}> |
|
|
|
|
${self._view.bar} |
|
|
|
|
${self._view.term} |
|
|
|
|
</div> |
|
|
|
|
` |
|
|
|
|
self.log(self.data.banner) |
|
|
|
|
self._output(self.data.banner) |
|
|
|
|
|
|
|
|
|
function focusinput (event) { |
|
|
|
|
if (self._view.journal.offsetHeight - (self._view.term.scrollTop + self._view.term.offsetHeight) < 50) { |
|
|
|
|
refocus() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
function refocus () { |
|
|
|
|
self._view.input.focus() |
|
|
|
|
reattach({ currentTarget: self._view.term }) |
|
|
|
|
self.scroll2bottom() |
|
|
|
|
} |
|
|
|
|
var css2 = csjs` |
|
|
|
|
.anchor { |
|
|
|
|
position : static; |
|
|
|
|
border-top : 2px dotted blue; |
|
|
|
|
height : 10px; |
|
|
|
|
} |
|
|
|
|
.overlay { |
|
|
|
|
position : absolute; |
|
|
|
|
width : 100%; |
|
|
|
|
display : flex; |
|
|
|
|
align-items : center; |
|
|
|
|
justify-content : center; |
|
|
|
|
bottom : 0; |
|
|
|
|
right : 15px; |
|
|
|
|
height : 20%; |
|
|
|
|
min-height : 50px; |
|
|
|
|
} |
|
|
|
|
.text { |
|
|
|
|
z-index : 2; |
|
|
|
|
color : black; |
|
|
|
|
font-size : 25px; |
|
|
|
|
font-weight : bold; |
|
|
|
|
pointer-events : none; |
|
|
|
|
} |
|
|
|
|
.background { |
|
|
|
|
z-index : 1; |
|
|
|
|
opacity : 0.8; |
|
|
|
|
background-color : #a6aeba; |
|
|
|
|
cursor : pointer; |
|
|
|
|
} |
|
|
|
|
` |
|
|
|
|
var text = yo`<div class="${css2.overlay} ${css2.text}"></div>` |
|
|
|
|
var background = yo`<div class="${css2.overlay} ${css2.background}"></div>` |
|
|
|
|
var placeholder = yo`<div class=${css2.anchor}>${background}${text}</div>` |
|
|
|
|
var inserted = false |
|
|
|
|
|
|
|
|
|
function throttle (fn, wait) { |
|
|
|
|
var time = Date.now() |
|
|
|
|
return function () { |
|
|
|
|
if ((time + wait - Date.now()) < 0) { |
|
|
|
|
fn.apply(this, arguments) |
|
|
|
|
time = Date.now() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
function reattach (event) { |
|
|
|
|
var el = event.currentTarget |
|
|
|
|
var isBottomed = el.scrollHeight - el.scrollTop === el.clientHeight |
|
|
|
|
var isBottomed = el.scrollHeight - el.scrollTop < el.clientHeight + 30 |
|
|
|
|
if (isBottomed) { |
|
|
|
|
if (inserted) { |
|
|
|
|
text.innerText = '' |
|
|
|
|
background.onclick = undefined |
|
|
|
|
self._view.journal.removeChild(placeholder) |
|
|
|
|
} |
|
|
|
|
inserted = false |
|
|
|
|
delete self.scroll2bottom |
|
|
|
|
// @TODO: delete new message indicator
|
|
|
|
|
} else { |
|
|
|
|
self.scroll2bottom = function () { } |
|
|
|
|
// @TODO: while in stopped mode: show indicator about new lines getting logged
|
|
|
|
|
if (!inserted) self._view.journal.appendChild(placeholder) |
|
|
|
|
inserted = true |
|
|
|
|
check() |
|
|
|
|
if (!placeholder.nextElementSibling) { |
|
|
|
|
placeholder.style.display = 'none' |
|
|
|
|
} else { |
|
|
|
|
placeholder.style = '' |
|
|
|
|
} |
|
|
|
|
self.scroll2bottom = function () { |
|
|
|
|
var next = placeholder.nextElementSibling |
|
|
|
|
if (next) { |
|
|
|
|
placeholder.style = '' |
|
|
|
|
check() |
|
|
|
|
var messages = 1 |
|
|
|
|
while ((next = next.nextElementSibling)) messages += 1 |
|
|
|
|
text.innerText = `${messages} new unread log entries` |
|
|
|
|
} else { |
|
|
|
|
placeholder.style.display = 'none' |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
function check () { |
|
|
|
|
var pos1 = self._view.term.offsetHeight + self._view.term.scrollTop - (self._view.el.offsetHeight * 0.15) |
|
|
|
|
var pos2 = placeholder.offsetTop |
|
|
|
|
if ((pos1 - pos2) > 0) { |
|
|
|
|
text.style.display = 'none' |
|
|
|
|
background.style.position = 'relative' |
|
|
|
|
background.style.opacity = 0.3 |
|
|
|
|
background.style.right = 0 |
|
|
|
|
background.style.borderBox = 'content-box' |
|
|
|
|
background.style.padding = '2px' |
|
|
|
|
background.style.height = (self._view.journal.offsetHeight - (placeholder.offsetTop + placeholder.offsetHeight)) + 'px' |
|
|
|
|
background.onclick = undefined |
|
|
|
|
background.style.cursor = 'default' |
|
|
|
|
} else { |
|
|
|
|
background.style = '' |
|
|
|
|
text.style = '' |
|
|
|
|
background.onclick = function (event) { |
|
|
|
|
console.error('background click') |
|
|
|
|
placeholder.scrollIntoView() |
|
|
|
|
check() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
function hover (event) { event.currentTarget.classList.toggle(css.hover) } |
|
|
|
|
function minimize (event) { |
|
|
|
|
event.preventDefault() |
|
|
|
|
event.stopPropagation() |
|
|
|
|
var classList = self._view.icon.classList |
|
|
|
|
classList.toggle('fa-angle-double-down') |
|
|
|
|
classList.toggle('fa-angle-double-up') |
|
|
|
|
self.event.trigger('resize', []) |
|
|
|
|
if (event.button === 0) { |
|
|
|
|
var classList = self._view.icon.classList |
|
|
|
|
classList.toggle('fa-angle-double-down') |
|
|
|
|
classList.toggle('fa-angle-double-up') |
|
|
|
|
self.event.trigger('resize', []) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// ----------------- resizeable ui ---------------
|
|
|
|
|
function mousedown (event) { |
|
|
|
@ -256,7 +375,7 @@ class Terminal { |
|
|
|
|
self.event.trigger('resize', [self._api.getPosition(event)]) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return self._view.panel |
|
|
|
|
return self._view.el |
|
|
|
|
|
|
|
|
|
function change (event) { |
|
|
|
|
if (event.which === 13) { |
|
|
|
@ -301,7 +420,7 @@ class Terminal { |
|
|
|
|
} |
|
|
|
|
_blocksRenderer (mode) { |
|
|
|
|
var self = this |
|
|
|
|
var modes = { log: true, info: true, error: true, default: true } |
|
|
|
|
var modes = { log: true, info: true, error: true } |
|
|
|
|
if (modes[mode]) { |
|
|
|
|
return function render () { |
|
|
|
|
var args = [].slice.call(arguments) |
|
|
|
@ -319,23 +438,44 @@ class Terminal { |
|
|
|
|
throw new Error('mode is not supported') |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
execute (script) { |
|
|
|
|
var self = this |
|
|
|
|
script = String(script) |
|
|
|
|
self._output({ type: 'log', value: `> ${script}` }) |
|
|
|
|
self._shell(script, function (error, output) { |
|
|
|
|
if (error) { |
|
|
|
|
self._output({ type: 'error', value: error }) |
|
|
|
|
return error |
|
|
|
|
} else { |
|
|
|
|
self._output({ type: 'log', value: output }) |
|
|
|
|
return output |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
registerType (typename, template) { |
|
|
|
|
var self = this |
|
|
|
|
if (typeof template !== 'function') throw new Error('invalid template') |
|
|
|
|
self._templates[typename] = template |
|
|
|
|
self.logger[typename] = function () { |
|
|
|
|
var args = [...arguments].map(x => ({ type: typename, value: x })) |
|
|
|
|
self._output.apply(self, args) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
log () { |
|
|
|
|
// @TODO: temporary to not break stuff that uses the old API
|
|
|
|
|
this._output.apply(this, arguments) |
|
|
|
|
} |
|
|
|
|
_output () { |
|
|
|
|
var self = this |
|
|
|
|
var args = [...arguments] |
|
|
|
|
self.data.session.push(args) |
|
|
|
|
args.forEach(function (data) { |
|
|
|
|
if (!data || !data.type) data = { type: 'default', value: data } |
|
|
|
|
if (!data || !data.type) data = { type: 'log', value: data } |
|
|
|
|
var render = self._templates[data.type] |
|
|
|
|
if (!render) render = self._templates.default |
|
|
|
|
var blocks = render(data.value) |
|
|
|
|
blocks = blocks instanceof Array ? blocks : [blocks] |
|
|
|
|
blocks = [].concat(blocks) |
|
|
|
|
blocks.forEach(function (block) { |
|
|
|
|
self._view.log.appendChild(yo` |
|
|
|
|
self._view.journal.appendChild(yo` |
|
|
|
|
<div class="${css.block} ${css[data.type] || data.type}"> |
|
|
|
|
${block} |
|
|
|
|
</div> |
|
|
|
@ -344,41 +484,18 @@ class Terminal { |
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
info () { |
|
|
|
|
var self = this |
|
|
|
|
var args = [...arguments].map(x => ({ type: 'info', value: x })) |
|
|
|
|
self.log.apply(self, args) |
|
|
|
|
} |
|
|
|
|
error () { |
|
|
|
|
var self = this |
|
|
|
|
var args = [...arguments].map(x => ({ type: 'error', value: x })) |
|
|
|
|
self.log.apply(self, args) |
|
|
|
|
} |
|
|
|
|
scroll2bottom () { |
|
|
|
|
var self = this |
|
|
|
|
setTimeout(function () { |
|
|
|
|
self._view.term.scrollTop = self._view.term.scrollHeight |
|
|
|
|
}, 0) |
|
|
|
|
} |
|
|
|
|
execute (input) { |
|
|
|
|
_shell (script, done) { // default shell
|
|
|
|
|
var self = this |
|
|
|
|
input = String(input) |
|
|
|
|
self.log(`> ${input}`) |
|
|
|
|
self._shell(input, function (error, output) { |
|
|
|
|
if (error) { |
|
|
|
|
self.error(error) |
|
|
|
|
return error |
|
|
|
|
} else { |
|
|
|
|
self.log(output) |
|
|
|
|
return output |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
_shell (input, done) { // default shell
|
|
|
|
|
try { |
|
|
|
|
var context = vm.createContext(Object.assign(this._jsSandboxContext, domTerminalFeatures(this))) |
|
|
|
|
var result = vm.runInContext(input, context) |
|
|
|
|
this._jsSandboxContext = Object.assign({}, context) |
|
|
|
|
var context = vm.createContext(Object.assign(self._jsSandboxContext, domTerminalFeatures(self))) |
|
|
|
|
var result = vm.runInContext(script, context) |
|
|
|
|
self._jsSandboxContext = Object.assign({}, context) |
|
|
|
|
done(null, result) |
|
|
|
|
} catch (error) { |
|
|
|
|
done(error.message) |
|
|
|
@ -386,12 +503,12 @@ class Terminal { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// @TODO add all the `console` functions
|
|
|
|
|
function domTerminalFeatures (self) { |
|
|
|
|
// @TODO add all the `console` functions
|
|
|
|
|
return { |
|
|
|
|
web3: self._api.context() !== 'vm' ? new Web3(self._api.web3().currentProvider) : null, |
|
|
|
|
web3: executionContext.getProvider() !== 'vm' ? new Web3(executionContext.web3().currentProvider) : null, |
|
|
|
|
console: { |
|
|
|
|
log: function () { self.log.apply(self, arguments) } |
|
|
|
|
log: function () { self._output.apply(self, arguments) } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|