Ver código fonte

Refactor terminal tag to use riot observable API

This is cleaner than passing the terminal and shell object around, and
shell has more full control e.g. simulating an "app" is a lot simpler,
and also entirely up to the shell.
Weiyi Lou 10 anos atrás
pai
commit
63f1532710
2 arquivos alterados com 169 adições e 211 exclusões
  1. 81 84
      js/pgsh.js
  2. 88 127
      tags/terminal.tag

+ 81 - 84
js/pgsh.js

@@ -1,4 +1,20 @@
-function pgsh() {
+function pgsh(ev) {
+  var self = this
+
+  ev.on('cmd_entered', function(input) {
+    // First word is the command, all others are arguments.
+    var parts = input.trim().split(' ')
+    var command = self.active ? self.active : parts.splice(0, 1)
+    var args = parts
+    if (command in self.commands) {
+      self.commands[command].run(args)
+    }
+  })
+
+  var show = function(text) {
+    ev.trigger('disp_add', text)
+  }
+
   this.prompt = '<span style="color:blueviolet">pgs </span><span style="color:green">$ </span>'
   this.welcome = '\
 Linux parsleygardens.net 3.4.5-6-7-i286 #8 PGS Vimputer 3.4.56-7 i286 \n\
@@ -12,28 +28,28 @@ Type `help` for list of commands\n\
     'about': {
       'help': 'Author Information\nUsage: about',
       'run': function(args) {
-        if (args.length > 0) { return this.help }
-        return 'Site by Weiyi Lou ' + new Date().getFullYear()
+        if (args.length > 0) { show(this.help); return }
+        show('Site by Weiyi Lou ' + new Date().getFullYear())
       }
     },
     'clear': {
       'help': 'Clears the screen\nUsage: clear',
-      'run' : function(args, shell, terminal) {
-        terminal.display.clear()
+      'run' : function(args) {
+        ev.trigger('disp_clear')
       }
     },
     'version': {
       'help': 'Shell Information\nUsage: version',
       'run': function(args) {
-        if (args.length > 0) { return this.help }
-        return 'Parsley Gardens Shell (pgsh) 1.0.0. Built with <a target="_blank" href="http://riotjs.com/">Riot.js</a>'
+        if (args.length > 0) { show(this.help); return }
+        show('Parsley Gardens Shell (pgsh) 1.0.0. Built with <a target="_blank" href="http://riotjs.com/">Riot.js</a>')
       }
     },
     'git': {
       'help': 'Link to Code Repository\nUsage: git',
       'run': function(args) {
-        if (args.length > 0) { return this.help }
-        return 'Self-Hosted Code Repository at <a target="_blank" href="https://code.parsleygardens.net/explore/projects">code.parsleygardens.net</a>'
+        if (args.length > 0) { show(this.help); return }
+        show('Self-Hosted Code Repository at <a target="_blank" href="https://code.parsleygardens.net/explore/projects">code.parsleygardens.net</a>')
       }
     },
     'hello': {
@@ -41,122 +57,103 @@ Type `help` for list of commands\n\
       'run': function(args) {
         address = args.join(' ').trim()
         if (address.length == 0) {
-          return 'Hello to you too'
+          show('Hello to you too')
         } else if (address == 'pgsh') {
-          return 'Hello human'
+          show('Hello human')
         } else {
-          return 'My name is not "' + address + '"'
+          show('My name is not "' + address + '"')
         }
       }
     },
     'magic': {
       'help': 'Link to Artist\nUsage: magic',
       'run': function(args) {
-        if (args.length > 0) { return this.help }
-        return 'Animation and Illustration at <a target="_blank" href="http://slightlymagic.com.au">slightlymagic.com.au</a>'
+        if (args.length > 0) { show(this.help); return }
+        show('Animation and Illustration at <a target="_blank" href="http://slightlymagic.com.au">slightlymagic.com.au</a>')
       }
     },
     'su': {
       'help': 'Substitute as root user for Phenomenal Cosmic Power\nUsage: su',
-      'run': function(args, shell, terminal) {
-        if (args.length > 0) { return this.help }
-        var output = ''
-        if (!shell.su) {
-          terminal.commandline.setPrompt('<span style="color:tomato">root </span><span style="color:red">% </span>')
-          shell.su = true
-          output = 'With Great Power comes Great'
+      'run': function(args) {
+        if (args.length > 0) { show(this.help); return }
+        if (!self.su) {
+          self.su = true
+          ev.trigger('prompt_set', '<span style="color:tomato">root </span><span style="color:red">% </span>')
+          show('With Great Power comes Great')
         }
-        return output
       }
     },
     'exit': {
       'help': 'Leave the current context\nUsage: exit',
-      'run': function(args, shell, terminal) {
-        if (args.length > 0) { return this.help }
-        var output = 'Close browser window to exit'
-        if (shell.su) {
-          terminal.commandline.setPrompt(shell.prompt)
-          shell.su = false
-          output = ''
+      'run': function(args) {
+        if (args.length > 0) { show(this.help); return }
+        if (self.su) {
+          self.su = false
+          ev.trigger('prompt_set', self.prompt)
+          return
         }
-        return output
+        show('Close browser window to exit')
       }
     },
     'search': {
       'help': 'Search the Web (with a Duck)\nUsage: search [query]',
-      'run': function(args, shell, terminal) {
-        if (!args.join(' ').trim()) { return this.help }
-        terminal.commandline.hidePrompt()
+      'run': function(args) {
+        if (!args.join(' ').trim()) { show(this.help); return }
+        ev.trigger('prompt_hide')
+        show('Searching for "' + args.join(' ') + '" in new window...')
         setTimeout(function() {
           window.open('https://duckduckgo.com/?q=' + args.join('+'), '_blank')
-          terminal.commandline.showPrompt()
-          terminal.update()
+          ev.trigger('prompt_show')
         }, 1000)
-        return 'Searching for "' + args.join(' ') + '" in new window...'
       }
     },
     'questions': {
       'help': 'Answer some questions!\nUsage: questions',
-      'run': function() {
-        return {
-          'run': function(input, terminal) {
-            if (!this.running) {
-              this.running = true
-              terminal.display.hide()
-              terminal.display.add('Welcome to the questions. Do you want to continue?')
-              terminal.commandline.hidePrompt()
-              return
-            }
-            if (input != 'exit') {
-              terminal.display.set('You answered with "' + input + '"!\nNext question!\n<span style="color:#555">(Type "exit" to end)</span>\n\n')
-              var rand = Math.floor(Math.random() * this.questions.length);
-              terminal.display.add(this.questions[rand])
-            } else {
-              terminal.display.restore()
-              terminal.display.add('Thanks for answering questions!')
-              terminal.commandline.showPrompt()
-              terminal.returnToShell()
-            }
-          },
-          'questions': [
-            'Isn\'t <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">this song</a> the best?',
-            'Iggledy Piggledy',
-            '1 + 2 = ?',
-            'Am I a sandwich?',
-            'Where were you at 3:15am on April 14th?',
-            "Don't you mean prism?",
-            'Buts twelve by pies?'
-          ]
+      'run': function(input) {
+        if (!self.active) {
+          self.active = 'questions'
+          ev.trigger('disp_hide')
+          ev.trigger('prompt_hide')
+          show('Welcome to the questions. Do you want to continue?')
+          return
         }
-      }
+        if (input != 'exit') {
+          ev.trigger('disp_set', 'You answered with "' + input + '"!\nNext question!\n<span style="color:#555">(Type "exit" to end)</span>\n\n')
+          var rand = Math.floor(Math.random() * this.questions.length);
+          show(this.questions[rand])
+        } else {
+          ev.trigger('disp_restore')
+          ev.trigger('prompt_show')
+          show('Thanks for answering questions!')
+          self.active = ''
+        }
+      },
+      'questions': [
+        'Isn\'t <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">this song</a> the best?',
+        'Iggledy Piggledy',
+        '1 + 2 = ?',
+        'Am I a sandwich?',
+        'Where were you at 3:15am on April 14th?',
+        "Don't you mean prism?",
+        'Buts twelve by pies?'
+      ]
     },
     'help': {
       'help': 'List available commands or view information for a given command\nUsage: help [command]',
-      'run': function(args, shell) {
+      'run': function(args) {
         command = args.join(' ').trim()
-        if (command in shell.commands) {
-          return shell.commands[command].help
+        if (command in self.commands) {
+          show(self.commands[command].help)
         } else if (command) {
-          return 'Command not found: ' + command
+          show('Command not found: ' + command)
         } else {
           var commands = [];
-          for(var name in shell.commands) {
+          for(var name in self.commands) {
             commands.push(name)
           }
-          return 'Available commands:\n' + commands.sort().join(' ') + '\n\n`help [command]` for more information.'
+          show('Available commands:\n' + commands.sort().join(' ') + '\n\n`help [command]` for more information.')
         }
       }
     }
   }
-  this.run = function(input, terminal) {
-    var parts = input.trim().split(' ')
-    // First word is the command, all others are arguments.
-    var command = parts.splice(0, 1)
-    var args = parts
-    var output = ''
-    if (command in this.commands) {
-      output = this.commands[command].run(args, this, terminal)
-    }
-    return output
-  }
 }

+ 88 - 127
tags/terminal.tag

@@ -2,23 +2,24 @@
  * # Terminal Riot Tag
  *
  * Provides a pretend commandline interface, capable of displaying output from a
- * "shell" javascript class. Multiple terminal tags can be used on a page, using
+ * "shell" JavaScript class. Multiple terminal tags can be used on a page, using
  * the same shell class or different ones - each terminal will work
  * independently.
  *
  * ## Defaults
  *
- * The default prompt is `'$'`. There is no default the welcome message. The
- * default shell performs no actions: pressing enter will simply move the cursor
- * to the next prompt line.
+ * The default prompt is `'$ '`.
+ * The default welcome message is empty.
+ * The default shell performs no actions: pressing enter will simply move the
+ * cursor to the next prompt line.
  *
  * ## Usage
  *
- *     <terminal shell='jsclass' welcome='text' prompt='text'></terminal>
+ *     <terminal shell='myshellclass' welcome='text' prompt='text'></terminal>
  *
  *     <script src='riot+compiler.min.js'></script>
  *     <script src='he.js'></script>
- *     <script src='shell.js'></script>
+ *     <script src='myshell.js'></script>
  *     <script src='terminal.tag' type='riot/tag'></script>
  *     <script>riot.mount('terminal')</script>
  *
@@ -29,101 +30,45 @@
  *
  * ## Making A Shell Class
  *
- * The shell class must be defined before the tag is mounted. Shells can keep
- * prompt and welcome message settings, and must define a `run()` function.
+ * A shell class must be defined before the tag is mounted. Shells can keep
+ * prompt and welcome message settings, and should listen to the `'cmd_entered'`
+ * event to process input from the `commandline` tag.
+ *
  * Here is the structure of a minimal shell that does nothing:
  *
  *     // Contents of myshell.js
- *     function() myshell() {
+ *     function myshellclass(events) {
  *        this.prompt = ''
  *        this.welcome = ''
- *        this.run = function(input, terminal) {
- *          return ''
- *        }
+ *        events.on('cmd_entered', function(input) {
+ *          // Do nothing
+ *        })
  *     }
  *
- *     // Shell class used by Terminal Riot Tag in index.html
- *     <terminal shell='myshell'></terminal>
- *
- * The `run()` function can take 2 parameters. The first will contain user input
- * from the command line. The second will contain the terminal object itself -
- * this will allow the shell access to all of the terminal's functions, mainly
- * through its 2 child tags, `terminal.display` and
- * `terminal.commandline`. Refer those tags below for capabilities.
- *
- * A shell can, at its most basic operation, return a string of text or HTML,
- * and this will be appended to the display.
- *
- * ## Apps
+ * The shell can trigger events available on the `display` and `commandline`
+ * tags to make things happen:
  *
- * A shell can also return an object that will temporarily process all input,
- * until the object returns control back to the shell. This can be thought of as
- * the shell returning an "app" object.
+ *     events.trigger('disp_add', text)   // Append `text` to the display
+ *     events.trigger('disp_set', text)   // Display only `text`
+ *     events.trigger('disp_clear')       // Clear the display
+ *     events.trigger('disp_hide')        // Save the display, then clear it
+ *     events.trigger('disp_restore')     // Restore the saved display
+ *     events.trigger('prompt_set', text) // Change the command prompt to `text`
+ *     events.trigger('prompt_hide')      // Hide the command prompt
+ *     events.trigger('prompt_show')      // Show the command prompt, if hidden
  *
- * An "app" object should implement a `run()` function, just like the shell
- * class. It should also have exit conditions under which it will return control
- * to the shell by calling `terminal.returnToShell()`.
- *
- *     {
- *       'run': function(input, terminal) {
- *         terminal.commandline.hidePrompt()
- *         terminal.display.add('Hello! inputs will now come to this app.')
- *         if (input == 'exit') {
- *           terminal.display.add('Bye! Returning control to the shell.')
- *           terminal.commandline.showPrompt()
- *           terminal.returnToShell()
- *         }
- *       }
- *     }
- *
- * An "app" is an object, so can keep state information for a current run with
- * `this`. For more persistent saving of information, it is also possible to
- * save data to `terminal` or to `terminal.shell`.
  */
 <terminal>
-  <display welcome={ welcome } />
-  <commandline prompt={ prompt } />
-
-  // Create a new shell with the class name given to the terminal tag.
-  var shell = { 'run': function() { return ''; } }
-  this.shell = window[opts.shell] ? new window[opts.shell] : shell
-  this.active = this.shell
-  this.welcome = this.shell.welcome || opts.welcome
-  this.prompt = this.shell.prompt || opts.prompt
-  this.display = this.tags.display
-  this.commandline = this.tags.commandline
+  <display welcome={ welcome } events={ this } />
+  <commandline prompt={ prompt } events={ this } />
 
   /**
-   * How to process a command:
-   *
-   * - Make the input safe by transforming html entities.
-   * - Keep the last command line on display before other output.
-   * - Based on shell response, append output, or give control to an app object.
-   * - Update the display.
+   * Create a new shell with the class name given to the terminal tag.
+   * The terminal tag object passes events between the shell and the other tags.
    */
-  var self = this
-  this.commandline.on('cmdEntered', function(prompt, input) {
-    input = he.encode(input)
-    self.display.add(prompt + input + '\n')
-    var response = self.active.run(input, self)
-    if (typeof response === 'string') {
-      self.display.add(response)
-    }
-    if (typeof response === 'object' && typeof response.run === 'function') {
-      self.active = response
-      response.run(input, self)
-    }
-    self.update()
-  })
-
-  /**
-   * Make the shell the active process.
-   *
-   * For app objects to return control to the shell when finished.
-   */
-  returnToShell() {
-    this.active = this.shell
-  }
+  var shell = window[opts.shell] ? new window[opts.shell](this) : {}
+  this.welcome = shell.welcome || opts.welcome
+  this.prompt = shell.prompt || opts.prompt
 </terminal>
 
 <display>
@@ -131,38 +76,55 @@
     <raw content={ content } />
   </div>
 
-  add(text) {
+  var ev = opts.events
+  var self = this
+  this.output = []
+
+  this.on('mount', function() {
+    this.add(opts.welcome)
+  })
+
+  ev.on('disp_add', function(text) {
+    self.add(text)
+  })
+
+  ev.on('disp_set', function(text) {
     if (text) {
-      text = this.preserveWhiteSpace(text)
-      this.output.push({ 'content': text })
+      self.clear()
+      self.add(text)
     }
-  }
+  })
+
+  ev.on('disp_clear', function() {
+    self.clear()
+  })
 
-  set(text) {
+  ev.on('disp_hide', function() {
+    self.saved = self.output.splice(0, self.output.length)
+    self.clear()
+  })
+
+  ev.on('disp_restore', function() {
+    if (self.saved.length > 0) {
+      self.clear()
+      self.output = self.saved.splice(0, self.saved.length)
+      self.update()
+    }
+  })
+
+  add(text) {
     if (text) {
-      this.clear()
-      this.add(text)
+      text = this.preserveWhiteSpace(text)
+      this.output.push({ 'content': text })
+      this.update()
     }
   }
 
   clear() {
     this.output.length = 0
-    // This is needed for subsequent changes to `output` to display properly.
     this.update()
   }
 
-  hide() {
-    this.saved = this.output.splice(0, this.output.length)
-    this.clear()
-  }
-
-  restore() {
-    if (this.saved.length > 0) {
-      this.clear()
-      this.output = this.saved.splice(0, this.saved.length)
-    }
-  }
-
   preserveWhiteSpace(text) {
     text = text.replace(/(?:\r\n|\r|\n)/g, '<br />')
     // Search for tags or whitespace. Escape whitespace, leave tags.
@@ -172,15 +134,11 @@
     })
     return text
   }
-
-  this.output = []
-  this.add(opts.welcome)
 </display>
 
 <commandline>
   <form autocomplete='off' onsubmit={ process }>
-    <raw name='lhs' content={ prompt } show={ visible } />
-    <input type='text' name='command' />
+    <raw name='lhs' content={ prompt } show={ visible } /><input type='text' name='command' />
   </form>
 
   <style>
@@ -192,6 +150,8 @@
     }
   </style>
 
+  var ev = opts.events
+  var self = this
   this.prompt = opts.prompt || '$ '
   this.visible = true
 
@@ -199,24 +159,25 @@
     document.getElementsByName('command')[0].focus()
   })
 
-  process() {
-    var prompt = this.visible ? this.prompt : '';
-    this.trigger('cmdEntered', prompt, this.command.value)
-    this.command.value = ''
-  }
-
-  setPrompt(value) {
-    this.prompt = value
+  ev.on('prompt_set', function(value) {
+    self.prompt = value
     // `write()` is called to actually update the `raw` tag's html.
-    this.tags.lhs.write(value)
-  }
+    self.tags.lhs.write(value)
+  })
 
-  hidePrompt() {
-    this.visible = false
-  }
+  ev.on('prompt_hide', function() {
+    self.update({ visible: false })
+  })
+
+  ev.on('prompt_show', function() {
+    self.update({ visible: true })
+  })
 
-  showPrompt() {
-    this.visible = true
+  process() {
+    var prompt = this.visible ? this.prompt : '';
+    ev.trigger('disp_add', prompt + this.command.value + '\n')
+    ev.trigger('cmd_entered', this.command.value)
+    this.command.value = ''
   }
 </commandline>