Last active
May 5, 2022 12:29
-
-
Save xenohunter/005c66e78507278cedfb93dfdc2281b0 to your computer and use it in GitHub Desktop.
Terminal emulator (circa 2014 CE)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var terminal; | |
function init() { | |
var elem = $('#terminal'); | |
if (!elem.size()) { | |
console.log('Error! Something is wrong!'); | |
return false; | |
} else if (elem.data('busy')) { | |
return false; | |
} | |
terminal = new Terminal({ | |
elemID: '#terminal', | |
requisites: { | |
program: 'root', | |
pointer: '# ' | |
}, | |
stackLength: 20, | |
maxMsgAmount: 200 | |
}, [ | |
'-- Welcome to the Poorly Designed Terminal Emulator!', | |
'-- Print `help` or `h` to get some information.', | |
'-- Use `cls` to clear the screen.', | |
'-- To lookup docs, use `docs %command%.`' | |
]); | |
} | |
function type(v) { | |
if (v === null) { | |
return 'null'; | |
} else if (typeof v === 'object') { | |
var toClass = {}.toString; | |
return toClass.call(v).slice(8, -1).toLowerCase(); | |
} else { | |
return typeof v; | |
} | |
} | |
/* TERMINAL */ | |
function Terminal(options, messages) { | |
var self = this; | |
self.HTMLElem = $(options.elemID); | |
self.HTMLElem.data('busy', true); | |
self.context = 0; | |
self.requisites = options.requisites; | |
self.input = Terminal.createInput({ | |
target: self.HTMLElem, | |
requisites: self.requisites, | |
maxMsgAmount: options.maxMsgAmount | |
}); | |
self.stack = Terminal.createStack( | |
self.input.value, | |
options.stackLength || 100 | |
); | |
self.API = { | |
clearScreen: self.input.clearTerminal, | |
inverseColors: function () { | |
self.HTMLElem.toggleClass('inverse'); | |
} | |
}; | |
self.stdin = function (command) { | |
command = command.trim(); | |
self.stack.push(command); | |
self.stdout(command, true); | |
if (!command.length) { | |
self.input.on(); | |
} else if (self.context) { | |
if (type(self.context) === 'object' && self.context.exec) { | |
self.context.exec(command, self.input.on); | |
} else { | |
self.context = 0; | |
self.stdout('-- An error occured!'); | |
self.input.setRequisites(0); | |
self.input.on(); | |
} | |
} else { | |
self.input.setRequisites(0); | |
var cmd = Terminal.parse(command), | |
program = Program.get(cmd.name); | |
if (program) { | |
program(cmd.args, cmd.keys, Terminal.makePrintFunc(self.stdout), function (res) { | |
self.context = res || 0; | |
self.input.setRequisites(self.context ? self.context.requisites : 0); | |
self.input.on(); | |
}, self.API); | |
} else { | |
self.stdout('-- The command is not found.'); | |
self.input.on(); | |
} | |
} | |
}; | |
self.stdout = function (answer, echo) { | |
answer = answer || ''; | |
var elem = $('<div/>', { className: 'stdout ' + (self.context ? 'program' : echo ? 'echo' : 'answer') }), | |
message = echo ? utils.reqsToString(self.context.requisites || self.requisites) : ''; | |
if (type(answer) === 'string') { | |
message += answer; | |
} | |
elem.text(message); | |
self.input.postMessage(elem); | |
}; | |
Terminal.setKeyboardEvents( | |
self.input.value, | |
self.stdin, | |
self.stack.skim | |
); | |
if (messages && messages.length) { | |
setTimeout(function () { | |
var ln = messages.length, i; | |
for (i = 0; i < ln; i++) { | |
self.stdout(messages[i]); | |
} | |
}, 0); | |
} | |
} | |
Terminal.createInput = function (options) { | |
var startReqs = utils.reqsToString(options.requisites); | |
/* elements declaration */ | |
var container = $('<div/>', { | |
className: 'input-container' | |
}), | |
input = $('<div/>', { | |
className: 'input' | |
}), | |
prefix = $('<span/>', { | |
className: 'prefix', | |
text: startReqs | |
}), | |
area = $('<span/>', { | |
className: 'area' | |
}), | |
cursor = $('<span/>', { | |
className: 'cursor', | |
text: '_' | |
}); | |
/* cursor blinking */ | |
(function blink() { | |
setTimeout(function () { | |
if (cursor.is(':visible')) { | |
cursor.hide(); | |
} else { | |
cursor.show(); | |
} | |
blink(); | |
}, 500); | |
}()); | |
/* controls object */ | |
var inputActive = true; | |
var inputManager = { | |
setRequisites: function (newReqs) { | |
if (newReqs) { | |
prefix.text(utils.reqsToString(newReqs)); | |
} else { | |
prefix.text(startReqs); | |
} | |
}, | |
value: function (data, fromStack) { | |
if (!inputActive) return; | |
var val = area.text(); | |
if (typeof data === 'string') { | |
if (fromStack) { | |
area.text(data); | |
} else { | |
area.text(val + data); | |
} | |
} else if (data === -1) { | |
area.text(val.slice(0, -1)); | |
} else { | |
inputActive = false; | |
area.empty(); | |
} | |
return val; | |
}, | |
postMessage: function (elem) { | |
container.append(elem); | |
options.target.scrollTop(container.height()); | |
var messages = container.children(), | |
size = messages.size(); | |
while (size > (options.maxMsgAmount || 100)) { | |
messages.first().remove(); | |
size--; | |
} | |
}, | |
clearTerminal: function () { | |
container.children().remove(); | |
}, | |
on: function () { | |
inputActive = true; | |
} | |
}; | |
input.append(prefix).append(area).append(cursor); | |
input.appendTo(options.target); | |
input.before(container); | |
return inputManager; | |
}; | |
Terminal.createStack = function (value, len) { | |
var stack = [], | |
pos = -1; | |
return { | |
push: function (command) { | |
stack = stack.filter(function (item, index) { | |
return item !== command && index < len - 1; | |
}); | |
stack.unshift(command); | |
}, | |
skim: function (where) { | |
var stackTop = stack.length - 1, | |
BFront = where === 'down' && pos === -1, | |
TFront = where === 'up' && pos === stackTop, | |
val = ''; | |
if (stack.length && !BFront && !TFront) { | |
if (where === 'up' && pos < stackTop) { | |
val = stack[++pos]; | |
} else if (where === 'down' && pos > 0) { | |
val = stack[--pos]; | |
} else if (where === 'down' && pos === 0) { | |
val = ''; | |
pos--; | |
} | |
value(val, true); | |
} | |
} | |
}; | |
}; | |
Terminal.setKeyboardEvents = function (value, execute, stack) { | |
var win = $(window); | |
win.on('keydown', function (e) { | |
var exclude = [9, 35, 36, 37, 39], | |
key = e.keyCode; | |
if (exclude.indexOf(key) !== -1) { | |
return false; | |
} else if (key === 8) { | |
value(-1); | |
return false; | |
} else if (key === 13) { | |
var line = value(); | |
execute(line); | |
} else if (key === 38) { | |
stack('up'); | |
} else if (key === 40) { | |
stack('down'); | |
} | |
}); | |
win.on('keypress', function (e) { | |
if (e.ctrlKey || e.altKey) return; | |
try { | |
value(String.fromCharCode(e.charCode)); | |
} catch (e) {} | |
}); | |
}; | |
Terminal.makePrintFunc = function (stdout) { | |
return function (res) { | |
stdout(res.toString()); | |
}; | |
}; | |
Terminal.parse = function (line) { | |
var rAll = /([^\s'"]{1,32}|(['"])([^\2]*?)\2)+/g, | |
items = [], | |
item, | |
res; | |
while ((item = rAll.exec(line)) !== null) { | |
res = { val: item[3] || item [0] }; | |
if (item.index === 0) { | |
res.type = 'name'; | |
} else if (item[0][0] === '-') { | |
res.type = 'key'; | |
} else { | |
res.type = 'arg'; | |
if (item[3]) { | |
res.string = true; | |
} | |
} | |
items.push(res); | |
} | |
return { | |
name: items[0].val, | |
args: items.map(function (item, i) { | |
if (i && item.type === 'arg') return item.val; | |
}).filter(utils.notNull), | |
keys: items.map(function (item, i) { | |
if (i && item.type === 'key') return item.val; | |
}).filter(utils.notNull), | |
full: items | |
}; | |
}; | |
/* UTILS */ | |
var utils = { | |
reqsToString: function (obj) { | |
var s = ''; | |
s += obj.program || ''; | |
s += obj.pointer; | |
return s; | |
}, | |
notNull: function (item) { | |
return item; | |
} | |
}; | |
/* PROGRAM */ | |
function Program(name, func, props) { | |
var ln = Program.storage.push({ | |
names: name.split(' '), | |
exec: func, | |
props: props | |
}); | |
ln--; | |
Program.storage[ln].exec.index = ln; | |
} | |
Program.createContext = function (func, props, end, async) { | |
return { | |
exec: (function () { | |
var local = {}; | |
return function (input, callback) { | |
if (input === '\\q') { | |
end(); | |
} else { | |
func(input, local, callback); | |
if (!async) callback(); | |
} | |
}; | |
}()), | |
requisites: { | |
program: props.name, | |
pointer: props.pointer || ': ' | |
} | |
} | |
}; | |
Program.innerGet = function (name) { | |
var ln = Program.storage.length, i; | |
for (i = 0; i < ln; i++) { | |
if (Program.storage[i].names.indexOf(name) !== -1) { | |
return Program.storage[i]; | |
} | |
} | |
}; | |
Program.get = function (name) { | |
var program = Program.innerGet(name); | |
if (program) { | |
return program.exec; | |
} | |
}; | |
Program.storage = []; | |
/* PROGRAMS ADDING */ | |
Program('help h', function (args, keys, print, end) { | |
Program.storage.forEach(function (item) { | |
var admin = keys.indexOf('-a') === -1; | |
if (!admin && (!item.props || item.props.hidden)) return; | |
var text = item.names[0]; | |
if (keys.indexOf('-n') !== -1 && item.names.length > 1) { | |
text += ' [' + item.names.slice(1).join('|') + ']'; | |
} | |
text += item.props ? (' - ' + item.props.desc) : ''; | |
print(text); | |
}); | |
end(); | |
}, { | |
desc: 'Prints a list of commands (-n to see alternative names).', | |
docs: 'Use -n to see alternative names of commands; use -a to see hidden commands.' | |
}); | |
Program('docs d', function (args, keys, print, end) { | |
if (!args[0]) { | |
print('-- No program name.'); | |
} else { | |
var program = Program.innerGet(args[0]); | |
if (!program) { | |
print('-- Program name is incorrect.'); | |
} else if (!program.props.docs) { | |
print('-- There is no docs for that program.'); | |
} else { | |
print(program.props.docs); | |
} | |
} | |
end(); | |
}, { | |
desc: 'Prints docs on a command.' | |
}); | |
Program('scripter s', function (args, keys, print, end) { | |
var pre = 'scripter: -- '; | |
print(pre + 'Enter some JS code, then:'); | |
print(pre + '\\r - to run it'); | |
print(pre + '\\s - to save it'); | |
print(pre + '\\c - to clear context'); | |
print(pre + '\\q - to quit'); | |
end(Program.createContext(function (input, local) { | |
if (local.saveName || input === '\\s') { | |
if (!local.code) { | |
print(pre + 'Enter some JS code!'); | |
} else if (!local.saveName) { | |
print(pre + 'Enter the name:'); | |
local.saveName = true; | |
} else { | |
var name = input.match(/^\w+\b/)[0]; | |
if (Program.innerGet(name)) { | |
print(pre + 'Please, enter a different name:') | |
} else { | |
var code = local.code; | |
Program(name, function (args, keys, print, int20h) { | |
var a = args, | |
k = keys, | |
p = print; | |
try { eval(code); } catch (e) { | |
print(e); | |
} | |
int20h(); | |
}); | |
local.code = ''; | |
local.saveName = false; | |
print(pre + 'Your script is saved, context is cleared.'); | |
} | |
} | |
} else if (input === '\\r') { | |
if (local.code) { | |
try { eval(local.code); } catch (e) { | |
print(e); | |
} | |
} | |
} else if (input === '\\c') { | |
local.code = ''; | |
print(pre + 'Context is cleared.'); | |
} else { | |
local.code = local.code || ''; | |
local.code += input; | |
} | |
}, { name: 'scripter' }, end)); | |
}, { | |
desc: 'Allows to run your own JS code.', | |
docs: | |
'If you save a script and then run is, you can pass args to your script. There will be: ' + | |
'`args[]` for simple arguments, `keys[]` for hyphened args, and a function `print` for stdout. ' + | |
'Those args are also available by short names: `a`, `k`, and `p`.' | |
}); | |
Program('create', function (args, keys, print, end) { | |
if (!args[0] || !args[1]) { | |
print('-- Too few arguments.'); | |
end(); | |
} else if (Program.innerGet(args[0])) { | |
print('-- There is already such a command.'); | |
end(); | |
} else { | |
var code = args[1]; | |
Program(args[0], function (args, keys, print, int20h) { | |
var a = args, | |
k = keys, | |
p = print; | |
try { eval(code); } catch (e) { | |
print(e); | |
} | |
int20h(); | |
}); | |
print('-- The command is created!'); | |
end(); | |
} | |
}, { | |
desc: 'Allows to create a JS command in one line.', | |
docs: | |
'The first argument must be a name of a command, the second must be a JS code in quote marks. ' + | |
'There will be: `args[]` for simple arguments, `keys[]` for hyphened args, and a function `print` for stdout. ' + | |
'Those args are also available by short names: `a`, `k`, and `p`.' | |
}); | |
Program('clear cls', function (args, keys, print, end, API) { | |
API.clearScreen(); | |
end(); | |
}, { | |
desc: 'Clears the screen.' | |
}); | |
Program('inverse inv', function (args, keys, print, end, API) { | |
API.inverseColors(); | |
end(); | |
}, { | |
desc: 'Inverses the UI colors.' | |
}); | |
Program('back', function (args, keys, print, end) { | |
history.back(); | |
end(); | |
}, { | |
desc: 'Sends back in browser history.' | |
}); | |
Program('home', function (args, keys, print, end) { | |
location.replace('/'); | |
end(); | |
}, { | |
desc: 'Sends to xenohunter.me.' | |
}); | |
Program('reload renew', function (args, keys, print, end) { | |
location.reload(); | |
end(); | |
}, { | |
desc: 'Reloads page.' | |
}); | |
window.onload = init; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment