Pulled konsole out into it's own module
All checks were successful
Build Website / Build NPM Project (push) Successful in 22s
Build Website / Tag Version (push) Successful in 6s
Build Website / Build & Push Dockerfile (push) Successful in 1m18s

This commit is contained in:
Zakary Timson 2024-01-07 12:29:00 -05:00
parent 42d26503bc
commit 0046dc1cb4
20 changed files with 407 additions and 387 deletions

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import '../misc/konsole';
import '../modules/konsole';
import {onMounted, ref} from 'vue';
const animate = ref(true);
@ -28,48 +28,6 @@ onMounted(async () => {
});
</script>
<style>
.cli-container {
padding: 1rem;
background: #333;
font-family: monospace !important;
overflow-y: auto;
min-height: 150px;
max-height: 300px;
.cli-stdout {
flex-grow: 1;
color: #0f0;
.cli-stdout-line {
padding: 0;
min-height: 1.25rem;
}
}
.cli-stdin {
display: flex;
margin: 0;
.cli-stdin-prompt {
padding-right: 0.55em;
text-wrap: nowrap;
color: #0f0;
}
.cli-stdin-input {
border: none;
outline: none;
font-size: 1rem;
background-color: rgba(0, 0, 0, 0);
color: #0f0;
flex-grow: 1;
padding: 0;
}
}
}
</style>
<template>
<div id="konsole" @click="animate = false"></div>
</template>

View File

@ -1,344 +0,0 @@
window.cli = {
// Element references
_input: null,
_parent: null,
_prompt: null,
_output: null,
// CLI State
_history: [],
_index: 0,
pwd: '/',
hostname: 'virtual',
env: {},
exec: {},
filesystem: {},
user: 'root',
version: '0.3.0',
build: (elementId) => {
window.cli._parent = document.querySelector(elementId);
if(!window.cli._parent)
throw new Error(`Could not create konsole, element "${elementId}" does not exist`);
window.cli._parent.innerHTML = `
<div class="cli-container">
<div class="cli-stdout"></div>
<div class="cli-stdin" onclick="window.cli._input.focus()">
<label for="${elementId}-cli-stdin-input" class="cli-stdin-prompt">${window.cli.prompt()}</label>
<input id="${elementId}-cli-stdin-input" class="cli-stdin-input" type="text" />
</div>
</div>`;
window.cli._input = document.querySelector(elementId + ' .cli-stdin-input');
window.cli._prompt = document.querySelector(elementId + ' .cli-stdin-prompt');
window.cli._output = document.querySelector(elementId + ' .cli-stdout');
window.cli._input.addEventListener('keyup', (e) => {
if(e.key == "Enter") {
window.cli.disable();
if(!!window.cli._input.value) {
window.cli._history.push(window.cli._input.value);
window.cli._index = window.cli._history.length;
window.cli.stdIn(window.cli._input.value)
}
window.cli._input.value = '';
window.cli.enable();
} else if(e.key == 'Up' || e.key == 'ArrowUp') {
if(window.cli._index > 0) window.cli._index--;
window.cli._input.value = window.cli._index == window.cli._history.length ? '' : window.cli._history[window.cli._index];
setTimeout(() => {
const end = window.cli._input.value.length;
window.cli._input.setSelectionRange(end, end);
window.cli._input.focus();
}, 1)
} else if(e.key == 'Down' || e.key == 'ArrowDown') {
if(window.cli._index < window.cli._history.length) window.cli._index++;
window.cli._input.value = window.cli._index == window.cli._history.length ? '' : window.cli._history[window.cli._index];
setTimeout(() => {
const end = window.cli._input.value.length;
window.cli._input.setSelectionRange(end, end);
window.cli._input.focus();
}, 1)
}
});
setTimeout(() => window.cli.exec['banner'].run(), 1);
},
disable: () => {
window.cli._input.disabled = true;
window.cli._prompt.style.visibility = 'hidden';
},
enable: () => {
window.cli._input.disabled = false;
window.cli._input.focus();
window.cli._prompt.style.visibility = 'visible';
},
path: (path=window.cli.pwd) => {
let p = path[0] == '/'? path : (window.cli.pwd + (window.cli.pwd.endsWith('/') ? '' : '/') + path.replace('./', ''))
.replaceAll('//', '/');
const parts = p.split('/').filter(p => !!p);
for(let i = 0; i < parts.length; i++) {
if(parts[i] == '..') {
i--;
parts.splice(i, 2);
i--;
}
}
return '/' + (parts.length ? parts.join('/') : '');
},
prompt: () => `${window.cli.user}@${window.cli.hostname}:${window.cli.pwd}${window.cli.user == 'root' ? '#' : '$'}`,
fs: (path, set) => {
return window.cli.path(path).split('/').filter(p => !!p).reduce((t, p, i, arr) => {
if(!t?.hasOwnProperty(p)) {
if(set == undefined) return undefined;
t[p] = {};
}
if(set !== undefined && i == arr.length - 1) {
if(set == null) delete t[p];
else t[p] = set;
}
return t[p];
}, window.cli.filesystem);
},
stdErr: (text) => {
const p = document.createElement('p');
p.classList.add('cli-stdout-line');
p.classList.add('cli-stdout-error');
p.innerText = text;
window.cli._output.appendChild(p);
},
stdIn:(command, silent=false) => {
(Array.isArray(command) ? command.join(' ') : command).split(';').filter(c => !!c).forEach(c => {
const parts = c.match(/(?:[^\s"]+|"[^"]*")+/g);
if(!parts) return;
const exec = window.cli.exec[parts[0]];
if(!exec?.run) {
window.cli.stdErr(`${window.cli.prompt()} ${command}\n${parts[0]}: command not found`);
} else {
try {
const args = parts.slice(1).map(a => (a[0] == '"' || a[0] == "'") ? a.slice(1, -1) : a);
const out = exec.run(args);
if(!silent) window.cli.stdOut(`${window.cli.prompt()} ${command}${out ? '\n' + out : ''}`);
} catch(err) {
console.error(err);
window.cli.stdErr(`${window.cli.prompt()} ${command}\n${err.message || `${parts[0]}: exited with a non-zero status`}`);
}
}
});
},
stdOut: (text, html=false) => {
const p = document.createElement('p');
p.classList.add('cli-stdout-line');
p[html ? 'innerHTML' : 'innerText'] = text;
window.cli._output.appendChild(p);
},
type: (text, speed=150) => {
let counter = 0;
return new Promise(res => {
let typing = setInterval(() => {
if(counter < text.length) {
window.cli._input.value += text[counter];
} else {
clearInterval(typing);
setTimeout(() => {
window.cli.stdIn(text);
window.cli._input.value = '';
res();
}, 750);
}
counter++;
}, speed);
});
}
};
window.cli.exec['banner'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Display login banner';
},
run: args => {
window.cli.stdOut(`Konsole ${window.cli.version} LTS ${window.cli.hostname} tty1\n\n${window.cli.hostname} login: ${window.cli.user}\npassword:\n\n`);
}
}
window.cli.exec['cd'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Clear console output';
},
run: args => {
const path = window.cli.fs(args[0]);
if(!path) throw new Error(`cd: \'${args[0]}\': No such file or directory`)
window.cli.pwd = window.cli.path(args[0]);
window.cli._prompt.innerText = window.cli.prompt();
}
}
window.cli.exec['clear'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Clear console output';
},
run: args => {
setTimeout(() => window.cli._output.innerHTML = '', 1);
}
}
window.cli.exec['date'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get current date & time';
},
run: args => {
return (new Date()).toLocaleString();
}
}
window.cli.exec['echo'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Output text to console';
},
run: args => {
return args.join(' ');
}
}
window.cli.exec['exit'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'End session';
},
run: args => {
window.cli.stdIn('clear');
window.cli._history = [];
window.cli._index = 0;
window.cli.stdIn('banner');
}
}
window.cli.exec['help'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Display all commands';
},
run: args => {
return `Konsole v${window.cli.version} - A prototype bash emulator written by Zakary Timson\n\n` +
Object.keys(window.cli.exec).map(command => `${command} - ${window.cli.exec[command].help()}`).join('\n');
}
}
window.cli.exec['hostname'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get computer hostname';
},
run: args => {
return window.cli.hostname;
}
}
window.cli.exec['ls'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Display directory contents';
},
run: args => {
const target = window.cli.fs(args[0]);
if(!target) throw new Error(`ls: cannot access \'${args[0]}\': No such file or directory`)
return Object.keys(target).reduce((acc, p) => acc + `${p}\n`, '');
}
}
window.cli.exec['man'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'View command\'s manual';
},
run: args => {
return window.cli.exec[args[0]].help();
}
}
window.cli.exec['mkdir'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Create new directory';
},
run: args => {
if(!args[0]) throw new Error('mkdir: missing operand');
window.cli.fs(args[0], {});
}
}
window.cli.exec['pwd'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get present working directory';
},
run: args => {
return window.cli.pwd;
}
}
window.cli.exec['rm'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Delete file or directory';
},
run: args => {
if(!args[0]) throw new Error('rm: missing operand');
window.cli.fs(args[0], null);
}
}
window.cli.exec['shower-thought'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Random shower thought';
},
run: args => {
const motd = [
'Why do kamikaze pilots wear helmets?',
'How are giraffes real, but unicorns made up',
'When you are a kid you don\'t realize you are also watching your parents grow up',
'Some one at Google was like "Yea, just have someone drive down every road on earth!"',
'The number of people older than you never goes up',
'When you brush your teeth you are cleaning your skeleton',
'Pregnancy is like a group project where one person get\'s stuck with all the work',
'If the universe wasn\'t infinite it would be even scarier',
'Either we are alone in the universe or we are not. both are terrifying',
'The object of golf is to play the least amount of golf.'
];
return motd[~~(Math.random() * motd.length)];
}
}
window.cli.exec['whoami'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get current user account';
},
run: args => {
return window.cli.user;
}
}

View File

@ -0,0 +1,11 @@
window.cli.exec['banner'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Display login banner';
},
run: args => {
return `Konsole ${window.cli.version} LTS ${window.cli.hostname} tty1\n\n${window.cli.hostname} login: ${window.cli.user}\npassword:\n\n`;
}
}

View File

@ -0,0 +1,15 @@
window.cli.exec['cd'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Clear console output';
},
run: args => {
const path = window.cli.fs(args[0]);
if(!path) throw new Error(`cd: \'${args[0]}\': No such file or directory`)
window.cli.pwd = window.cli.path(args[0]);
window.cli._prompt.innerText = window.cli._buildPrompt();
}
}

View File

@ -0,0 +1,11 @@
window.cli.exec['clear'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Clear console output';
},
run: args => {
setTimeout(() => window.cli._output.innerHTML = '', 1);
}
}

View File

@ -0,0 +1,11 @@
window.cli.exec['date'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get current date & time';
},
run: args => {
return new Date().toLocaleString();
}
}

View File

@ -0,0 +1,11 @@
window.cli.exec['echo'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Output text to console';
},
run: args => {
return args.join(' ');
}
}

View File

@ -0,0 +1,16 @@
window.cli.exec['exit'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'End session';
},
run: args => {
setTimeout(() => {
window.cli._history = [];
window.cli._index = 0;
window.cli._output.innerHTML = ''
window.cli.stdOut(window.cli.exec['banner'].run());
}, 1);
}
}

View File

@ -0,0 +1,12 @@
window.cli.exec['help'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Display all commands';
},
run: args => {
return `Konsole v${window.cli.version} - an experimental bash emulator written in JavaScript\nCreated By: Zakary Timson\n\n` +
Object.keys(window.cli.exec).map(command => `${command} - ${window.cli.exec[command].help()}`).join('\n');
}
}

View File

@ -0,0 +1,11 @@
window.cli.exec['hostname'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get computer hostname';
},
run: args => {
return window.cli.hostname;
}
}

View File

@ -0,0 +1,13 @@
window.cli.exec['ls'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Display directory contents';
},
run: args => {
const target = window.cli.fs(args[0]);
if(!target) throw new Error(`ls: cannot access \'${args[0]}\': No such file or directory`)
return Object.keys(target).reduce((acc, p) => acc + `${p}\n`, '');
}
}

View File

@ -0,0 +1,11 @@
window.cli.exec['man'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'View command\'s manual';
},
run: args => {
return window.cli.exec[args[0]].help();
}
}

View File

@ -0,0 +1,12 @@
window.cli.exec['mkdir'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Create new directory';
},
run: args => {
if(!args[0]) throw new Error('mkdir: missing operand');
window.cli.fs(args[0], {});
}
}

View File

@ -0,0 +1,11 @@
window.cli.exec['pwd'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get present working directory';
},
run: args => {
return window.cli.pwd;
}
}

View File

@ -0,0 +1,12 @@
window.cli.exec['rm'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Delete file or directory';
},
run: args => {
if(!args[0]) throw new Error('rm: missing operand');
window.cli.fs(args[0], null);
}
}

View File

@ -0,0 +1,23 @@
window.cli.exec['shower-thought'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Random shower thought';
},
run: args => {
const motd = [
'Why do kamikaze pilots wear helmets?',
'How are giraffes real, but unicorns made up',
'When you are a kid you don\'t realize you are also watching your parents grow up',
'Some one at Google was like "Yea, just have someone drive down every road on earth!"',
'The number of people older than you never goes up',
'When you brush your teeth you are cleaning your skeleton',
'Pregnancy is like a group project where one person get\'s stuck with all the work',
'If the universe wasn\'t infinite it would be even scarier',
'Either we are alone in the universe or we are not. both are terrifying',
'The object of golf is to play the least amount of golf.'
];
return motd[~~(Math.random() * motd.length)];
}
}

View File

@ -0,0 +1,11 @@
window.cli.exec['whoami'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get current user account';
},
run: args => {
return window.cli.user;
}
}

View File

@ -0,0 +1,19 @@
import './konsole.js';
import './konsole.css';
// CLI Commands
import './commands/banner.js';
import './commands/cd.js'
import './commands/clear.js';
import './commands/date.js';
import './commands/echo.js';
import './commands/exit.js';
import './commands/help.js';
import './commands/hostname.js';
import './commands/ls.js';
import './commands/man.js';
import './commands/mkdir.js';
import './commands/pwd.js';
import './commands/rm.js';
import './commands/shower-thoughts.js';
import './commands/whoami.js';

View File

@ -0,0 +1,39 @@
.cli-container {
padding: 1em;
background: #333;
font-family: monospace !important;
overflow-y: auto;
min-height: 150px;
max-height: 300px;
}
.cli-stdout {
flex-grow: 1;
color: #0f0;
}
.cli-stdout-line {
padding: 0;
margin: 0;
min-height: 1em;
}
.cli-stdin {
display: flex;
}
.cli-stdin-prompt {
padding-right: 0.5em;
text-wrap: nowrap;
color: #0f0;
}
.cli-stdin-input {
border: none;
outline: none;
font-size: 1em;
background-color: rgba(0, 0, 0, 0);
color: #0f0;
flex-grow: 1;
padding: 0;
}

View File

@ -0,0 +1,157 @@
window.cli = {
// Element references
_input: null,
_parent: null,
_prompt: null,
_output: null,
// CLI State
_history: [],
_index: 0,
pwd: '/',
hostname: 'virtual',
env: {},
exec: {},
filesystem: {},
user: 'root',
version: '0.3.0',
_buildPrompt: () => `${window.cli.user}@${window.cli.hostname}:${window.cli.pwd}${window.cli.user == 'root' ? '#' : '$'}`,
build: (elementId) => {
window.cli._parent = document.querySelector(elementId);
if(!window.cli._parent)
throw new Error(`Could not create konsole, element "${elementId}" does not exist`);
window.cli._parent.innerHTML = `
<div class="cli-container">
<div class="cli-stdout"></div>
<div class="cli-stdin" onclick="window.cli._input.focus()">
<label for="${elementId}-cli-stdin-input" class="cli-stdin-prompt">${window.cli._buildPrompt()}</label>
<input id="${elementId}-cli-stdin-input" class="cli-stdin-input" type="text" />
</div>
</div>`;
window.cli._input = document.querySelector(elementId + ' .cli-stdin-input');
window.cli._prompt = document.querySelector(elementId + ' .cli-stdin-prompt');
window.cli._output = document.querySelector(elementId + ' .cli-stdout');
window.cli._input.addEventListener('keyup', (e) => {
if(e.key == "Enter") {
window.cli.disable();
if(!!window.cli._input.value) {
window.cli._history.push(window.cli._input.value);
window.cli._index = window.cli._history.length;
window.cli.stdIn(window.cli._input.value)
}
window.cli._input.value = '';
window.cli.enable();
} else if(e.key == 'Up' || e.key == 'ArrowUp') {
if(window.cli._index > 0) window.cli._index--;
window.cli._input.value = window.cli._index == window.cli._history.length ? '' : window.cli._history[window.cli._index];
setTimeout(() => {
const end = window.cli._input.value.length;
window.cli._input.setSelectionRange(end, end);
window.cli._input.focus();
}, 1)
} else if(e.key == 'Down' || e.key == 'ArrowDown') {
if(window.cli._index < window.cli._history.length) window.cli._index++;
window.cli._input.value = window.cli._index == window.cli._history.length ? '' : window.cli._history[window.cli._index];
setTimeout(() => {
const end = window.cli._input.value.length;
window.cli._input.setSelectionRange(end, end);
window.cli._input.focus();
}, 1)
}
});
setTimeout(() => window.cli.stdOut(window.cli.exec['banner'].run()), 1);
},
disable: () => {
window.cli._input.disabled = true;
window.cli._prompt.style.visibility = 'hidden';
},
enable: () => {
window.cli._input.disabled = false;
window.cli._input.focus();
window.cli._prompt.style.visibility = 'visible';
},
path: (path=window.cli.pwd) => {
let p = path[0] == '/'? path : (window.cli.pwd + (window.cli.pwd.endsWith('/') ? '' : '/') + path.replace('./', ''))
.replaceAll('//', '/');
const parts = p.split('/').filter(p => !!p);
for(let i = 0; i < parts.length; i++) {
if(parts[i] == '..') {
i--;
parts.splice(i, 2);
i--;
}
}
return '/' + (parts.length ? parts.join('/') : '');
},
fs: (path, set) => {
return window.cli.path(path).split('/').filter(p => !!p).reduce((t, p, i, arr) => {
if(!t?.hasOwnProperty(p)) {
if(set == undefined) return undefined;
t[p] = {};
}
if(set !== undefined && i == arr.length - 1) {
if(set == null) delete t[p];
else t[p] = set;
}
return t[p];
}, window.cli.filesystem);
},
stdErr: (text) => {
const p = document.createElement('p');
p.classList.add('cli-stdout-line');
p.classList.add('cli-stdout-error');
p.innerText = text;
window.cli._output.appendChild(p);
},
stdIn:(command, suppress=false) => {
(Array.isArray(command) ? command.join(' ') : command).split(';').filter(c => !!c).forEach(c => {
const parts = c.match(/(?:[^\s"]+|"[^"]*")+/g);
if(!parts) return;
const exec = window.cli.exec[parts[0]];
if(!exec?.run) {
if(!suppress) window.cli.stdErr(`${window.cli._buildPrompt()} ${command}\n${parts[0]}: command not found`);
} else {
try {
const args = parts.slice(1).map(a => (a[0] == '"' || a[0] == "'") ? a.slice(1, -1) : a);
const out = exec.run(args);
if(!suppress) window.cli.stdOut(`${window.cli._buildPrompt()} ${command}${out ? '\n' + out : ''}`);
} catch(err) {
console.error(err);
if(!suppress) {
window.cli._output.removeChild(window.cli._output.children[window.cli._output.children.length - 1]);
window.cli.stdErr(`${window.cli._buildPrompt()} ${command}\n${err.message || `${parts[0]}: exited with a non-zero status`}`);
}
}
}
});
},
stdOut: (text='', html=false) => {
const p = document.createElement('p');
p.classList.add('cli-stdout-line');
p[html ? 'innerHTML' : 'innerText'] = text;
window.cli._output.appendChild(p);
},
type: (text, speed=150) => {
let counter = 0;
return new Promise(res => {
let typing = setInterval(() => {
if(counter < text.length) {
window.cli._input.value += text[counter];
} else {
clearInterval(typing);
setTimeout(() => {
window.cli.stdIn(text);
window.cli._input.value = '';
res();
}, 750);
}
counter++;
}, speed);
});
}
};