mirror of
https://codeberg.org/JasterV/terminal-vm.git
synced 2026-04-26 18:10:08 +00:00
first commit
This commit is contained in:
commit
04b4c365a1
7 changed files with 1095 additions and 0 deletions
120
README.md
Normal file
120
README.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# JS Terminal
|
||||||
|
|
||||||
|
This project is based on 2 main objects which allow me to work in a very easy way with the terminal and all the file structure.
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
As we are working on the implementation of a terminal which allows to execute commands, I thought of creating an object which would provide me with all the basic functionalities expected from such a terminal like executing a command, checking if a command exists, printing results in the terminal etc.
|
||||||
|
|
||||||
|
For the implementation of each command, I created an object which contains as keys the names of each command and as value all the information about that command including a *run* function to execute it.
|
||||||
|
|
||||||
|
This way, to execute any command entered by the user I only need to:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
cli.run(command, params);
|
||||||
|
```
|
||||||
|
|
||||||
|
And the object *cli* will be in charge of executing:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
commands[command].run(params);
|
||||||
|
```
|
||||||
|
|
||||||
|
The commands accepted by this terminal are: ```**pwd**, **ls**, **cd**, **mkdir**, **echo**, **cat**, **rm**, **mv**, **clear**, **help**, **man**, **urbandict**, **js**```,
|
||||||
|
|
||||||
|
To work with paths, files and directories I implemented an object called *filesTree* which provides me with all the functionalities I need to work with a file hierarchy.
|
||||||
|
|
||||||
|
## Files Tree
|
||||||
|
|
||||||
|
To develop a terminal that works with files and directories, first I proposed a tree type object that could perform operations such as adding nodes, removing nodes, moving nodes, searching nodes, etc.
|
||||||
|
|
||||||
|
This structure is based on a set of nodes, where each node contains a reference to the parent node and, in the case of a directory, references to the children.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Then I can implement in a very fast and simple way any command that is requested as well as *ls* (list child nodes), *mkdir* (create a new directory type node), *mv* (move a node) etc.
|
||||||
|
|
||||||
|
In addition, working with a tree structure allows me to navigate between the nodes using very simple recursive functions.
|
||||||
|
|
||||||
|
## Local Storage
|
||||||
|
|
||||||
|
In the *local storage* both the file tree and the history of entered commands are stored.
|
||||||
|
To be able to save a circular tree structure like the one we have created (Children nodes save references from the parents and vice versa), we cannot convert to *string* the object directly.
|
||||||
|
To do that, I have implemented 2 intermediate steps, *serialize* and *deserialize*.
|
||||||
|
|
||||||
|
To serialize the tree, I go through it recursively exchanging all the references to an object for an id that represents that object, and I store this id-object pair in a new object.
|
||||||
|
This way I can save this new object I create in the *local storage*.
|
||||||
|
|
||||||
|
To deserialize the tree we do the opposite process. We go through this new object and transform each id to the object it identifies, creating again a tree structure.
|
||||||
|
|
||||||
|
## Keyboard shortcuts
|
||||||
|
|
||||||
|
+ Up Arrow: See the previous command entered
|
||||||
|
+ Down Arrow: See the following entered command
|
||||||
|
+ Ctrl + L: Clear Terminal
|
||||||
|
+ Tab: Autocomplete with the following available file/directory
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
To save each entered command we use an array which we save in the *local storage* every time we enter a new command.
|
||||||
|
|
||||||
|
## Urban Dictionary API
|
||||||
|
|
||||||
|
To call this api from the terminal I have created a command called *urbandict* which receives a word as a parameter.
|
||||||
|
|
||||||
|
This [API](https://rapidapi.com/community/api/urban-dictionary) receives the word that the user has written and returns a list with all the existing definitions in the *Urban Dictionary* web of that word.
|
||||||
|
|
||||||
|
ENDPOINT: ```https://mashape-community-urban-dictionary.p.rapidapi.com/define```
|
||||||
|
|
||||||
|
To ask for the definitions of a word we add that word in the *term* parameter:
|
||||||
|
|
||||||
|
```https://mashape-community-urban-dictionary.p.rapidapi.com/define?term=hair```
|
||||||
|
|
||||||
|
### API Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"definition": "A separate creature that happens to live on your head, hard to [tame]. Ferociously attacking it with scissors, dye or hairproducts may [euthanize] said beast for a short while but beware of [angering] it.",
|
||||||
|
"permalink": "http://hair.urbanup.com/4950762",
|
||||||
|
"thumbs_up": 186,
|
||||||
|
"sound_urls": [],
|
||||||
|
"author": "nofu",
|
||||||
|
"word": "hair",
|
||||||
|
"defid": 4950762,
|
||||||
|
"current_vote": "",
|
||||||
|
"written_on": "2010-05-09T00:00:00.000Z",
|
||||||
|
"example": "[Lizzie] tried hot-ironing her hair to [submission] but oh [woes], it was raining and the beast came out on top in the end anyway.",
|
||||||
|
"thumbs_down": 52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"definition": "A growing substance found mostly [on the head]. Hair is largely [amino] [acid] based and can come in a veriety of colours e.g. brown, ginger, black or blonde.",
|
||||||
|
"permalink": "http://hair.urbanup.com/109686",
|
||||||
|
"thumbs_up": 527,
|
||||||
|
"sound_urls": [],
|
||||||
|
"author": "Jim Hodgson",
|
||||||
|
"word": "hair",
|
||||||
|
"defid": 109686,
|
||||||
|
"current_vote": "",
|
||||||
|
"written_on": "2003-04-28T00:00:00.000Z",
|
||||||
|
"example": "[Tomorrow] I will [comb] [my hair]",
|
||||||
|
"thumbs_down": 236
|
||||||
|
},
|
||||||
|
...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Incidents Record
|
||||||
|
|
||||||
|
+ Dividing a *path* into *tail* and *name* has given many problems as there are many *corner cases* that are hard to find
|
||||||
|
+ Auto complete with *tab* has given some strange behavior but is not a problem for the execution
|
||||||
|
+ Moving a directory gave problems if the current directory was underneath the moving node. This is now fixed
|
||||||
|
+ Saving the tree in the local storage gave an error. I have solved it through a serialization process
|
||||||
|
|
||||||
|
## Lessons Learnt Record
|
||||||
|
|
||||||
|
+ Creation of regex for the split of a valid linux path
|
||||||
|
+ Serializing and deserializing a tree in a recursive way
|
||||||
|
+ I have been able to implement one of my favorite data structures since its operations are recursive by nature and I have had a great time :)
|
||||||
BIN
assets/tree_diagram.png
Normal file
BIN
assets/tree_diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
44
index.html
Normal file
44
index.html
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>JS CLI</title>
|
||||||
|
|
||||||
|
<!-- import the webpage's stylesheet -->
|
||||||
|
<link rel="stylesheet" href="src/style.css" />
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"
|
||||||
|
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||||
|
<script src="src/scripts/filesTree.js"></script>
|
||||||
|
<script src="src/scripts/cli.js"></script>
|
||||||
|
<script src="src/scripts/script.js" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>JS CLI</h1>
|
||||||
|
|
||||||
|
<div class="cli">
|
||||||
|
<div class="cli-hd">
|
||||||
|
<div class="circle"></div>
|
||||||
|
<div class="circle"></div>
|
||||||
|
<div class="circle"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cli-out">
|
||||||
|
<div class="cli-inp" id="main-inp">
|
||||||
|
<p class="route"><span>/</span> ></p>
|
||||||
|
<input type="text" spellcheck="false" autofocus>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
500
src/scripts/cli.js
Normal file
500
src/scripts/cli.js
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
function createCli() {
|
||||||
|
/** PRIVATE ATTRIBUTES */
|
||||||
|
var tree = getFilesFromStorage();
|
||||||
|
var history = getHistory();
|
||||||
|
var historyIndex = -1;
|
||||||
|
var tabs = [];
|
||||||
|
var tabsIndex = 0;
|
||||||
|
var commands = {
|
||||||
|
ls: {
|
||||||
|
run: ls,
|
||||||
|
name: "ls - list directory contents",
|
||||||
|
synopsis: "ls [OPTION] [FILE]",
|
||||||
|
description: "List information about the FILEs (the current directory by default)" +
|
||||||
|
"<br>" + "-R, list subdirectories recursively" +
|
||||||
|
"<br>" + "-S, sort by file size, largest first" +
|
||||||
|
"<br>" + "-t, sort by modification time, newest first",
|
||||||
|
},
|
||||||
|
pwd: {
|
||||||
|
run: pwd,
|
||||||
|
name: "pwd - print name of current/working directory",
|
||||||
|
synopsis: "pwd",
|
||||||
|
description: "Print the full filename of the current working directory."
|
||||||
|
},
|
||||||
|
cd: {
|
||||||
|
run: cd,
|
||||||
|
name: "cd - move to another directory",
|
||||||
|
synopsis: "cd [PATH]",
|
||||||
|
description: "Move to the specified path (the root directory by default)"
|
||||||
|
},
|
||||||
|
mkdir: {
|
||||||
|
run: function (params) {
|
||||||
|
mkdir(params);
|
||||||
|
saveFiles();
|
||||||
|
},
|
||||||
|
name: "mkdir - make directories",
|
||||||
|
synopsis: "mkdir DIRECTORY",
|
||||||
|
description: "Create the DIRECTORY(ies), if they do not already exist."
|
||||||
|
},
|
||||||
|
echo: {
|
||||||
|
run: function (params) {
|
||||||
|
echo(params);
|
||||||
|
saveFiles();
|
||||||
|
},
|
||||||
|
name: "echo - display a line of text",
|
||||||
|
synopsis: "echo [STRING] | echo [STRING] > [FILE]",
|
||||||
|
description: "Echo the STRING(s) to standard output. Can be used with the '>' and '>>' operators to create a new file."
|
||||||
|
},
|
||||||
|
cat: {
|
||||||
|
run: cat,
|
||||||
|
name: "cat - print file contents on the standard output",
|
||||||
|
synopsis: "cat [FILE]",
|
||||||
|
description: "Echo a FILE to standard output"
|
||||||
|
},
|
||||||
|
rm: {
|
||||||
|
run: function (params) {
|
||||||
|
rm(params);
|
||||||
|
saveFiles();
|
||||||
|
},
|
||||||
|
name: "rm - remove files or directories",
|
||||||
|
synopsis: "rm [PATH]",
|
||||||
|
description: "rm removes the specified file/directory. It can't remove parent directories or the current directory."
|
||||||
|
},
|
||||||
|
mv: {
|
||||||
|
run: function (params) {
|
||||||
|
mv(params);
|
||||||
|
saveFiles();
|
||||||
|
},
|
||||||
|
name: "mv - move (rename) files",
|
||||||
|
synopsis: "mv SOURCE DEST | mv SOURCE DIRECTORY",
|
||||||
|
description: "Rename SOURCE to DEST, or move SOURCE to DIRECTORY."
|
||||||
|
},
|
||||||
|
clear: {
|
||||||
|
run: clear,
|
||||||
|
name: "clear - clear the terminal screen",
|
||||||
|
synopsis: "clear",
|
||||||
|
description: "clear clears your screen if this is possible."
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
run: help,
|
||||||
|
name: "help - display a brief explanation of each command",
|
||||||
|
synopsis: "help",
|
||||||
|
description: "Display a shorthand manual with all the commands available"
|
||||||
|
},
|
||||||
|
man: {
|
||||||
|
run: man,
|
||||||
|
name: "man - an interface to the system reference manuals",
|
||||||
|
synopsis: "man [COMMAND]",
|
||||||
|
description: "man is the system's manual pager. Each page argument given to man is normally the name of a program, utility or function. The manual page associated with each of these arguments is then found and displayed",
|
||||||
|
},
|
||||||
|
urbandict: {
|
||||||
|
run: function (params) {
|
||||||
|
urbandict(params);
|
||||||
|
},
|
||||||
|
name: "urbandict - search definitions of the given word",
|
||||||
|
synopsis: "urbandict [STRING]",
|
||||||
|
description: "Make a GET request to the Urban Dictionary API (https://www.urbandictionary.com/) and echo the results to standard output.",
|
||||||
|
},
|
||||||
|
js: {
|
||||||
|
run: function (params) {
|
||||||
|
js(params);
|
||||||
|
},
|
||||||
|
name: "js - evaluate the given file",
|
||||||
|
synopsis: "js [FILE]",
|
||||||
|
description: "evaluates the content of a given js file and shows the result of the evaluation",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUBLIC METHODS */
|
||||||
|
|
||||||
|
return {
|
||||||
|
has(command) {
|
||||||
|
return command in commands;
|
||||||
|
},
|
||||||
|
run(command, params) {
|
||||||
|
if (params == undefined) params = [];
|
||||||
|
commands[command].run(params);
|
||||||
|
},
|
||||||
|
newLine: newLine,
|
||||||
|
getInputValue: getInputValue,
|
||||||
|
historyUp() {
|
||||||
|
if (historyIndex < history.length - 1) {
|
||||||
|
historyIndex += 1;
|
||||||
|
setInputValue(history[historyIndex]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
historyDown() {
|
||||||
|
if (historyIndex >= 0) {
|
||||||
|
historyIndex -= 1;
|
||||||
|
setInputValue(history[historyIndex]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addToHistory(value) {
|
||||||
|
if (value != "") {
|
||||||
|
history.unshift(value);
|
||||||
|
saveHistory();
|
||||||
|
}
|
||||||
|
historyIndex = -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete() {
|
||||||
|
var value = getInputValue();
|
||||||
|
var tokens = value.split(' ');
|
||||||
|
var parent = tree.currentNode();
|
||||||
|
var path = splitPath(tree.currentPath());
|
||||||
|
if (value.length > 0) {
|
||||||
|
if (tokens.length > 1) {
|
||||||
|
path = splitPath(tokens[tokens.length - 1])
|
||||||
|
parent = tree.findNode(path[0]);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var child = getNextAvailableChild(parent, path[1]);
|
||||||
|
var newPath = autocompleteTail(path[0], child.name);
|
||||||
|
if (tokens.length > 1) tokens[tokens.length - 1] = newPath;
|
||||||
|
else tokens.push(child.name);
|
||||||
|
setInputValue(tokens.join(' '));
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetTab() {
|
||||||
|
tabs = [];
|
||||||
|
tabsIndex = 0;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PRIVATE METHODS */
|
||||||
|
|
||||||
|
function js(params) {
|
||||||
|
if (params.length != 1) {
|
||||||
|
newLine("USAGE: js [FILE]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var path = params[0]
|
||||||
|
var file = tree.findNode(path)
|
||||||
|
try {
|
||||||
|
var result = tree.evaluateFile(file);
|
||||||
|
newLine(result);
|
||||||
|
} catch (error) {
|
||||||
|
newLine(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function urbandict(params) {
|
||||||
|
if (params.length != 1) {
|
||||||
|
newLine("USAGE: urbandict [STRING]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var word = params[0];
|
||||||
|
getMeanings(word).then(function (response) {
|
||||||
|
if (response.length == 0) {
|
||||||
|
newLine("No definitions were found for " + word);
|
||||||
|
} else {
|
||||||
|
newLine()
|
||||||
|
response.forEach(function (item) {
|
||||||
|
cliLog("<strong>DEFINITION</strong>")
|
||||||
|
cliLog(item.definition.replace(/[\[\]]/g, ''));
|
||||||
|
cliLog("<strong>EXAMPLE</strong>");
|
||||||
|
cliLog(item.example.replace(/[\[\]]/g, ''));
|
||||||
|
cliLog("<strong>REFERENCE</strong>")
|
||||||
|
cliLog(item.permalink)
|
||||||
|
cliLog("<strong>- - - - - - - - - -</strong>")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch(function (error) {
|
||||||
|
newLine(error);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function help(params) {
|
||||||
|
if (params.length > 0) {
|
||||||
|
newLine("Usage: help");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newLine();
|
||||||
|
for (var key of Object.keys(commands))
|
||||||
|
cliLog(commands[key].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function man(params) {
|
||||||
|
if (params.length != 1) {
|
||||||
|
newLine("Usage: man [COMMAND]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var command = params[0];
|
||||||
|
if (!(command in commands)) {
|
||||||
|
newLine("Error: " + command + " is not a valid command");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
command = commands[command];
|
||||||
|
newLine();
|
||||||
|
cliLog("<strong>NAME</strong>")
|
||||||
|
cliLog(command.name);
|
||||||
|
cliLog("<strong>SYNOPSIS</strong>");
|
||||||
|
cliLog(command.synopsis);
|
||||||
|
cliLog("<strong>DESCRIPTION</strong>");
|
||||||
|
cliLog(command.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ls(params) {
|
||||||
|
if (params.length > 2) {
|
||||||
|
newLine("Usage: ls [option] [path]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var flag = undefined;
|
||||||
|
var dir = tree.currentNode();
|
||||||
|
if (params.length == 1) {
|
||||||
|
if (params[0].startsWith("-")) flag = params[0]
|
||||||
|
else dir = tree.findNode(params[0])
|
||||||
|
} else if (params.length == 2) {
|
||||||
|
flag = params[0]
|
||||||
|
dir = tree.findNode(params[1])
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var result = tree.listNode(dir, flag);
|
||||||
|
newLine()
|
||||||
|
if (flag == "-R")
|
||||||
|
for (var elem of result) cliLog(elem[1] + elem[0])
|
||||||
|
else
|
||||||
|
for (var elem of result) cliLog(elem.name)
|
||||||
|
} catch (error) {
|
||||||
|
newLine(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rm(params) {
|
||||||
|
if (params.length != 1) {
|
||||||
|
newLine("Usage: rm [path]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dir = tree.findNode(params[0]);
|
||||||
|
try {
|
||||||
|
tree.removeNode(dir);
|
||||||
|
newLine();
|
||||||
|
} catch (error) {
|
||||||
|
newLine(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mv(params) {
|
||||||
|
if (params.length != 2) {
|
||||||
|
newLine("Usage: mv [from] [to]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var from = tree.findNode(params[0])
|
||||||
|
var to = tree.findNode(params[1])
|
||||||
|
try {
|
||||||
|
if (to == undefined) {
|
||||||
|
var path = splitPath(params[1])
|
||||||
|
var tail = path[0],
|
||||||
|
fromName = path[1]
|
||||||
|
to = tree.findNode(tail)
|
||||||
|
tree.moveNode(from, to, fromName);
|
||||||
|
} else tree.moveNode(from, to);
|
||||||
|
newLine()
|
||||||
|
} catch (error) {
|
||||||
|
newLine(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(params) {
|
||||||
|
if (params.length > 0) {
|
||||||
|
newLine("Usage: clear");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var clone = $("#main-inp").clone(true);
|
||||||
|
$(".cli-out").empty();
|
||||||
|
$(".cli-out").append(clone);
|
||||||
|
clearInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
function echo(params) {
|
||||||
|
if (params.length != 1 && params.length != 3) {
|
||||||
|
newLine("Usage: echo [str] [>|>>] [path]")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (params.length == 1) {
|
||||||
|
newLine(params[0])
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (params[1] != '>' && params[1] != '>>') {
|
||||||
|
newLine("Usage: echo [str] [>|>>] [path]")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var path = splitPath(params[2])
|
||||||
|
var name = path[1],
|
||||||
|
tail = path[0]
|
||||||
|
var dir = tree.findNode(tail);
|
||||||
|
try {
|
||||||
|
if (params[1] == ">>") tree.createFile(dir, name, params[0], true)
|
||||||
|
else tree.createFile(dir, name, params[0])
|
||||||
|
newLine();
|
||||||
|
} catch (error) {
|
||||||
|
newLine(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pwd(params) {
|
||||||
|
if (params.length > 0) {
|
||||||
|
newLine("Usage: pwd");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newLine(tree.currentPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkdir(params) {
|
||||||
|
if (params.length != 1) {
|
||||||
|
newLine("Usage: mkdir [path]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var path = splitPath(params[0])
|
||||||
|
var name = path[1],
|
||||||
|
tail = path[0]
|
||||||
|
var dir = tree.findNode(tail);
|
||||||
|
try {
|
||||||
|
tree.createDir(dir, name)
|
||||||
|
newLine();
|
||||||
|
} catch (error) {
|
||||||
|
newLine(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cat(params) {
|
||||||
|
if (params.length != 1) {
|
||||||
|
newLine("Usage: cat [path]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var file = tree.findNode(params[0]);
|
||||||
|
try {
|
||||||
|
var content = tree.getContent(file);
|
||||||
|
newLine(content);
|
||||||
|
} catch (error) {
|
||||||
|
newLine(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cd(params) {
|
||||||
|
if (params.length > 1) {
|
||||||
|
newLine("Usage: cd [path]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dir = tree.root();
|
||||||
|
if (params.length == 1) dir = tree.findNode(params[0]);
|
||||||
|
try {
|
||||||
|
tree.moveTo(dir);
|
||||||
|
newLine();
|
||||||
|
} catch (error) {
|
||||||
|
newLine(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** LOCAL STORAGE METHODS */
|
||||||
|
|
||||||
|
function getFilesFromStorage() {
|
||||||
|
var item = localStorage.getItem("filetree");
|
||||||
|
if (item != undefined)
|
||||||
|
return deserializeTree(JSON.parse(item))
|
||||||
|
return filesTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFiles() {
|
||||||
|
var item = tree.serialize();
|
||||||
|
localStorage.setItem("filetree", JSON.stringify(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHistory() {
|
||||||
|
var item = localStorage.getItem("history");
|
||||||
|
if (item != undefined) return JSON.parse(item);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHistory() {
|
||||||
|
localStorage.setItem("history", JSON.stringify(history));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DOM INTERACTION METHODS */
|
||||||
|
|
||||||
|
function newLine(result) {
|
||||||
|
var clone = cloneMainInp();
|
||||||
|
$("#main-inp").before(clone);
|
||||||
|
if (result != undefined)
|
||||||
|
cliLog(result)
|
||||||
|
$("#main-inp span").text(tree.currentPath());
|
||||||
|
clearInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cliLog(result) {
|
||||||
|
$("#main-inp").before($('<p class="cmd-result">' + result + '</p>'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneMainInp() {
|
||||||
|
var clone = $('<div class="cli-inp"></div>');
|
||||||
|
var route = $('<p class="route">' + '<span>' + $("#main-inp span").text() + '</span> > ' + $("#main-inp input").val() + ' </p>');
|
||||||
|
clone.append(route);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInput() {
|
||||||
|
$("#main-inp input").focus();
|
||||||
|
$("#main-inp input").val("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInputValue(value) {
|
||||||
|
$("#main-inp input").val(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputValue() {
|
||||||
|
return $("#main-inp input").val().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTIL METHODS */
|
||||||
|
|
||||||
|
function getMeanings(word) {
|
||||||
|
return axios({
|
||||||
|
"url": "https://mashape-community-urban-dictionary.p.rapidapi.com/define?term=" + word,
|
||||||
|
"method": "get",
|
||||||
|
"timeout": 0,
|
||||||
|
"headers": {
|
||||||
|
"x-rapidapi-key": "a3a58aad81mshba110cbc0274d35p1d8b24jsn848d26bf6933"
|
||||||
|
},
|
||||||
|
}).then(function (response) {
|
||||||
|
return response.data.list;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitPath(path) {
|
||||||
|
var tailRegex = /^\S*\/[\.]*|^\.+$/gm;
|
||||||
|
var baseRegex = /[\w\-\_]+$|([\w\-\_]*\.+\w+)$|([\w\-\_]+\.+\w*)$/gm;
|
||||||
|
var tail = tailRegex.exec(path)
|
||||||
|
var base = baseRegex.exec(path)
|
||||||
|
tail = tail == null ? "." : tail[0]
|
||||||
|
base = base == null ? "" : base[0]
|
||||||
|
return [tail, base]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextAvailableChild(parent, name) {
|
||||||
|
if (tabs.length > 0) {
|
||||||
|
var child = tabs[tabsIndex];
|
||||||
|
tabsIndex = (tabsIndex + 1) % tabs.length;
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
tabs = tree.listNode(parent);
|
||||||
|
for (var i = 0; i < tabs.length; i++) {
|
||||||
|
if (tabs[i].name.startsWith(name)) {
|
||||||
|
var child = tabs[i];
|
||||||
|
tabsIndex = (i + 1) % tabs.length;
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var child = tabs[tabsIndex];
|
||||||
|
tabsIndex = (tabsIndex + 1) % tabs.length;
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
function autocompleteTail(tail, newName) {
|
||||||
|
return tail.endsWith("/") ? tail + newName :
|
||||||
|
tail.startsWith(".") ? newName :
|
||||||
|
tail + "/" + newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
258
src/scripts/filesTree.js
Normal file
258
src/scripts/filesTree.js
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
function filesTree(root) {
|
||||||
|
/** PRIVATE ATTRIBUTES */
|
||||||
|
if (root == undefined) root = {
|
||||||
|
parent: null,
|
||||||
|
childs: {},
|
||||||
|
name: "/",
|
||||||
|
time: Date.now(),
|
||||||
|
type: "d",
|
||||||
|
}
|
||||||
|
var currentDir = root;
|
||||||
|
|
||||||
|
/** PUBLIC METHODS */
|
||||||
|
return {
|
||||||
|
currentPath() {
|
||||||
|
return getPath(currentDir);
|
||||||
|
},
|
||||||
|
|
||||||
|
currentNode() {
|
||||||
|
return currentDir;
|
||||||
|
},
|
||||||
|
|
||||||
|
root() {
|
||||||
|
return root;
|
||||||
|
},
|
||||||
|
|
||||||
|
findNode(path) {
|
||||||
|
var node = currentDir;
|
||||||
|
if (path == "/") return root;
|
||||||
|
if (path == ".") return currentDir;
|
||||||
|
if (path.startsWith("/")) node = root;
|
||||||
|
tokens = path.replace(/\//g, " ").trim().split(" ")
|
||||||
|
|
||||||
|
for (var i = 0; i < tokens.length; i++) {
|
||||||
|
if (tokens[i] == '.') continue;
|
||||||
|
if (tokens[i] == ".." && node.parent != null)
|
||||||
|
node = node.parent;
|
||||||
|
else if (tokens[i] == ".." && node.parent == null) continue;
|
||||||
|
else {
|
||||||
|
if (tokens[i] in node.childs) {
|
||||||
|
if (isdir(node.childs[tokens[i]]))
|
||||||
|
node = node.childs[tokens[i]];
|
||||||
|
else return node.childs[tokens[i]]
|
||||||
|
} else return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
},
|
||||||
|
|
||||||
|
listNode(node, flag) {
|
||||||
|
var validFlags = ["-S", "-R", "-t"];
|
||||||
|
var result = []
|
||||||
|
if (node == undefined) throw Error("Path not found");
|
||||||
|
if (isfile(node)) throw Error("Can't list a file");
|
||||||
|
if (flag != undefined && !validFlags.includes(flag)) throw Error("Invalid option");
|
||||||
|
|
||||||
|
if (flag == "-R") result = listRecursive(node)
|
||||||
|
else {
|
||||||
|
for (var key of Object.keys(node.childs)) result.push(node.childs[key])
|
||||||
|
if (flag == "-S") result.sort(compareSize);
|
||||||
|
else if (flag == "-t") result.sort(compareTime);
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNode(node) {
|
||||||
|
if (node == undefined) throw Error("Path not found");
|
||||||
|
if (isChild(currentDir, node)) throw Error("Can't remove a parent directory");
|
||||||
|
if (node == currentDir) throw Error("Can't remove the current directory");
|
||||||
|
delete node.parent.childs[node.name];
|
||||||
|
},
|
||||||
|
|
||||||
|
moveNode(from, to, newName) {
|
||||||
|
if (from == undefined || to == undefined) throw Error("Path not found");
|
||||||
|
if (from == root) throw Error("Can't move the root directory");
|
||||||
|
if (from == currentDir) throw Error("Can't move the current directory");
|
||||||
|
if (from == to) throw Error("File already exists");
|
||||||
|
if (isdir(from) && isChild(currentDir, from)) throw Error("Can't move a parent directory");
|
||||||
|
if (isChild(to, from)) throw Error("Can't move a file to a child");
|
||||||
|
if (isdir(from) && isfile(to)) throw Error("cannot overwrite non-directory with directory");
|
||||||
|
|
||||||
|
this.removeNode(from);
|
||||||
|
from.time = Date.now()
|
||||||
|
if (isdir(to)) {
|
||||||
|
from.parent = to;
|
||||||
|
if (newName) from.name = newName;
|
||||||
|
} else {
|
||||||
|
from.name = to.name;
|
||||||
|
from.parent = to.parent;
|
||||||
|
}
|
||||||
|
from.parent.childs[from.name] = from;
|
||||||
|
},
|
||||||
|
|
||||||
|
createDir(parent, name) {
|
||||||
|
if (parent == undefined) throw Error("Path not found");
|
||||||
|
if (isfile(parent)) throw Error(getPath(parent) + " is a file")
|
||||||
|
if (name in parent.childs) throw Error("Folder already exists");
|
||||||
|
if (name == "") throw Error(getPath(parent) + " already exists");
|
||||||
|
|
||||||
|
parent.childs[name] = {
|
||||||
|
name: name,
|
||||||
|
parent: parent,
|
||||||
|
childs: {},
|
||||||
|
time: Date.now(),
|
||||||
|
type: "d",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createFile(parent, name, content, append) {
|
||||||
|
if (parent == undefined) throw Error("Path not found");
|
||||||
|
if (isfile(parent)) throw Error(getPath(parent) + " is a file")
|
||||||
|
if (name == "") throw Error("Name undefined");
|
||||||
|
if (name in parent.childs && isdir(parent.childs[name])) throw Error(name + " is a directory");
|
||||||
|
|
||||||
|
parent.childs[name] = {
|
||||||
|
name: name,
|
||||||
|
parent: parent,
|
||||||
|
content: append && name in parent.childs ? parent.childs[name].content + '\n' + content : content,
|
||||||
|
time: Date.now(),
|
||||||
|
type: "f",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
evaluateFile(file) {
|
||||||
|
if(file == undefined) throw Error("Path not found");
|
||||||
|
if(!isfile(file)) throw Error("Can't evaluate a directory");
|
||||||
|
return eval(file.content);
|
||||||
|
},
|
||||||
|
|
||||||
|
moveTo(node) {
|
||||||
|
if(node == undefined) throw Error("Path not found")
|
||||||
|
if (node != undefined && isfile(node)) throw Error("Cannot move to a file");
|
||||||
|
currentDir = node ? node : root;
|
||||||
|
},
|
||||||
|
|
||||||
|
getContent(file) {
|
||||||
|
if (file == undefined) throw Error("Path not found");
|
||||||
|
if (isdir(file)) throw Error("Cannot read a directory");
|
||||||
|
return file.content;
|
||||||
|
},
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return serializeNode(root);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PRIVATE METHODS */
|
||||||
|
function compareSize(a, b) {
|
||||||
|
var size1 = getNodeSize(a)
|
||||||
|
var size2 = getNodeSize(b)
|
||||||
|
if (size1 < size2) return 1
|
||||||
|
if (size1 > size2) return -1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeSize(node) {
|
||||||
|
if (isfile(node)) return 2 * node.content.length
|
||||||
|
var size = 0;
|
||||||
|
for (var key of Object.keys(node.childs)) size += getNodeSize(node.childs[key])
|
||||||
|
return 4096 + size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareTime(a, b) {
|
||||||
|
if (a.time < b.time) return 1
|
||||||
|
if (a.time > b.time) return -1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function listRecursive(node, level, result) {
|
||||||
|
if (level == undefined) level = 0;
|
||||||
|
if (result == undefined) result = [];
|
||||||
|
var tabs = '   '.repeat(level);
|
||||||
|
result.push([node.name, tabs]);
|
||||||
|
if (isdir(node)) {
|
||||||
|
for (var key of Object.keys(node.childs))
|
||||||
|
listRecursive(node.childs[key], level + 1, result)
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isdir(node) {
|
||||||
|
return node.type == "d";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isfile(node) {
|
||||||
|
return node.type == "f";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChild(child, parent) {
|
||||||
|
if (child == null || parent == null) return false
|
||||||
|
if (child.parent == parent) return true;
|
||||||
|
return isChild(child.parent, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPath(node, path) {
|
||||||
|
if (path == undefined) path = "";
|
||||||
|
if (node == root) return "/" + path;
|
||||||
|
var next = path == "" ? node.name : node.name + "/" + path;
|
||||||
|
return getPath(node.parent, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeNode(node, result, parentId) {
|
||||||
|
if (result == undefined) result = {}
|
||||||
|
var id = generateUniqueId(result);
|
||||||
|
|
||||||
|
if (parentId != undefined) result[parentId].childs.push(id);
|
||||||
|
result[id] = Object.assign({}, node);
|
||||||
|
result[id].parent = parentId;
|
||||||
|
|
||||||
|
if (isdir(node)) {
|
||||||
|
var childs = node.childs;
|
||||||
|
result[id].childs = []
|
||||||
|
for (var key of Object.keys(childs))
|
||||||
|
serializeNode(childs[key], result, id)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUniqueId(obj) {
|
||||||
|
var id = s4() + '-' + s4();
|
||||||
|
while (id in obj) {
|
||||||
|
id = s4() + '-' + s4();
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function s4() {
|
||||||
|
return Math.floor((1 + Math.random()) * 0x10000)
|
||||||
|
.toString(16)
|
||||||
|
.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** FILES TREE CONSTRUCTOR FROM SERIALIZED OBJECT */
|
||||||
|
|
||||||
|
function deserializeTree(obj) {
|
||||||
|
var rootNode = {}
|
||||||
|
for (var key of Object.keys(obj)) {
|
||||||
|
if (obj[key].name == "/") {
|
||||||
|
rootNode = obj[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deserializeNode(rootNode, obj);
|
||||||
|
return filesTree(rootNode);
|
||||||
|
|
||||||
|
function deserializeNode(node, storage) {
|
||||||
|
if (node.parent != null) node.parent = storage[node.parent]
|
||||||
|
if (node.type == "d") {
|
||||||
|
var childs = node.childs;
|
||||||
|
node.childs = {}
|
||||||
|
for (var id of childs) {
|
||||||
|
var child = storage[id];
|
||||||
|
node.childs[child.name] = child;
|
||||||
|
deserializeNode(child, storage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/scripts/script.js
Normal file
53
src/scripts/script.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
$(function () {
|
||||||
|
var cli = createCli();
|
||||||
|
var ctrl_down = false;
|
||||||
|
|
||||||
|
/** EVENT LISTENERS */
|
||||||
|
$(".cli-out").click(function () {
|
||||||
|
$("#main-inp input").focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".cli-out").keydown(function (e) {
|
||||||
|
// on ENTER
|
||||||
|
if (e.which == 13) {
|
||||||
|
var value = cli.getInputValue();
|
||||||
|
cli.addToHistory(value);
|
||||||
|
cmd(value);
|
||||||
|
}
|
||||||
|
// on CTRL
|
||||||
|
if (e.which == 17) ctrl_down = true;
|
||||||
|
// on L
|
||||||
|
if (e.which == 76 && ctrl_down) {
|
||||||
|
e.preventDefault();
|
||||||
|
cli.run("clear");
|
||||||
|
ctrl_down = false;
|
||||||
|
}
|
||||||
|
// on ARROW UP
|
||||||
|
if (e.which == 38) cli.historyUp();
|
||||||
|
// on ARROW DOWN
|
||||||
|
if (e.which == 40) cli.historyDown();
|
||||||
|
// on TAB
|
||||||
|
if(e.which == 9) {
|
||||||
|
e.preventDefault();
|
||||||
|
cli.autocomplete();
|
||||||
|
} else cli.resetTab();
|
||||||
|
});
|
||||||
|
$(".cli-out").keyup(function (e) {
|
||||||
|
// on CTRL UP
|
||||||
|
if (e.which == 17) ctrl_down = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** FUNCTIONS */
|
||||||
|
|
||||||
|
function cmd(value) {
|
||||||
|
var seq = value.split(' ');
|
||||||
|
var command = seq[0];
|
||||||
|
var params = seq.splice(1);
|
||||||
|
if (cli.has(command))
|
||||||
|
cli.run(command, params);
|
||||||
|
else if (command == "")
|
||||||
|
cli.newLine();
|
||||||
|
else
|
||||||
|
cli.newLine("Command not found: " + command);
|
||||||
|
}
|
||||||
|
});
|
||||||
120
src/style.css
Normal file
120
src/style.css
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-color: #171622;
|
||||||
|
--text-color: white;
|
||||||
|
--cmd-color: #1D1C29;
|
||||||
|
--cmd-hd-color: #343148;
|
||||||
|
--gray-color: #5D5B6D;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, p {
|
||||||
|
margin: 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 5rem;
|
||||||
|
padding: 2rem 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: min(90%, 800px);
|
||||||
|
margin: 0rem auto 5rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: gray;
|
||||||
|
background-color: var(--cmd-color);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-hd {
|
||||||
|
height: 30px;
|
||||||
|
background-color: var(--cmd-hd-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
background-color: var(--gray-color);
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-out {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-anchor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-out:hover {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-out > * {
|
||||||
|
margin: .5rem 0rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-inp {
|
||||||
|
display: flex;
|
||||||
|
padding: 0rem 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-inp span {
|
||||||
|
color: greenyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-inp .route {
|
||||||
|
padding-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-inp input {
|
||||||
|
flex: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmd-result {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: .5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 450px) {
|
||||||
|
main h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
padding: 1rem 0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue