소스 검색

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 년 전
부모
커밋
63f1532710
2개의 변경된 파일169개의 추가작업 그리고 211개의 파일을 삭제
  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>