Skip to content

Instantly share code, notes, and snippets.

@xenohunter
Last active May 5, 2022 12:29
Show Gist options
  • Save xenohunter/005c66e78507278cedfb93dfdc2281b0 to your computer and use it in GitHub Desktop.
Save xenohunter/005c66e78507278cedfb93dfdc2281b0 to your computer and use it in GitHub Desktop.
Terminal emulator (circa 2014 CE)
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