commit 04b4c365a1dc9a1c3fde05f623fe47ecf4f4c39d Author: JasterV Date: Fri Oct 2 14:36:04 2020 +0200 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d55085 --- /dev/null +++ b/README.md @@ -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. + +![Tree Diagram](assets/tree_diagram.png) + +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 :) diff --git a/assets/tree_diagram.png b/assets/tree_diagram.png new file mode 100644 index 0000000..90d06a5 Binary files /dev/null and b/assets/tree_diagram.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..e5dfc94 --- /dev/null +++ b/index.html @@ -0,0 +1,44 @@ + + + + + + + + JS CLI + + + + + + + + + + + + +
+

JS CLI

+ +
+
+
+
+
+
+ +
+
+

/ >

+ +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/src/scripts/cli.js b/src/scripts/cli.js new file mode 100644 index 0000000..fd96529 --- /dev/null +++ b/src/scripts/cli.js @@ -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)" + + "
" + "-R, list subdirectories recursively" + + "
" + "-S, sort by file size, largest first" + + "
" + "-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("DEFINITION") + cliLog(item.definition.replace(/[\[\]]/g, '')); + cliLog("EXAMPLE"); + cliLog(item.example.replace(/[\[\]]/g, '')); + cliLog("REFERENCE") + cliLog(item.permalink) + cliLog("- - - - - - - - - -") + }) + } + }).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("NAME") + cliLog(command.name); + cliLog("SYNOPSIS"); + cliLog(command.synopsis); + cliLog("DESCRIPTION"); + 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($('

' + result + '

')) + } + + function cloneMainInp() { + var clone = $('
'); + var route = $('

' + '' + $("#main-inp span").text() + ' > ' + $("#main-inp input").val() + '

'); + 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; + } + +} \ No newline at end of file diff --git a/src/scripts/filesTree.js b/src/scripts/filesTree.js new file mode 100644 index 0000000..773a696 --- /dev/null +++ b/src/scripts/filesTree.js @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/scripts/script.js b/src/scripts/script.js new file mode 100644 index 0000000..584f675 --- /dev/null +++ b/src/scripts/script.js @@ -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); + } +}); \ No newline at end of file diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..acd5429 --- /dev/null +++ b/src/style.css @@ -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; + } +} \ No newline at end of file