terminal.tag 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /**
  2. * # Terminal Riot Tag
  3. *
  4. * Provides a pretend commandline interface, capable of displaying output from a
  5. * "shell" javascript class. Multiple terminal tags can be used on a page, using
  6. * the same shell class or different ones - each terminal will work
  7. * independently.
  8. *
  9. * ## Defaults
  10. *
  11. * The default prompt is `'$'`. There is no default the welcome message. The
  12. * default shell performs no actions: pressing enter will simply move the cursor
  13. * to the next prompt line.
  14. *
  15. * ## Usage
  16. *
  17. * <terminal shell='jsclass' welcome='text' prompt='text'></terminal>
  18. *
  19. * <script src='riot+compiler.min.js'></script>
  20. * <script src='he.js'></script>
  21. * <script src='shell.js'></script>
  22. * <script src='terminal.tag' type='riot/tag'></script>
  23. * <script>riot.mount('terminal')</script>
  24. *
  25. * ## Dependencies
  26. *
  27. * - riot.js (http://riotjs.com/)
  28. * - he.js (https://github.com/mathiasbynens/he) for HTML Entity conversion.
  29. *
  30. * ## Making A Shell Class
  31. *
  32. * The shell class must be defined before the tag is mounted. Shells can keep
  33. * prompt and welcome message settings, and must define a `run()` function.
  34. * Here is the structure of a minimal shell that does nothing:
  35. *
  36. * // Contents of myshell.js
  37. * function() myshell() {
  38. * this.prompt = ''
  39. * this.welcome = ''
  40. * this.run = function(input, terminal) {
  41. * return ''
  42. * }
  43. * }
  44. *
  45. * // Shell class used by Terminal Riot Tag in index.html
  46. * <terminal shell='myshell'></terminal>
  47. *
  48. * The `run()` function can take 2 parameters. The first will contain user input
  49. * from the command line. The second will contain the terminal object itself -
  50. * this will allow the shell access to all of the terminal's functions, mainly
  51. * through its 2 child tags, `terminal.display` and
  52. * `terminal.commandline`. Refer those tags below for capabilities.
  53. *
  54. * A shell can, at its most basic operation, return a string of text or HTML,
  55. * and this will be appended to the display.
  56. *
  57. * ## Apps
  58. *
  59. * A shell can also return an object that will temporarily process all input,
  60. * until the object returns control back to the shell. This can be thought of as
  61. * the shell returning an "app" object.
  62. *
  63. * An "app" object should implement a `run()` function, just like the shell
  64. * class. It should also have exit conditions under which it will return control
  65. * to the shell by calling `terminal.returnToShell()`.
  66. *
  67. * {
  68. * 'run': function(input, terminal) {
  69. * terminal.commandline.hidePrompt()
  70. * terminal.display.add('Hello! inputs will now come to this app.')
  71. * if (input == 'exit') {
  72. * terminal.display.add('Bye! Returning control to the shell.')
  73. * terminal.commandline.showPrompt()
  74. * terminal.returnToShell()
  75. * }
  76. * }
  77. * }
  78. *
  79. * An "app" is an object, so can keep state information for a current run with
  80. * `this`. For more persistent saving of information, it is also possible to
  81. * save data to `terminal` or to `terminal.shell`.
  82. */
  83. <terminal>
  84. <display welcome={ welcome } />
  85. <commandline prompt={ prompt } />
  86. // Create a new shell with the class name given to the terminal tag.
  87. var shell = { 'run': function() { return ''; } }
  88. this.shell = window[opts.shell] ? new window[opts.shell] : shell
  89. this.active = this.shell
  90. this.welcome = this.shell.welcome || opts.welcome
  91. this.prompt = this.shell.prompt || opts.prompt
  92. this.display = this.tags.display
  93. this.commandline = this.tags.commandline
  94. /**
  95. * How to process a command:
  96. *
  97. * - Make the input safe by transforming html entities.
  98. * - Keep the last command line on display before other output.
  99. * - Based on shell response, append output, or give control to an app object.
  100. * - Update the display.
  101. */
  102. var self = this
  103. this.commandline.on('cmdEntered', function(prompt, input) {
  104. input = he.encode(input)
  105. self.display.add(prompt + input + '\n')
  106. var response = self.active.run(input, self)
  107. if (typeof response === 'string') {
  108. self.display.add(response)
  109. }
  110. if (typeof response === 'object' && typeof response.run === 'function') {
  111. self.active = response
  112. response.run(input, self)
  113. }
  114. self.update()
  115. })
  116. /**
  117. * Make the shell the active process.
  118. *
  119. * For app objects to return control to the shell when finished.
  120. */
  121. returnToShell() {
  122. this.active = this.shell
  123. }
  124. </terminal>
  125. <display>
  126. <div each={ output }>
  127. <raw content={ content } />
  128. </div>
  129. add(text) {
  130. if (text) {
  131. text = this.preserveWhiteSpace(text)
  132. this.output.push({ 'content': text })
  133. }
  134. }
  135. set(text) {
  136. if (text) {
  137. this.clear()
  138. this.add(text)
  139. }
  140. }
  141. clear() {
  142. this.output.length = 0
  143. // This is needed for subsequent changes to `output` to display properly.
  144. this.update()
  145. }
  146. hide() {
  147. this.saved = this.output.splice(0, this.output.length)
  148. this.clear()
  149. }
  150. restore() {
  151. if (this.saved.length > 0) {
  152. this.clear()
  153. this.output = this.saved.splice(0, this.saved.length)
  154. }
  155. }
  156. preserveWhiteSpace(text) {
  157. text = text.replace(/(?:\r\n|\r|\n)/g, '<br />')
  158. // Search for tags or whitespace. Escape whitespace, leave tags.
  159. text = text.replace(/<[^<]+>|( )/g, function(match, group1) {
  160. if (group1 == " ") { return '&nbsp;' }
  161. return match
  162. })
  163. return text
  164. }
  165. this.output = []
  166. this.add(opts.welcome)
  167. </display>
  168. <commandline>
  169. <form autocomplete='off' onsubmit={ process }>
  170. <raw name='lhs' content={ prompt } show={ visible } />
  171. <input type='text' name='command' />
  172. </form>
  173. <style>
  174. input[name='command'] {
  175. background: transparent;
  176. border: none; outline: none;
  177. padding: 0; margin: 0;
  178. width: 90%;
  179. }
  180. </style>
  181. this.prompt = opts.prompt || '$ '
  182. this.visible = true
  183. this.on('mount', function() {
  184. document.getElementsByName('command')[0].focus()
  185. })
  186. process() {
  187. var prompt = this.visible ? this.prompt : '';
  188. this.trigger('cmdEntered', prompt, this.command.value)
  189. this.command.value = ''
  190. }
  191. setPrompt(value) {
  192. this.prompt = value
  193. // `write()` is called to actually update the `raw` tag's html.
  194. this.tags.lhs.write(value)
  195. }
  196. hidePrompt() {
  197. this.visible = false
  198. }
  199. showPrompt() {
  200. this.visible = true
  201. }
  202. </commandline>
  203. <raw>
  204. <span></span>
  205. // Initialise HTML from tag options and expose `write()` for updating.
  206. write(text) {
  207. this.root.innerHTML = text
  208. }
  209. this.write(opts.content)
  210. </raw>