main.js (9734B)
1 /** 2 * The main FakeTerminal class 3 * @param el 4 * @param options 5 * @returns {window.FakeTerminal} 6 */ 7 window.FakeTerminal.main = function (el, options) { 8 9 /** 10 * Avoid scope issues by using `base` instead of `this` 11 * @type {Object} 12 */ 13 var base = this; 14 if (!jQuery) { 15 throw 'FakeTerminal: jQuery required'; 16 } else { 17 var $ = jQuery; 18 } 19 20 /** 21 * jQuery version of the element 22 * @type {jQuery} 23 */ 24 base.$el = $(el); 25 26 /** 27 * The original HTML of the element prior to initialisation (so we can restore the original terminal) 28 * @type {string} 29 */ 30 base.originalHtml = ''; 31 32 /** 33 * Any existing items within the element 34 * @type {Array} 35 */ 36 base.existingText = []; 37 38 /** 39 * References to the currently executing command's instance and deferred object 40 * @type {Object} 41 */ 42 base.executingCommand = { 43 'instance': null, 44 'deferred': null 45 }; 46 47 /** 48 * Map of keyCodes 49 * @type {Object} 50 */ 51 base.keymap = { 52 ENTER: 13, 53 UP: 38, 54 DOWN: 40, 55 C: 67, 56 D: 68, 57 U: 85 58 }; 59 60 /** 61 * The output service 62 * @type {window.FakeTerminal.output} 63 */ 64 base.output = null; 65 66 /** 67 * The input service 68 * @type {window.FakeTerminal.input} 69 */ 70 base.input = null; 71 72 /** 73 * The filesystem service 74 * @type {window.FakeTerminal.filesystem} 75 */ 76 base.filesystem = null; 77 78 /** 79 * The history service 80 * @type {window.FakeTerminal.history} 81 */ 82 base.history = null; 83 84 // -------------------------------------------------------------------------- 85 86 /** 87 * Constructs the FakeTerminal 88 * @return {void} 89 */ 90 base.__construct = function () { 91 92 base.$el.trigger('ft:init', [base]); 93 94 // Merge the options together 95 base.options = $.extend({}, window.FakeTerminal.defaultOptions, options); 96 97 // Copy the original markup so we can destroy nicely 98 base.originalHtml = base.$el.get(0).outerHTML; 99 base.existingText = base.$el.get(0).innerHTML ? base.$el.get(0).innerHTML.split('\n') : []; 100 101 // Prepare the element 102 base.$el 103 .addClass('faketerminal') 104 .empty(); 105 106 // Bind listeners 107 base.bindListeners(); 108 109 // Construct the core classes 110 base.output = new window.FakeTerminal.output(base); 111 base.input = new window.FakeTerminal.input(base); 112 base.filesystem = new window.FakeTerminal.filesystem(base); 113 base.history = new window.FakeTerminal.history(base); 114 115 /** 116 * Add the existing content; if there is more than one line of content skip the first and last line 117 * This is so that we can layout the HTML correctly, i.e contents on a new line 118 */ 119 for (var i = 0, j = base.existingText.length; i < j; i++) { 120 if (base.existingText.length > 1 && i === 0) { 121 continue;/**/ 122 } else if (base.existingText.length > 1 && i === base.existingText.length - 1) { 123 continue; 124 } 125 base.output.write($.trim(base.existingText[i])); 126 } 127 128 // Focus the input 129 base.input.focus(); 130 131 base.$el.trigger('ft:ready', [base]); 132 133 // Run the login command, if there is one 134 if (base.options.login) { 135 base.exec(base.options.login); 136 } 137 }; 138 139 // -------------------------------------------------------------------------- 140 141 /** 142 * Binds listeners to the DOM element 143 * @return {void} 144 */ 145 base.bindListeners = function () { 146 base.$el 147 .on('click', function () { 148 base.input.focus(); 149 }) 150 .on('keyup', function (e) { 151 if (e.ctrlKey && e.which === base.keymap.C) { 152 base.input.ctrlC(); 153 } else if (e.ctrlKey && e.which === base.keymap.U) { 154 base.input.ctrlU(); 155 } 156 }); 157 }; 158 159 // -------------------------------------------------------------------------- 160 161 /** 162 * Returns a new instance of a command if it exists 163 * @return {Object} 164 */ 165 base.findCommand = function (command) { 166 var cmdInstance; 167 if (typeof window.FakeTerminal.command[command] === 'function') { 168 cmdInstance = new window.FakeTerminal.command[command](base); 169 } 170 return cmdInstance; 171 }; 172 173 // -------------------------------------------------------------------------- 174 175 /** 176 * Sets the value of the prompt automatically 177 * @return {String} 178 */ 179 base.getPrompt = function () { 180 181 var hostname, username, cwd, text; 182 183 // Determine values 184 if (typeof base.options.hostname === 'function') { 185 hostname = base.options.hostname.call(); 186 } else { 187 hostname = base.options.hostname; 188 } 189 190 if (typeof base.options.username === 'function') { 191 username = base.options.username.call(); 192 } else { 193 username = base.options.username; 194 } 195 196 if (typeof base.options.cwd === 'function') { 197 cwd = base.options.cwd.call(); 198 } else { 199 cwd = base.options.cwd; 200 } 201 202 // Ensure the username is lowercase, alpha-numeric 203 username = username.toLowerCase().replace(/[^a-z0-9]/g, ''); 204 205 // Compile the string 206 text = base.options.prompt; 207 text = text.replace(/%hostname%/g, hostname); 208 text = text.replace(/%username%/g, username); 209 text = text.replace(/%cwd%/g, cwd); 210 211 return base.colorize(text); 212 }; 213 214 // -------------------------------------------------------------------------- 215 216 /** 217 * Replaces references to <info> etc with spans which can be colourised 218 * @param {String} line The line to colorize 219 * @returns {String} 220 */ 221 base.colorize = function (line) { 222 line = line.replace(/<([a-zA-Z].+?)>/g, '<span class="color--$1">', line); 223 line = line.replace(/<\/([a-zA-Z].+)>/g, '</span>', line); 224 return line; 225 }; 226 227 // -------------------------------------------------------------------------- 228 229 /** 230 * Scroll to the bottom of the terminal window 231 * @returns {Object} A reference to the class, for chaining 232 */ 233 base.scrollToBottom = function () { 234 base.$el.scrollTop(base.$el.get(0).scrollHeight); 235 return base; 236 }; 237 238 // -------------------------------------------------------------------------- 239 240 /** 241 * Destroys the fake terminal, reverting it back to its previous state 242 * @return {Object} A reference to the class, for chaining 243 */ 244 base.destroy = function () { 245 base.$el.replaceWith($(base.originalHtml)); 246 base.$el.trigger('ft:destroy', [base]); 247 return base; 248 }; 249 250 // -------------------------------------------------------------------------- 251 252 /** 253 * Executes a command 254 * @param {String} commandString The command to execute 255 * @param {Boolean} hidden Whether the commands are added to the command history 256 * @return {Object} A reference to the class, for chaining 257 */ 258 base.exec = function (commandString, hidden) { 259 260 var deferred, command, userArgs, commandInstance; 261 deferred = new $.Deferred(); 262 263 commandString = $.trim(commandString); 264 265 if (!hidden) { 266 base.output.write(commandString, true); 267 } 268 269 // If the command is empty then no need to proceed. 270 if (commandString.length === 0) { 271 deferred.reject(); 272 return deferred.promise(); 273 } 274 275 command = $.trim(commandString.split(' ').slice(0, 1)); 276 userArgs = commandString.split(' ').slice(1); 277 commandInstance = base.findCommand(command); 278 279 if (commandInstance) { 280 281 // If a command is currently executing, terminate it 282 if (base.executingCommand.instance) { 283 base.executingCommand.instance.terminate(); 284 base.executingCommand.instance = null; 285 base.executingCommand.deferred = null; 286 } 287 288 /** 289 * The command has been called, hide the prompt until the command resolves 290 * or if CTRL+C is encountered. 291 */ 292 base.input.disable(); 293 294 /** 295 * Call the command's execute function. It is responsible for output and 296 * resolving the promise once it's complete 297 */ 298 base.executingCommand.instance = commandInstance; 299 base.executingCommand.deferred = commandInstance 300 .execute.apply(commandInstance, userArgs) 301 .done(function () { 302 deferred.resolve(arguments); 303 }) 304 .fail(function () { 305 deferred.reject(arguments); 306 }) 307 .always(function () { 308 base.input.enable().focus(); 309 }); 310 311 } else if (command.length > 0) { 312 if (!hidden) { 313 base.output.write('command not found: "' + command + '"'); 314 } 315 deferred.reject(); 316 } else { 317 deferred.reject(); 318 } 319 320 if (!hidden) { 321 base.history.push(command); 322 } 323 324 base.$el.trigger('ft:command', [base, command]); 325 326 return deferred.promise(); 327 }; 328 329 // -------------------------------------------------------------------------- 330 331 // Run constructor 332 base.__construct(); 333 };