├── .gitignore ├── README.md ├── demo_screen.png ├── demo_screen_new.png ├── demo_screen_newer.png ├── index.html ├── npm ├── package-lock.json ├── package.json ├── src ├── App.vue ├── assets │ ├── logo.png │ └── styles.css ├── components │ ├── filemanager │ │ ├── filemanager.vue │ │ └── prompt.vue │ ├── logger.js │ ├── response.js │ ├── settings │ │ ├── settings.vue │ │ └── settingsIcon.vue │ ├── terminal │ │ ├── prompt-decoration.vue │ │ ├── prompt-input.vue │ │ ├── prompt.vue │ │ ├── suggestions.vue │ │ ├── term-out.vue │ │ ├── terminal.vue │ │ └── working-directory.vue │ ├── utility.js │ └── widgets │ │ ├── filetree.vue │ │ ├── todo.vue │ │ ├── weather.vue │ │ └── window.vue ├── main.js ├── store │ └── index.js └── util │ ├── config │ └── index.js │ └── filesystem │ ├── directory.js │ ├── file.js │ └── filesystem.js ├── test.html └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules/* 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # BASHRC 3 | 4 | ![demo](https://github.com/cbrasser/bashrc/blob/master/demo_screen_newer.png) 5 | 6 | # Live version 7 | 8 | I'll be pushing a new live version very soon! 9 | 10 | ## Open issues 11 | - shortcuts for 'quitting' and opening applications 12 | 13 | 14 | ## Functionality 15 | 16 | The basic principle of this startpage is to act as a bookmark repository. 17 | ...But in a very cool way. 18 | 19 | This is a start page heavily inspired by my linux desktop setup, where I mainly operate in the terminal or terminal-based applications. Naturally, I'd like my browser startpage to be keyboard oriented as well. But it should also look nice. 20 | 21 | Directories in the file system resemble bookmark categories and Files are named links to your webpages. 22 | 23 | The Idea is to act like an os with a desktop environment, a file manager and open programs that are then arranged on the page trough a Window Manager (currently tiling or floating). 24 | 25 | ### Tiled 26 | 27 | windows are arranged automatically to fill all available space (with sexy gaps, ofc) 28 | 29 | ### Floating 30 | 31 | In the floating layout, you can arrange the windows by dragging and resizing them on their side borders. 32 | 33 | window locations and dimensions are stored in the config and should be persistent upon refreshing the page. This way you can arrange your windows in a way that you like. 34 | 35 | Note: The window locations are absolute. Meaning that if you place a window on, e.g., the bottom-right corner on a big screen and then resize your screen the window might be out of the visible area. There are some checks to bring them back upon refreshing the page, but i did not test this too much :) 36 | 37 | 38 | ## Available programs 39 | 40 | ### shell 41 | 42 | available commands: 43 | - cd [path]: change directory 44 | - ls [path]: list content of directory 45 | - touch [path, url]: create file linking to [url] 46 | - rm [path]: remove file 47 | - mkdir [path]: create new directory 48 | - rmdir [path]: remove directory 49 | - fetch: cool system information 50 | - echo [args]: print [args] to stdout 51 | - pwd: print current working directory 52 | - open [path]: open url of file at [path] in new tab 53 | - clear: clear stdout 54 | 55 | There is autocompletion for both commands and paths. you can invoke it by starting to type something and then hitting 'tab'. You can cycle through suggestions with tab and accept one with 'enter'. If there is only one suggestion, 'tab' will also work for accepting. 56 | 57 | 58 | ### filemanager 59 | 60 | Modeled after the linux file manager 'fff' (fucking fast filemanager). 61 | It is completely keyboard based. 62 | 63 | #### controls 64 | 65 | You can move up and down the content of the current directory with the arrow keys. 66 | - Right arrow key either enters the selected item if its a directory or opens the url of the file if its a file. 67 | - Left arrow key goes into the parent directory of the current dir 68 | - input 'f' to add a new file 69 | - input 'n' to add a new dir 70 | - input '/' to search in the current dir 71 | - input ':' to open a file/directory by typing its name 72 | - input 'd' to go into 'deletion' mode: All (with 'd') selected elements will be deleted uppon pressing 'p'. Directories can only be deleted if they are empty. 73 | - input 'esc' to leave the input prompt or deletion mode, if one of them is open or to cancel the filter applied by searching. 74 | 75 | The status bar at the bottom displays the path of the current working directory as well as the position of the selected element and the currently active filter. 76 | 77 | ### weather 78 | 79 | Just a tiny applet that displays local weather information. you can set your 80 | city in the settings widget. 81 | 82 | ### todo 83 | 84 | a tiny todo tracker. todos store a name and a description of a task, as well as 85 | 0 to several tags for marking tasks that belong to the same category (work, private, 86 | university, some project, whatever). You can color-code the tags by clicking on them and selecting a color in the color strip. 87 | 88 | 89 | ### Settings 90 | 91 | The wheel on the bottom right of the page opens the settings. you can select which applications you would like to have open upon startup as well as your preferred state of the window manager. You can also set your city for the weather applet and configure the colors, wallpaper, and window decorations to your likings! 92 | 93 | The background image option needs to be a valid url of an image in oder to work. 94 | The colors currently only support hex values 95 | The opacity needs to be a value from 0 to 1. 96 | 97 | ### Active TODO list 98 | 99 | - implement filetree widget 100 | 101 | ### General remarks 102 | 103 | Links and dirs are clickable in all programs as well, but thats not the point of this website right?. 104 | 105 | All bookmarks are currently stored in local storage, so you might now want to clear your cache if you stored a lot of bookmarks on the page. I am planning on porting the page into a chrome extension to get better storage options. 106 | 107 | It should work with all modern browsers, however I'm mainly using firefox and can't guarantee that everything looks nicely on other browsers. 108 | 109 | -------------------------------------------------------------------------------- /demo_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrasser/bashrc/5104f166dda62dad7bf5b1f521af3d4aebf9279f/demo_screen.png -------------------------------------------------------------------------------- /demo_screen_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrasser/bashrc/5104f166dda62dad7bf5b1f521af3d4aebf9279f/demo_screen_new.png -------------------------------------------------------------------------------- /demo_screen_newer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrasser/bashrc/5104f166dda62dad7bf5b1f521af3d4aebf9279f/demo_screen_newer.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | bashrc 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /npm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrasser/bashrc/5104f166dda62dad7bf5b1f521af3d4aebf9279f/npm -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "description": "A Vue.js project", 4 | "version": "1.0.0", 5 | "author": "cbrasser ", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 10 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 11 | }, 12 | "dependencies": { 13 | "bootstrap4-toggle": "^3.6.1", 14 | "easy-autocomplete": "^1.3.5", 15 | "eslint": "^6.7.2", 16 | "fs": "0.0.1-security", 17 | "jquery": "^3.4.1", 18 | "tabcomplete": "^1.5.3", 19 | "vue": "^2.6.11", 20 | "vue-bootstrap-typeahead": "^0.2.6", 21 | "vue-drag-resize": "^1.3.2", 22 | "vue-resizable": "^1.2.5", 23 | "vue2-autocomplete-js": "^0.2.2", 24 | "vuejs-toggle-switch": "^1.3.1", 25 | "vuex": "^3.6.0" 26 | }, 27 | "browserslist": [ 28 | "> 1%", 29 | "last 2 versions", 30 | "not ie <= 8" 31 | ], 32 | "devDependencies": { 33 | "babel-core": "^6.26.0", 34 | "babel-loader": "^7.1.2", 35 | "babel-preset-env": "^1.6.0", 36 | "babel-preset-stage-3": "^6.24.1", 37 | "cross-env": "^5.0.5", 38 | "css-loader": "^0.28.7", 39 | "expose-loader": "^0.7.5", 40 | "file-loader": "^1.1.4", 41 | "node-sass": "^4.5.3", 42 | "sass-loader": "^6.0.6", 43 | "vue-loader": "^13.0.5", 44 | "vue-template-compiler": "^2.6.11", 45 | "webpack": "^3.6.0", 46 | "webpack-dev-server": "^2.9.1" 47 | }, 48 | "browser": { 49 | "tabcomplete": false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 176 | 177 | 226 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrasser/bashrc/5104f166dda62dad7bf5b1f521af3d4aebf9279f/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrasser/bashrc/5104f166dda62dad7bf5b1f521af3d4aebf9279f/src/assets/styles.css -------------------------------------------------------------------------------- /src/components/filemanager/filemanager.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 228 | 229 | 298 | -------------------------------------------------------------------------------- /src/components/filemanager/prompt.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 49 | 50 | -------------------------------------------------------------------------------- /src/components/logger.js: -------------------------------------------------------------------------------- 1 | export function log(name, value, color) { 2 | var col = color ? color : 'black'; 3 | console.log(`%c${name}: ${value}`,`color: ${col}`) 4 | } 5 | -------------------------------------------------------------------------------- /src/components/response.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function newResponse() { 4 | return new Response(); 5 | } 6 | 7 | class Response { 8 | constructor() { 9 | this.directory = null; 10 | this.dirs = []; 11 | this.files = []; 12 | this.messages = []; 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /src/components/settings/settings.vue: -------------------------------------------------------------------------------- 1 | 159 | 160 | 232 | 233 | 367 | -------------------------------------------------------------------------------- /src/components/settings/settingsIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 53 | -------------------------------------------------------------------------------- /src/components/terminal/prompt-decoration.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/terminal/prompt-input.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 116 | 117 | 136 | -------------------------------------------------------------------------------- /src/components/terminal/prompt.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 53 | 54 | 63 | -------------------------------------------------------------------------------- /src/components/terminal/suggestions.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 45 | 46 | 74 | -------------------------------------------------------------------------------- /src/components/terminal/term-out.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/terminal/terminal.vue: -------------------------------------------------------------------------------- 1 | 11 | 226 | 227 | 232 | -------------------------------------------------------------------------------- /src/components/terminal/working-directory.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/utility.js: -------------------------------------------------------------------------------- 1 | export function get_browser_info(){ 2 | var ua=navigator.userAgent,tem,M=ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; 3 | if(/trident/i.test(M[1])){ 4 | tem=/\brv[ :]+(\d+)/g.exec(ua) || []; 5 | return {name:'IE ',version:(tem[1]||'')}; 6 | } 7 | if(M[1]==='Chrome'){ 8 | tem=ua.match(/\bOPR\/(\d+)/) 9 | if(tem!=null) {return {name:'Opera', version:tem[1]};} 10 | } 11 | M=M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?']; 12 | if((tem=ua.match(/version\/(\d+)/i))!=null) {M.splice(1,1,tem[1]);} 13 | return { 14 | name: M[0], 15 | version: M[1] 16 | }; 17 | } -------------------------------------------------------------------------------- /src/components/widgets/filetree.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/widgets/todo.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 151 | 152 | 247 | -------------------------------------------------------------------------------- /src/components/widgets/weather.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 91 | 92 | 109 | -------------------------------------------------------------------------------- /src/components/widgets/window.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 38 | 39 | 70 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store'; 4 | 5 | var data = {}; 6 | 7 | 8 | new Vue({ 9 | el: '#app', 10 | store, 11 | data : data, 12 | render: h => h(App) 13 | }) 14 | 15 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex' 2 | import Vue from 'vue'; 3 | import { FileSystem } from '../util/filesystem/filesystem'; 4 | 5 | Vue.use(Vuex); 6 | 7 | export const getDefaultConfig = () => { 8 | return { 9 | apps: [ 10 | { 11 | name: 'filemanager', 12 | position: { top: 20, left: 50 }, 13 | dimensions: { height: 200, width: 300 }, 14 | visible: true, 15 | }, 16 | { 17 | name: 'terminal', 18 | position: { top: 500, left: 100 }, 19 | dimensions: { height: 200, width: 300 }, 20 | visible: true, 21 | }, 22 | { 23 | name: 'todo', 24 | position: { top: 420, left: 900 }, 25 | dimensions: { height: 220, width: 500 }, 26 | visible: false, 27 | }, 28 | { 29 | name: 'weather', 30 | position: { top: 20, left: 900 }, 31 | dimensions: { height: 250, width: 300 }, 32 | visible: false, 33 | } 34 | ], 35 | city: "Zurich", 36 | windowState: "floating", 37 | windowBorders: false, 38 | backgroundImage: "", 39 | colors: { 40 | fg: '#d8dee9', 41 | bg: '#1a1e21', 42 | accent_1: '#8fbcbb', 43 | accent_2: '#bf616a', 44 | accent_3: '#ebcb8b', 45 | }, 46 | opacity: 1, 47 | numCols: 1, 48 | }; 49 | } 50 | 51 | const store = new Vuex.Store({ 52 | state: { 53 | fileTree: undefined, 54 | workingDirectory: undefined, 55 | config: getDefaultConfig, 56 | }, 57 | mutations: { 58 | CONFIGURATION(state, payload) { 59 | state.config = payload; 60 | localStorage.setItem("config", JSON.stringify(state.config)); 61 | }, 62 | WINDOW_STATE(state, payload) { 63 | state.config.windowState = payload; 64 | localStorage.setItem("config", JSON.stringify(state.config)); 65 | 66 | }, 67 | CITY(state, payload) { 68 | state.config.city = payload; 69 | localStorage.setItem("config", JSON.stringify(state.config)); 70 | }, 71 | FILE_TREE(state, payload) { 72 | state.fileTree = payload; 73 | localStorage.setItem("root", JSON.stringify(payload.getRoot().toJSON())); 74 | }, 75 | WORKING_DIRECTORY(state, payload) { 76 | state.workingDirectory = payload; 77 | } 78 | }, 79 | actions: { 80 | loadConfig({ commit }) { 81 | console.log('loading config') 82 | let config; 83 | try { 84 | config = JSON.parse(window.localStorage.getItem("config")); 85 | console.log('loaded config') 86 | } catch (e) { 87 | console.log('invalid config, loading default'); 88 | } 89 | 90 | if (!config) { 91 | config = getDefaultConfig(); 92 | }; 93 | commit('CONFIGURATION', config); 94 | }, 95 | loadFileTree({ commit }) { 96 | console.log('loading file system') 97 | let json_obj = JSON.parse(window.localStorage.getItem("root")); 98 | let tree = new FileSystem(json_obj); 99 | commit('FILE_TREE', tree); 100 | commit('WORKING_DIRECTORY', tree.getRoot()); 101 | }, 102 | updateConfig({ commit }, config) { 103 | commit('CONFIGURATION', config); 104 | }, 105 | updateFileTree({ commit }, fileTree) { 106 | commit('FILE_TREE', fileTree); 107 | }, 108 | updateWorkingDirectory({ commit }, wd) { 109 | commit('WORKING_DIRECTORY', wd); 110 | }, 111 | setCity({ commit }, city) { 112 | commit('CITY', city); 113 | }, 114 | setState({ commit }, windowState) { 115 | commit('WINDOW_STATE', windowState); 116 | }, 117 | }, 118 | getters: {}, 119 | }); 120 | 121 | export default store; -------------------------------------------------------------------------------- /src/util/config/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrasser/bashrc/5104f166dda62dad7bf5b1f521af3d4aebf9279f/src/util/config/index.js -------------------------------------------------------------------------------- /src/util/filesystem/directory.js: -------------------------------------------------------------------------------- 1 | import {fileFromJSON} from './file' 2 | 3 | export function newDirectory(name, parent, isRoot) { 4 | return new Directory(name, parent); 5 | } 6 | 7 | export function dirFromJSON(parent, json_obj){ 8 | var d = new Directory(json_obj.name, parent, json_obj.isRoot); 9 | d.children = json_obj.children.map(c => dirFromJSON(d, c)); 10 | d.files = json_obj.files.map(f => fileFromJSON(d, f)); 11 | return d; 12 | } 13 | 14 | 15 | class Directory { 16 | constructor(name, parent, isRoot) { 17 | this.name = name; 18 | this.isRoot = isRoot; 19 | if (parent == null) { 20 | this.isRoot = true; 21 | this.parent = null; 22 | } else { 23 | this.isRoot = false; 24 | this.parent = parent; 25 | } 26 | this.parent = parent; 27 | this.children = []; 28 | this.files = []; 29 | } 30 | 31 | toJSON() { 32 | var json = {}; 33 | json.name = this.name; 34 | json.isRoot = this.isRoot; 35 | json.children = this.children.map(c => c.toJSON()); 36 | json.files = this.files.map(f => f.toJSON()); 37 | return json; 38 | } 39 | 40 | 41 | addChild(child) { 42 | if(this.getFileNames().indexOf(child.getName()) != -1 || 43 | this.getChildrenNames().indexOf(child.getName()) != -1 44 | ) { 45 | return false; 46 | } 47 | this.children.push(child); 48 | return true; 49 | } 50 | 51 | addFile(file) { 52 | if(this.getFileNames().indexOf(file.getName()) != -1 || 53 | this.getChildrenNames().indexOf(file.getName()) != -1 54 | ) { 55 | return false; 56 | } 57 | this.files.push(file); 58 | return true; 59 | } 60 | 61 | getParent() { 62 | return this.parent; 63 | } 64 | 65 | getRelative(name){ 66 | if(name == '..') { 67 | return this.getParent(); 68 | } else { 69 | return this.getNode(name); 70 | } 71 | } 72 | 73 | isEmpty() { 74 | return this.children.length == 0 && this.files.length == 0; 75 | } 76 | 77 | getName() { 78 | return this.name; 79 | } 80 | getPath() { 81 | if (this.parent) { 82 | return this.parent.getPath().concat("/"+this.name); 83 | } else { 84 | return this.name; 85 | } 86 | } 87 | 88 | getChildren() { 89 | return this.children; 90 | } 91 | 92 | getChild(name) { 93 | return this.children.filter(c => c.getName() == name)[0]; 94 | } 95 | getFile(name) { 96 | return this.files.filter(f => f.getName() == name)[0]; 97 | } 98 | getNode(name) { 99 | return this.getChild(name) ? this.getChild(name) : this.getFile(name); 100 | } 101 | 102 | removeChild(name) { 103 | if(this.getChildrenNames().indexOf(name) == -1){ 104 | return false; 105 | } 106 | this.children = this.children.filter(c => c.getName() != name); 107 | return true; 108 | } 109 | removeFile(name) { 110 | if(this.getFileNames().indexOf(name) == -1){ 111 | return false; 112 | } 113 | this.files = this.files.filter(f => f.getName() != name); 114 | return true; 115 | } 116 | 117 | getChildrenNames() { 118 | return this.children.map( c => c.getName()); 119 | } 120 | 121 | isEmpty() { 122 | return this.children.length == 0; 123 | } 124 | 125 | getFileNames() { 126 | return this.files.map(f => f.getName()); 127 | } 128 | 129 | getFiles() { 130 | return this.files; 131 | } 132 | 133 | 134 | 135 | 136 | } 137 | 138 | -------------------------------------------------------------------------------- /src/util/filesystem/file.js: -------------------------------------------------------------------------------- 1 | export function newFile(name, url, parent) { 2 | return new File(name, url, parent); 3 | } 4 | 5 | export function fileFromJSON(parent, json_obj){ 6 | var f = new File(json_obj.name, json_obj.url, parent); 7 | return f; 8 | } 9 | 10 | class File { 11 | constructor(name, url, parent) { 12 | this.name = name; 13 | this.url = url; 14 | this.parent = parent; 15 | } 16 | toJSON() { 17 | var json = {}; 18 | json.name = this.name; 19 | json.url = this.url; 20 | return json; 21 | } 22 | 23 | getUrl() { 24 | return this.url; 25 | } 26 | getPath() { 27 | return this.parent.getPath().concat(this.name); 28 | } 29 | getName() { 30 | return this.name; 31 | } 32 | getParent() { 33 | return this.parent; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/util/filesystem/filesystem.js: -------------------------------------------------------------------------------- 1 | import { newDirectory, dirFromJSON } from './directory.js' 2 | import { newFile, fileFromJSON } from './file.js' 3 | import { newResponse } from '../../components/response.js' 4 | import { log } from '../../components/logger' 5 | import { formatWithOptions } from 'util'; 6 | 7 | 8 | 9 | /* This will be a Tree with all folders and files but no information on current working dir, just the tree */ 10 | export class FileSystem { 11 | constructor(config) { 12 | this.separator = '/' 13 | this.dirNameExp = /[\w|\~]+$/ 14 | this.containsSlashCheckExp = /\// 15 | this.isAbsolutePathExp = /^\~/ 16 | 17 | this.root = newDirectory('~', null, true); 18 | if (config) { 19 | console.log('constructing filesystem from store') 20 | this.root.children = config.children.map(c => dirFromJSON(this.root, c)); 21 | this.root.files = config.files.map(f => fileFromJSON(this.root, f)); 22 | } else { 23 | console.log('no file system found, creating new'); 24 | } 25 | } 26 | 27 | getRoot() { 28 | return this.root; 29 | } 30 | 31 | buildPathList(path) { 32 | return path.split(this.separator); 33 | } 34 | 35 | buildPathString(path_list) { 36 | return path_list.join(this.separator); 37 | } 38 | 39 | removeTrailingNode(path) { 40 | return path.substr(path.indexOf(this.separator) + 1) 41 | } 42 | 43 | /* 44 | type: 45 | 0: all 46 | 1: dirs 47 | 2: files 48 | TODO: Fix how we get nodes and files 49 | */ 50 | getNode(dir, path) { 51 | log('getNode: ', `${dir.getName()} - ${path}`) 52 | 53 | // path is absolute, start search at root and remove root at start of path 54 | if (this.isAbsolutePathExp.test(path)) { 55 | return this.getNode(this.root, this.removeTrailingNode(path)) 56 | } else 57 | // path does not contain separator, so it only contains a name 58 | // -> path is direct name 59 | if (path.indexOf(this.separator) == -1) { 60 | if (path.length == 0) { 61 | return dir; 62 | } 63 | return dir.getRelative(path); 64 | } 65 | 66 | // neither direct name nor absolute 67 | // -> get first node in path, restart search in first node in path with cut-off path 68 | var next_node = dir.getRelative(path.split(this.separator)[0]); 69 | return this.getNode(next_node, this.removeTrailingNode(path)); 70 | } 71 | 72 | call(command, directory, args) { 73 | try { 74 | return this[command](directory, args); 75 | } catch (e) { 76 | console.log(e); 77 | 78 | } 79 | } 80 | 81 | 82 | 83 | // ------------------------- BASIC FILE SYSTEM COMMANDS ------------------------ 84 | 85 | 86 | ls(dir, args) { 87 | log('ls', `${dir} - ${args}`, 'green') 88 | // node is either specified by path or current node 89 | var node = args[0] ? this.getNode(dir, args[0]) : dir; 90 | 91 | var res = newResponse(); 92 | res.dirs = node.getChildrenNames(); 93 | res.files = node.getFiles().map(f => ({ "name": f.getName(), "url": f.getUrl() })); 94 | return res; 95 | } 96 | 97 | cd(dir, args) { 98 | log('cd', `${dir} - ${args}`, 'green') 99 | var res = newResponse() 100 | if (!Array.isArray(args)) { 101 | args = args.split(' ') 102 | } 103 | if (args[0]) { 104 | var d = this.getNode(dir, args[0]) 105 | if (!d) { 106 | res.messages.push({ "type": "error", "value": `cd: No such directory: ${args[0]}` }); 107 | return res; 108 | } 109 | log('found node', d.getName()) 110 | if (d.url) { 111 | res.messages.push({ "type": "error", "value": `cd: not a directory: ${d.getName()}` }); 112 | return res; 113 | } 114 | res.directory = d; 115 | return res; 116 | } 117 | // goal dir is either root or a relative dir from current dir 118 | res.directory = this.getRoot() 119 | return res; 120 | } 121 | 122 | mkdir(dir, args) { 123 | log('mkdir', `${dir} - ${args}`, 'green') 124 | var res = newResponse(); 125 | // check if required args are submitted 126 | if (!args[0]) { 127 | res.messages.push({ "type": "error", "value": "mkdir: missing operand" }); 128 | return res; 129 | } 130 | if (!Array.isArray(args)) { 131 | args = args.split(' ') 132 | } 133 | // we always need at least a name 134 | var name = args[0].substr(args[0].lastIndexOf(this.separator) + 1); 135 | log('mkdir: name', name, 'green') 136 | var path = args[0].substr(0, args[0].lastIndexOf(this.separator)); 137 | var node = this.getNode(dir, path); 138 | if (!node) { 139 | res.messages.push({ 140 | "type": "error", 141 | "value": `mkdir: cannnot create directory '${args}': No such file or directory` 142 | }); 143 | return res; 144 | } 145 | if (node.addChild(newDirectory(name, node))) { 146 | return { success: true }; 147 | } else { 148 | res.messages.push({ "type": "error", "value": `mkdir: cannot create directory '${args[0]}': File exists` }); 149 | return res; 150 | } 151 | } 152 | 153 | rmdir(dir, args) { 154 | log("rmdir", dir + " - " + args, 'green') 155 | var res = newResponse(); 156 | // check if required args are submitted 157 | if (!args[0]) { 158 | res.messages.push({ "type": "error", "value": 'rmdir: missing operand' }); 159 | return res; 160 | } 161 | if (!Array.isArray(args)) { 162 | args = args.split(' ') 163 | } 164 | var node = this.getNode(dir, args[0]); 165 | if (!node) { 166 | res.messages.push({ 167 | "type": "error", 168 | "value": `rmdir: failed to remove '${args}': No such file or directory` 169 | }); 170 | return res; 171 | } 172 | if (node.url) { 173 | res.messages.push({ "type": "error", "value": `rmdir: failed to remove '${node.getName()}': Not a directory` }); 174 | return res; 175 | } 176 | if (!node.isEmpty()) { 177 | res.messages.push({ "type": "error", "value": `failed to remove ${node.getName()}: Directory not empty` }) 178 | return res 179 | } 180 | if (!node.getParent().removeChild(node.getName())) { 181 | res.messages.push({ "type": "error", "value": `rmdir: failed to remove ${node.getName()}: No such file or directory` }) 182 | return res; 183 | } 184 | this.storeConfigToLocalStorage() 185 | } 186 | 187 | touch(dir, args) { 188 | log('touch', `${dir} - ${args}`, 'purple') 189 | var res = newResponse(); 190 | if (!Array.isArray(args)) { 191 | args = args.split(' ') 192 | } 193 | if (!args[0] || !args[1]) { 194 | res.messages.push({ "type": "error", "value": "touch: missing file operand" }) 195 | return res 196 | } 197 | var name = args[0].substr(args[0].lastIndexOf(this.separator) + 1); 198 | var path = args[0].substr(0, args[0].lastIndexOf(this.separator)); 199 | 200 | var node = this.getNode(dir, path); 201 | if (!node) { 202 | res.messages.push({ 203 | "type": "error", 204 | "value": `touch: cannot touch '${args}': No such file or directory` 205 | }); 206 | return res; 207 | } 208 | if (!node.addFile(newFile(name, args[1], node))) { 209 | res.messages.push({ "type": "error", "value": `touch: cannot create file '${name}': File exists` }); 210 | return res; 211 | } 212 | this.storeConfigToLocalStorage() 213 | } 214 | 215 | rm(dir, args) { 216 | log('rm', `${dir} - ${args}`, 'green') 217 | var res = newResponse(); 218 | if (!Array.isArray(args)) { 219 | args = args.split(' ') 220 | } 221 | if (!args[0]) { 222 | res.messages.push({ "type": "error", "value": "rm: missing operand" }); 223 | return res; 224 | } 225 | 226 | var file = this.getNode(dir, args[0]); 227 | if (!file) { 228 | res.messages.push({ 229 | "type": "error", 230 | "value": `rm: cannot remove '${args}': No such file or directory` 231 | }); 232 | return res; 233 | } 234 | if (!file.url) { 235 | res.messages.push({ "type": "error", "value": `rm: cannot remove '${file.getName()}: Is a directory'` }); 236 | return res; 237 | } 238 | if (!file.getParent().removeFile(file.getName())) { 239 | res.messages.push({ "type": "error", "value": `rm: cannot remove '${file.getName()}: No such file'` }); 240 | return res; 241 | } 242 | } 243 | 244 | api() { 245 | return ["cd", "touch", "rm", "mkdir", "rmdir", "ls"]; 246 | } 247 | 248 | } 249 | 250 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ message }} 4 |
5 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | 5 | module.exports = { 6 | entry: './src/main.js', 7 | output: { 8 | path: path.resolve(__dirname, './dist'), 9 | publicPath: '/dist/', 10 | filename: 'build.js' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.css$/, 16 | use: [ 17 | 'vue-style-loader', 18 | 'css-loader' 19 | ], 20 | }, 21 | { 22 | test: /\.scss$/, 23 | use: [ 24 | 'vue-style-loader', 25 | 'css-loader', 26 | 'sass-loader' 27 | ], 28 | }, 29 | { 30 | test: /\.sass$/, 31 | use: [ 32 | 'vue-style-loader', 33 | 'css-loader', 34 | 'sass-loader?indentedSyntax' 35 | ], 36 | }, 37 | { 38 | test: /\.vue$/, 39 | loader: 'vue-loader', 40 | options: { 41 | loaders: { 42 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map 43 | // the "scss" and "sass" values for the lang attribute to the right configs here. 44 | // other preprocessors should work out of the box, no loader config like this necessary. 45 | 'scss': [ 46 | 'vue-style-loader', 47 | 'css-loader', 48 | 'sass-loader' 49 | ], 50 | 'sass': [ 51 | 'vue-style-loader', 52 | 'css-loader', 53 | 'sass-loader?indentedSyntax' 54 | ] 55 | } 56 | // other vue-loader options go here 57 | } 58 | }, 59 | { 60 | test: /\.js$/, 61 | loader: 'babel-loader', 62 | exclude: /node_modules/ 63 | }, 64 | { 65 | test: /\.(png|jpg|gif|svg)$/, 66 | loader: 'file-loader', 67 | options: { 68 | name: '[name].[ext]?[hash]' 69 | } 70 | } 71 | ] 72 | }, 73 | resolve: { 74 | alias: { 75 | 'vue$': 'vue/dist/vue.esm.js' 76 | }, 77 | extensions: ['*', '.js', '.vue', '.json'] 78 | }, 79 | devServer: { 80 | historyApiFallback: true, 81 | noInfo: true, 82 | overlay: true 83 | }, 84 | performance: { 85 | hints: false 86 | }, 87 | devtool: '#eval-source-map' 88 | } 89 | 90 | if (process.env.NODE_ENV === 'production') { 91 | module.exports.devtool = '#source-map' 92 | // http://vue-loader.vuejs.org/en/workflow/production.html 93 | module.exports.plugins = (module.exports.plugins || []).concat([ 94 | new webpack.DefinePlugin({ 95 | 'process.env': { 96 | NODE_ENV: '"production"' 97 | } 98 | }), 99 | new webpack.optimize.UglifyJsPlugin({ 100 | sourceMap: true, 101 | compress: { 102 | warnings: false 103 | } 104 | }), 105 | new webpack.LoaderOptionsPlugin({ 106 | minimize: true 107 | }) 108 | ]) 109 | } 110 | --------------------------------------------------------------------------------