web-console 0.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of web-console might be problematic. Click here for more details.

Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.markdown +121 -0
  4. data/Rakefile +48 -0
  5. data/app/assets/javascripts/web_console/application.js +4 -0
  6. data/app/assets/javascripts/web_console/console_sessions.js +24 -0
  7. data/app/assets/stylesheets/web_console/application.css +13 -0
  8. data/app/assets/stylesheets/web_console/console_sessions.css +8 -0
  9. data/app/controllers/web_console/application_controller.rb +12 -0
  10. data/app/controllers/web_console/console_sessions_controller.rb +23 -0
  11. data/app/helpers/web_console/application_helper.rb +4 -0
  12. data/app/helpers/web_console/console_session_helper.rb +4 -0
  13. data/app/models/web_console/console_session.rb +107 -0
  14. data/app/views/layouts/web_console/application.html.erb +14 -0
  15. data/app/views/web_console/console_sessions/index.html.erb +4 -0
  16. data/config/routes.rb +5 -0
  17. data/lib/web-console.rb +1 -0
  18. data/lib/web_console.rb +7 -0
  19. data/lib/web_console/engine.rb +38 -0
  20. data/lib/web_console/fiber.rb +48 -0
  21. data/lib/web_console/repl.rb +59 -0
  22. data/lib/web_console/repl/dummy.rb +38 -0
  23. data/lib/web_console/repl/irb.rb +61 -0
  24. data/lib/web_console/stream.rb +27 -0
  25. data/lib/web_console/version.rb +3 -0
  26. data/test/controllers/web_console/console_sessions_controller_test.rb +57 -0
  27. data/test/dummy/README.rdoc +28 -0
  28. data/test/dummy/Rakefile +6 -0
  29. data/test/dummy/app/assets/javascripts/application.js +13 -0
  30. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  31. data/test/dummy/app/controllers/application_controller.rb +5 -0
  32. data/test/dummy/app/helpers/application_helper.rb +2 -0
  33. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  34. data/test/dummy/bin/bundle +3 -0
  35. data/test/dummy/bin/rails +4 -0
  36. data/test/dummy/bin/rake +4 -0
  37. data/test/dummy/config.ru +4 -0
  38. data/test/dummy/config/application.rb +15 -0
  39. data/test/dummy/config/boot.rb +5 -0
  40. data/test/dummy/config/database.yml +25 -0
  41. data/test/dummy/config/environment.rb +5 -0
  42. data/test/dummy/config/environments/development.rb +29 -0
  43. data/test/dummy/config/environments/production.rb +80 -0
  44. data/test/dummy/config/environments/test.rb +36 -0
  45. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  46. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  47. data/test/dummy/config/initializers/inflections.rb +16 -0
  48. data/test/dummy/config/initializers/mime_types.rb +5 -0
  49. data/test/dummy/config/initializers/secret_token.rb +12 -0
  50. data/test/dummy/config/initializers/session_store.rb +3 -0
  51. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  52. data/test/dummy/config/locales/en.yml +23 -0
  53. data/test/dummy/config/routes.rb +2 -0
  54. data/test/dummy/db/development.sqlite3 +0 -0
  55. data/test/dummy/db/schema.rb +16 -0
  56. data/test/dummy/db/test.sqlite3 +0 -0
  57. data/test/dummy/log/development.log +26591 -0
  58. data/test/dummy/log/test.log +78368 -0
  59. data/test/dummy/public/404.html +58 -0
  60. data/test/dummy/public/422.html +58 -0
  61. data/test/dummy/public/500.html +57 -0
  62. data/test/dummy/public/favicon.ico +0 -0
  63. data/test/dummy/tmp/cache/assets/development/sprockets/0280bb38c5058cc31c4fcd8c392a5ec4 +0 -0
  64. data/test/dummy/tmp/cache/assets/development/sprockets/05bd7b24c0a86010ebb28b50ac7cad52 +0 -0
  65. data/test/dummy/tmp/cache/assets/development/sprockets/2a48be7daff14cf56911b263cbe017c7 +0 -0
  66. data/test/dummy/tmp/cache/assets/development/sprockets/3a37adb5ebd079cf67dae597b8c2e4f8 +0 -0
  67. data/test/dummy/tmp/cache/assets/development/sprockets/4f79e5f341043e081becefe4952395c5 +0 -0
  68. data/test/dummy/tmp/cache/assets/development/sprockets/508ab3c25833ea537a5e3fc90df33595 +0 -0
  69. data/test/dummy/tmp/cache/assets/development/sprockets/5a8ab22e707dfc7ba00691f90e054d7e +0 -0
  70. data/test/dummy/tmp/cache/assets/development/sprockets/5adcb7569cdd03204d650e285f19351f +0 -0
  71. data/test/dummy/tmp/cache/assets/development/sprockets/654a1bde557d359b957dc0aa12b0dfa0 +0 -0
  72. data/test/dummy/tmp/cache/assets/development/sprockets/71f57313d6c92f5483915a6c2d79a506 +0 -0
  73. data/test/dummy/tmp/cache/assets/development/sprockets/7ec0041a47c34b44e52e836c01454a1a +0 -0
  74. data/test/dummy/tmp/cache/assets/development/sprockets/a1534e0c08b73ad82c2edb8c364caa66 +0 -0
  75. data/test/dummy/tmp/cache/assets/development/sprockets/a841a3af20a321912acfed87036438fb +0 -0
  76. data/test/dummy/tmp/cache/assets/development/sprockets/ad8eea7f774b674c29708ca90952764f +0 -0
  77. data/test/dummy/tmp/cache/assets/development/sprockets/b0741545b4917192ba7b5e803c2e323d +0 -0
  78. data/test/dummy/tmp/cache/assets/development/sprockets/b3159b06164dd474ff8efc3c84aefbba +0 -0
  79. data/test/dummy/tmp/cache/assets/development/sprockets/b40b7c5b2003544010f30f0bd3fb81b2 +0 -0
  80. data/test/dummy/tmp/cache/assets/development/sprockets/bbbe6a3ce382662666a355d708c83d2d +0 -0
  81. data/test/dummy/tmp/cache/assets/development/sprockets/c4e26d8dbebb3afd7013acfefa564dd1 +0 -0
  82. data/test/dummy/tmp/cache/assets/development/sprockets/ca53fb2717d5aac2f1c3939d9444fe3b +0 -0
  83. data/test/dummy/tmp/cache/assets/development/sprockets/d6b37d10680a997662c379d0ff7cad27 +0 -0
  84. data/test/dummy/tmp/cache/assets/development/sprockets/e1d89809967e81220dca66770c50aa67 +0 -0
  85. data/test/dummy/tmp/cache/assets/development/sprockets/e3d5cafc071e8f9a8efc88fddf721947 +0 -0
  86. data/test/dummy/tmp/cache/assets/development/sprockets/f44e2a41bd51a92a84f99847ea675ba3 +0 -0
  87. data/test/dummy/tmp/cache/assets/development/sprockets/f4a21ed9cebe2c83a9d988504dc6720b +0 -0
  88. data/test/dummy/tmp/cache/assets/development/sprockets/f4d45273ff5b44879dab0a16805f4309 +0 -0
  89. data/test/dummy/tmp/cache/assets/development/sprockets/f5deea0ae9671fdb035aefc6c4ba1109 +0 -0
  90. data/test/dummy/tmp/cache/assets/test/sprockets/0280bb38c5058cc31c4fcd8c392a5ec4 +0 -0
  91. data/test/dummy/tmp/cache/assets/test/sprockets/05bd7b24c0a86010ebb28b50ac7cad52 +0 -0
  92. data/test/dummy/tmp/cache/assets/test/sprockets/2a48be7daff14cf56911b263cbe017c7 +0 -0
  93. data/test/dummy/tmp/cache/assets/test/sprockets/3824a9e25e846a4916b3ac1d67060782 +0 -0
  94. data/test/dummy/tmp/cache/assets/test/sprockets/3a37adb5ebd079cf67dae597b8c2e4f8 +0 -0
  95. data/test/dummy/tmp/cache/assets/test/sprockets/4f79e5f341043e081becefe4952395c5 +0 -0
  96. data/test/dummy/tmp/cache/assets/test/sprockets/508ab3c25833ea537a5e3fc90df33595 +0 -0
  97. data/test/dummy/tmp/cache/assets/test/sprockets/5a8ab22e707dfc7ba00691f90e054d7e +0 -0
  98. data/test/dummy/tmp/cache/assets/test/sprockets/5adcb7569cdd03204d650e285f19351f +0 -0
  99. data/test/dummy/tmp/cache/assets/test/sprockets/632346eda030b596f513fff2de181743 +0 -0
  100. data/test/dummy/tmp/cache/assets/test/sprockets/654a1bde557d359b957dc0aa12b0dfa0 +0 -0
  101. data/test/dummy/tmp/cache/assets/test/sprockets/71f57313d6c92f5483915a6c2d79a506 +0 -0
  102. data/test/dummy/tmp/cache/assets/test/sprockets/9b9aee85f29dc573732fbb4001ceda00 +0 -0
  103. data/test/dummy/tmp/cache/assets/test/sprockets/a841a3af20a321912acfed87036438fb +0 -0
  104. data/test/dummy/tmp/cache/assets/test/sprockets/bbbe6a3ce382662666a355d708c83d2d +0 -0
  105. data/test/dummy/tmp/cache/assets/test/sprockets/ca53fb2717d5aac2f1c3939d9444fe3b +0 -0
  106. data/test/dummy/tmp/cache/assets/test/sprockets/d6b37d10680a997662c379d0ff7cad27 +0 -0
  107. data/test/dummy/tmp/cache/assets/test/sprockets/e1d89809967e81220dca66770c50aa67 +0 -0
  108. data/test/dummy/tmp/cache/assets/test/sprockets/e3d5cafc071e8f9a8efc88fddf721947 +0 -0
  109. data/test/dummy/tmp/cache/assets/test/sprockets/f44e2a41bd51a92a84f99847ea675ba3 +0 -0
  110. data/test/dummy/tmp/cache/assets/test/sprockets/f4a21ed9cebe2c83a9d988504dc6720b +0 -0
  111. data/test/dummy/tmp/cache/assets/test/sprockets/f4d45273ff5b44879dab0a16805f4309 +0 -0
  112. data/test/dummy/tmp/cache/assets/test/sprockets/f5deea0ae9671fdb035aefc6c4ba1109 +0 -0
  113. data/test/dummy/tmp/pids/server.pid +1 -0
  114. data/test/helpers/web_console/console_session_helper_test.rb +6 -0
  115. data/test/models/console_session_test.rb +110 -0
  116. data/test/test_helper.rb +15 -0
  117. data/test/web_console/repl/dummy_test.rb +54 -0
  118. data/test/web_console/repl/irb_test.rb +108 -0
  119. data/test/web_console/repl_test.rb +15 -0
  120. data/test/web_console_test.rb +91 -0
  121. data/vendor/assets/javascripts/jquery.console.js +727 -0
  122. metadata +303 -0
@@ -0,0 +1,108 @@
1
+ require 'test_helper'
2
+
3
+ class IRBTest < ActiveSupport::TestCase
4
+ setup do
5
+ @irb1 = @irb = WebConsole::REPL::IRB.new
6
+ @irb2 = WebConsole::REPL::IRB.new
7
+ end
8
+
9
+ test 'sending input returns the result as output' do
10
+ assert_equal return_prompt(42), @irb.send_input('foo = 42')
11
+ end
12
+
13
+ test 'preserves the session in the binding' do
14
+ assert_equal return_prompt(42), @irb.send_input('foo = 42')
15
+ assert_equal return_prompt(50), @irb.send_input('foo + 8')
16
+ end
17
+
18
+ test 'session isolation requires own bindings' do
19
+ irb1 = WebConsole::REPL::IRB.new(Object.new.instance_eval('binding'))
20
+ irb2 = WebConsole::REPL::IRB.new(Object.new.instance_eval('binding'))
21
+ assert_equal return_prompt(42), irb1.send_input('foo = 42')
22
+ assert_match %r{NameError}, irb2.send_input('foo')
23
+ end
24
+
25
+ test 'session preservation requires same bindings' do
26
+ assert_equal return_prompt(42), @irb1.send_input('foo = 42')
27
+ assert_equal return_prompt(42), @irb2.send_input('foo')
28
+ end
29
+
30
+ test 'multiline sessions' do
31
+ irb = WebConsole::REPL::IRB.new(Object.new.instance_eval('binding'))
32
+ assert_equal "", irb.send_input('class A')
33
+ assert_equal return_prompt('nil'), irb.send_input('end')
34
+ assert_no_match %r{NameError}, irb.send_input('A')
35
+ end
36
+
37
+ test 'captures direct stdout output' do
38
+ assert_equal "42\n#{return_prompt('nil')}", @irb.send_input('puts 42')
39
+ end
40
+
41
+ test 'captures direct stderr output' do
42
+ assert_equal "42\n#{return_prompt(3)}", @irb.send_input('$stderr.write("42\n")')
43
+ end
44
+
45
+ test 'captures direct output from subprocesses' do
46
+ assert_equal "42\n#{return_prompt(true)}", @irb.send_input('system "echo 42"')
47
+ end
48
+
49
+ test 'captures direct output from forks' do
50
+ # This is a bummer, but currently I don't see how we can work around it,
51
+ # without monkey patching fork and the crew to be blocking calls. This
52
+ # won't scale well, but at least fork will show results. Otherwise, we can
53
+ # document the behaviour and expect the user to wait themselves, if they
54
+ # care about the output.
55
+ assert_match %r{42\n}, @irb.send_input('Process.wait(fork { puts 42 })')
56
+ end
57
+
58
+ test 'multiline support between threads' do
59
+ assert_equal "", @irb.send_input('class A')
60
+ Thread.new do
61
+ assert_equal return_prompt('nil'), @irb.send_input('end')
62
+ assert_no_match %r{NameError}, @irb.send_input('A')
63
+ end.join
64
+ end
65
+
66
+ test 'prompt is present' do
67
+ assert_not_nil @irb.prompt
68
+ end
69
+
70
+ test 'prompt is determined by ::IRB.conf' do
71
+ with_simple_prompt do
72
+ assert '>> ', WebConsole::REPL::IRB.new.prompt
73
+ end
74
+ end
75
+
76
+ test 'rails helpers are available in the session' do
77
+ each_rails_console_method do |meth|
78
+ assert_equal return_prompt(true), @irb.send_input("respond_to? :#{meth}")
79
+ end
80
+ end
81
+
82
+ private
83
+ def currently_selected_prompt
84
+ ::IRB.conf[:PROMPT][::IRB.conf[:PROMPT_MODE]]
85
+ end
86
+
87
+ def return_prompt(*args)
88
+ sprintf(currently_selected_prompt[:RETURN], *args)
89
+ end
90
+
91
+ def input_prompt
92
+ currently_selected_prompt[:PROMPT_I]
93
+ end
94
+
95
+ def with_simple_prompt
96
+ previous_prompt = ::IRB.conf[:PROMPT]
97
+ ::IRB.conf[:PROMPT] = :simple
98
+ yield
99
+ ensure
100
+ ::IRB.conf[:PROMPT] = previous_prompt
101
+ end
102
+
103
+ def each_rails_console_method(&block)
104
+ require 'rails/console/app'
105
+ require 'rails/console/helpers'
106
+ Rails::ConsoleMethods.public_instance_methods.each(&block)
107
+ end
108
+ end
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+
3
+ class REPLTest < ActiveSupport::TestCase
4
+ test 'standalone adapter registration' do
5
+ WebConsole::REPL::register_adapter adapter = Class.new, standalone: true
6
+ assert_equal adapter, WebConsole::REPL::adapters[adapter]
7
+ end
8
+
9
+ test 'fallback for unsupported config.console' do
10
+ app_mock = Class.new do
11
+ define_singleton_method(:config) { OpenStruct.new(console: Class.new) }
12
+ end
13
+ assert_equal WebConsole::REPL::Dummy, WebConsole::REPL.default(app_mock)
14
+ end
15
+ end
@@ -0,0 +1,91 @@
1
+ require 'test_helper'
2
+
3
+ class WebConsoleTest < ActiveSupport::TestCase
4
+ test 'different default_mount_path' do
5
+ new_uninitialized_app do |app|
6
+ app.config.web_console.default_mount_path = '/shell'
7
+ app.initialize!
8
+
9
+ assert app.routes.named_routes['web_console'].path.match('/shell')
10
+ end
11
+ end
12
+
13
+ test 'whitelisted ips are courced to IPAddr' do
14
+ new_uninitialized_app do |app|
15
+ app.config.web_console.whitelisted_ips = '127.0.0.1'
16
+ app.initialize!
17
+
18
+ assert_equal [ IPAddr.new('127.0.0.1') ], app.config.web_console.whitelisted_ips
19
+ end
20
+ end
21
+
22
+ test 'whitelisted ips are normalized and unique IPAddr' do
23
+ new_uninitialized_app do |app|
24
+ app.config.web_console.whitelisted_ips = [ '127.0.0.1', '127.0.0.1', nil, '', ' ' ]
25
+ app.initialize!
26
+
27
+ assert_equal [ IPAddr.new('127.0.0.1') ], app.config.web_console.whitelisted_ips
28
+ end
29
+ end
30
+
31
+ test 'whitelisted_ips.include? coerces to IPAddr' do
32
+ new_uninitialized_app do |app|
33
+ app.config.web_console.whitelisted_ips = '127.0.0.1'
34
+ app.initialize!
35
+
36
+ assert app.config.web_console.whitelisted_ips.include?('127.0.0.1')
37
+ end
38
+ end
39
+
40
+ test 'whitelisted_ips.include? works with IPAddr' do
41
+ new_uninitialized_app do |app|
42
+ app.config.web_console.whitelisted_ips = '127.0.0.1'
43
+ app.initialize!
44
+
45
+ assert app.config.web_console.whitelisted_ips.include?(IPAddr.new('127.0.0.1'))
46
+ end
47
+ end
48
+
49
+ test 'whitelist whole networks' do
50
+ new_uninitialized_app do |app|
51
+ app.config.web_console.whitelisted_ips = '172.16.0.0/12'
52
+ app.initialize!
53
+
54
+ 1.upto(255).each do |n|
55
+ assert_includes app.config.web_console.whitelisted_ips, "172.16.0.#{n}"
56
+ end
57
+ end
58
+ end
59
+
60
+ test 'whitelist multiple networks' do
61
+ new_uninitialized_app do |app|
62
+ app.config.web_console.whitelisted_ips = %w( 172.16.0.0/12 192.168.0.0/16 )
63
+ app.initialize!
64
+
65
+ 1.upto(255).each do |n|
66
+ assert_includes app.config.web_console.whitelisted_ips, "172.16.0.#{n}"
67
+ assert_includes app.config.web_console.whitelisted_ips, "192.168.0.#{n}"
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def new_uninitialized_app(root = File.expand_path('../dummy', __FILE__))
75
+ FileUtils.mkdir_p root
76
+ Dir.chdir root
77
+
78
+ old_app = Rails.application
79
+ Rails.application = nil
80
+
81
+ app = Class.new(Rails::Application)
82
+ app.config.eager_load = false
83
+ app.config.time_zone = 'UTC'
84
+ app.config.middleware ||= Rails::Configuration::MiddlewareStackProxy.new
85
+ app.config.active_support.deprecation = :notify
86
+
87
+ yield app
88
+ ensure
89
+ Rails.application = old_app
90
+ end
91
+ end
@@ -0,0 +1,727 @@
1
+ // JQuery Console 1.0
2
+ // Sun Feb 21 20:28:47 GMT 2010
3
+ //
4
+ // Copyright 2010 Chris Done, Simon David Pratt. All rights reserved.
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions
8
+ // are met:
9
+ //
10
+ // 1. Redistributions of source code must retain the above
11
+ // copyright notice, this list of conditions and the following
12
+ // disclaimer.
13
+ //
14
+ // 2. Redistributions in binary form must reproduce the above
15
+ // copyright notice, this list of conditions and the following
16
+ // disclaimer in the documentation and/or other materials
17
+ // provided with the distribution.
18
+ //
19
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
22
+ // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
23
+ // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
24
+ // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
25
+ // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26
+ // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28
+ // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29
+ // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30
+ // POSSIBILITY OF SUCH DAMAGE.
31
+
32
+ // TESTED ON
33
+ // Internet Explorer 6
34
+ // Opera 10.01
35
+ // Chromium 4.0.237.0 (Ubuntu build 31094)
36
+ // Firefox 3.5.8, 3.6.2 (Mac)
37
+ // Safari 4.0.5 (6531.22.7) (Mac)
38
+ // Google Chrome 5.0.375.55 (Mac)
39
+
40
+ (function($){
41
+ var isWebkit = !!~navigator.userAgent.indexOf(' AppleWebKit/');
42
+
43
+ $.fn.console = function(config){
44
+ ////////////////////////////////////////////////////////////////////////
45
+ // Constants
46
+ // Some are enums, data types, others just for optimisation
47
+ var keyCodes = {
48
+ // left
49
+ 37: moveBackward,
50
+ // right
51
+ 39: moveForward,
52
+ // up
53
+ 38: previousHistory,
54
+ // down
55
+ 40: nextHistory,
56
+ // backspace
57
+ 8: backDelete,
58
+ // delete
59
+ 46: forwardDelete,
60
+ // end
61
+ 35: moveToEnd,
62
+ // start
63
+ 36: moveToStart,
64
+ // return
65
+ 13: commandTrigger,
66
+ // tab
67
+ 18: doNothing,
68
+ // tab
69
+ 9: doComplete
70
+ };
71
+ var ctrlCodes = {
72
+ // C-a
73
+ 65: moveToStart,
74
+ // C-e
75
+ 69: moveToEnd,
76
+ // C-d
77
+ 68: forwardDelete,
78
+ // C-n
79
+ 78: nextHistory,
80
+ // C-p
81
+ 80: previousHistory,
82
+ // C-b
83
+ 66: moveBackward,
84
+ // C-f
85
+ 70: moveForward,
86
+ // C-k
87
+ 75: deleteUntilEnd
88
+ };
89
+ if(config.ctrlCodes) {
90
+ $.extend(ctrlCodes, config.ctrlCodes);
91
+ }
92
+ var altCodes = {
93
+ // M-f
94
+ 70: moveToNextWord,
95
+ // M-b
96
+ 66: moveToPreviousWord,
97
+ // M-d
98
+ 68: deleteNextWord
99
+ };
100
+ var cursor = '<span class="jquery-console-cursor">&nbsp;</span>';
101
+
102
+ ////////////////////////////////////////////////////////////////////////
103
+ // Globals
104
+ var container = $(this);
105
+ var inner = $('<div class="jquery-console-inner"></div>');
106
+ // erjiang: changed this from a text input to a textarea so we
107
+ // can get pasted newlines
108
+ var typer = $('<textarea class="jquery-console-typer"></textarea>');
109
+ // Prompt
110
+ var promptBox;
111
+ var prompt;
112
+ var promptLabel = config && config.promptLabel? config.promptLabel : "> ";
113
+ var continuedPromptLabel = config && config.continuedPromptLabel?
114
+ config.continuedPromptLabel : "> ";
115
+ var column = 0;
116
+ var promptText = '';
117
+ var restoreText = '';
118
+ var continuedText = '';
119
+ // Prompt history stack
120
+ var history = [];
121
+ var ringn = 0;
122
+ // For reasons unknown to The Sword of Michael himself, Opera
123
+ // triggers and sends a key character when you hit various
124
+ // keys like PgUp, End, etc. So there is no way of knowing
125
+ // when a user has typed '#' or End. My solution is in the
126
+ // typer.keydown and typer.keypress functions; I use the
127
+ // variable below to ignore the keypress event if the keydown
128
+ // event succeeds.
129
+ var cancelKeyPress = 0;
130
+ // When this value is false, the prompt will not respond to input
131
+ var acceptInput = true;
132
+ // When this value is true, the command has been canceled
133
+ var cancelCommand = false;
134
+
135
+ // External exports object
136
+ var extern = {};
137
+
138
+ ////////////////////////////////////////////////////////////////////////
139
+ // Main entry point
140
+ (function(){
141
+ container.append(inner);
142
+ inner.append(typer);
143
+ typer.css({position:'absolute',top:0,left:'-9999px'});
144
+ if (config.welcomeMessage)
145
+ message(config.welcomeMessage,'jquery-console-welcome');
146
+ newPromptBox();
147
+ if (config.autofocus) {
148
+ inner.addClass('jquery-console-focus');
149
+ typer.focus();
150
+ setTimeout(function(){
151
+ inner.addClass('jquery-console-focus');
152
+ typer.focus();
153
+ },100);
154
+ }
155
+ extern.inner = inner;
156
+ extern.typer = typer;
157
+ extern.scrollToBottom = scrollToBottom;
158
+ })();
159
+
160
+ ////////////////////////////////////////////////////////////////////////
161
+ // Reset terminal
162
+ extern.reset = function(){
163
+ var welcome = (typeof config.welcomeMessage != 'undefined');
164
+ inner.parent().fadeOut(function(){
165
+ inner.find('div').each(function(){
166
+ if (!welcome) {
167
+ $(this).remove();
168
+ } else {
169
+ welcome = false;
170
+ }
171
+ });
172
+ newPromptBox();
173
+ inner.parent().fadeIn(function(){
174
+ inner.addClass('jquery-console-focus');
175
+ typer.focus();
176
+ });
177
+ });
178
+ };
179
+
180
+ ////////////////////////////////////////////////////////////////////////
181
+ // Reset terminal
182
+ extern.notice = function(msg,style){
183
+ var n = $('<div class="notice"></div>').append($('<div></div>').text(msg))
184
+ .css({visibility:'hidden'});
185
+ container.append(n);
186
+ var focused = true;
187
+ if (style=='fadeout')
188
+ setTimeout(function(){
189
+ n.fadeOut(function(){
190
+ n.remove();
191
+ });
192
+ },4000);
193
+ else if (style=='prompt') {
194
+ var a = $('<br/><div class="action"><a href="javascript:">OK</a><div class="clear"></div></div>');
195
+ n.append(a);
196
+ focused = false;
197
+ a.click(function(){ n.fadeOut(function(){ n.remove();inner.css({opacity:1}) }); });
198
+ }
199
+ var h = n.height();
200
+ n.css({height:'0px',visibility:'visible'})
201
+ .animate({height:h+'px'},function(){
202
+ if (!focused) inner.css({opacity:0.5});
203
+ });
204
+ n.css('cursor','default');
205
+ return n;
206
+ };
207
+
208
+ extern.promptLabel = function(text) {
209
+ if (typeof text === 'string') {
210
+ promptLabel = text;
211
+ }
212
+ };
213
+
214
+ ////////////////////////////////////////////////////////////////////////
215
+ // Make a new prompt box
216
+ function newPromptBox() {
217
+ column = 0;
218
+ promptText = '';
219
+ ringn = 0; // Reset the position of the history ring
220
+ enableInput();
221
+ promptBox = $('<div class="jquery-console-prompt-box"></div>');
222
+ var label = $('<span class="jquery-console-prompt-label"></span>');
223
+ var labelText = extern.continuedPrompt? continuedPromptLabel : promptLabel;
224
+ promptBox.append(label.text(labelText).show());
225
+ label.html(label.html().replace(' ','&nbsp;'));
226
+ prompt = $('<span class="jquery-console-prompt"></span>');
227
+ promptBox.append(prompt);
228
+ inner.append(promptBox);
229
+ updatePromptDisplay();
230
+ };
231
+
232
+ ////////////////////////////////////////////////////////////////////////
233
+ // Handle setting focus
234
+ container.click(function(){
235
+ inner.addClass('jquery-console-focus');
236
+ inner.removeClass('jquery-console-nofocus');
237
+ if (isWebkit) {
238
+ typer.focusWithoutScrolling();
239
+ } else {
240
+ typer.css('position', 'fixed').focus();
241
+ }
242
+ scrollToBottom();
243
+ return false;
244
+ });
245
+
246
+ ////////////////////////////////////////////////////////////////////////
247
+ // Handle losing focus
248
+ typer.blur(function(){
249
+ inner.removeClass('jquery-console-focus');
250
+ inner.addClass('jquery-console-nofocus');
251
+ });
252
+
253
+ ////////////////////////////////////////////////////////////////////////
254
+ // Bind to the paste event of the input box so we know when we
255
+ // get pasted data
256
+ typer.bind('paste', function(e) {
257
+ // wipe typer input clean just in case
258
+ typer.val("");
259
+ // this timeout is required because the onpaste event is
260
+ // fired *before* the text is actually pasted
261
+ setTimeout(function() {
262
+ typer.consoleInsert(typer.val());
263
+ typer.val("");
264
+ }, 0);
265
+ });
266
+
267
+ ////////////////////////////////////////////////////////////////////////
268
+ // Handle key hit before translation
269
+ // For picking up control characters like up/left/down/right
270
+
271
+ typer.keydown(function(e){
272
+ cancelKeyPress = 0;
273
+ var keyCode = e.keyCode;
274
+ // C-c: cancel the execution
275
+ if(e.ctrlKey && keyCode == 67) {
276
+ cancelKeyPress = keyCode;
277
+ cancelExecution();
278
+ return false;
279
+ }
280
+ if (acceptInput) {
281
+ if (keyCode in keyCodes) {
282
+ cancelKeyPress = keyCode;
283
+ (keyCodes[keyCode])();
284
+ return false;
285
+ } else if (e.ctrlKey && keyCode in ctrlCodes) {
286
+ cancelKeyPress = keyCode;
287
+ (ctrlCodes[keyCode])();
288
+ return false;
289
+ } else if (e.altKey && keyCode in altCodes) {
290
+ cancelKeyPress = keyCode;
291
+ (altCodes[keyCode])();
292
+ return false;
293
+ }
294
+ }
295
+ });
296
+
297
+ ////////////////////////////////////////////////////////////////////////
298
+ // Handle key press
299
+ typer.keypress(function(e){
300
+ var keyCode = e.keyCode || e.which;
301
+ if (isIgnorableKey(e)) {
302
+ return false;
303
+ }
304
+ // C-v: don't insert on paste event
305
+ if ((e.ctrlKey || e.metaKey) && String.fromCharCode(keyCode).toLowerCase() == 'v') {
306
+ return true;
307
+ }
308
+ if (acceptInput && cancelKeyPress != keyCode && keyCode >= 32){
309
+ if (cancelKeyPress) return false;
310
+ if (
311
+ typeof config.charInsertTrigger == 'undefined' || (
312
+ typeof config.charInsertTrigger == 'function' &&
313
+ config.charInsertTrigger(keyCode,promptText)
314
+ )
315
+ ){
316
+ typer.consoleInsert(keyCode);
317
+ }
318
+ }
319
+ if (isWebkit) return false;
320
+ });
321
+
322
+ function isIgnorableKey(e) {
323
+ // for now just filter alt+tab that we receive on some platforms when
324
+ // user switches windows (goes away from the browser)
325
+ return ((e.keyCode == keyCodes.tab || e.keyCode == 192) && e.altKey);
326
+ };
327
+
328
+ ////////////////////////////////////////////////////////////////////////
329
+ // Rotate through the command history
330
+ function rotateHistory(n){
331
+ if (history.length == 0) return;
332
+ ringn += n;
333
+ if (ringn < 0) ringn = history.length;
334
+ else if (ringn > history.length) ringn = 0;
335
+ var prevText = promptText;
336
+ if (ringn == 0) {
337
+ promptText = restoreText;
338
+ } else {
339
+ promptText = history[ringn - 1];
340
+ }
341
+ if (config.historyPreserveColumn) {
342
+ if (promptText.length < column + 1) {
343
+ column = promptText.length;
344
+ } else if (column == 0) {
345
+ column = promptText.length;
346
+ }
347
+ } else {
348
+ column = promptText.length;
349
+ }
350
+ updatePromptDisplay();
351
+ };
352
+
353
+ function previousHistory() {
354
+ rotateHistory(-1);
355
+ };
356
+
357
+ function nextHistory() {
358
+ rotateHistory(1);
359
+ };
360
+
361
+ // Add something to the history ring
362
+ function addToHistory(line){
363
+ history.push(line);
364
+ restoreText = '';
365
+ };
366
+
367
+ // Delete the character at the current position
368
+ function deleteCharAtPos(){
369
+ if (column < promptText.length){
370
+ promptText =
371
+ promptText.substring(0,column) +
372
+ promptText.substring(column+1);
373
+ restoreText = promptText;
374
+ return true;
375
+ } else return false;
376
+ };
377
+
378
+ function backDelete() {
379
+ if (moveColumn(-1)){
380
+ deleteCharAtPos();
381
+ updatePromptDisplay();
382
+ }
383
+ };
384
+
385
+ function forwardDelete() {
386
+ if (deleteCharAtPos()){
387
+ updatePromptDisplay();
388
+ }
389
+ };
390
+
391
+ function deleteUntilEnd() {
392
+ while(deleteCharAtPos()) {
393
+ updatePromptDisplay();
394
+ }
395
+ };
396
+
397
+ function deleteNextWord() {
398
+ // A word is defined within this context as a series of alphanumeric
399
+ // characters.
400
+ // Delete up to the next alphanumeric character
401
+ while(
402
+ column < promptText.length &&
403
+ !isCharAlphanumeric(promptText[column])
404
+ ) {
405
+ deleteCharAtPos();
406
+ updatePromptDisplay();
407
+ }
408
+ // Then, delete until the next non-alphanumeric character
409
+ while(
410
+ column < promptText.length &&
411
+ isCharAlphanumeric(promptText[column])
412
+ ) {
413
+ deleteCharAtPos();
414
+ updatePromptDisplay();
415
+ }
416
+ };
417
+
418
+ ////////////////////////////////////////////////////////////////////////
419
+ // Validate command and trigger it if valid, or show a validation error
420
+ function commandTrigger() {
421
+ var line = promptText;
422
+ if (typeof config.commandValidate == 'function') {
423
+ var ret = config.commandValidate(line);
424
+ if (ret == true || ret == false) {
425
+ if (ret) {
426
+ handleCommand();
427
+ }
428
+ } else {
429
+ commandResult(ret,"jquery-console-message-error");
430
+ }
431
+ } else {
432
+ handleCommand();
433
+ }
434
+ };
435
+
436
+ // Scroll to the bottom of the view
437
+ function scrollToBottom() {
438
+ var version = jQuery.fn.jquery.split('.');
439
+ var major = parseInt(version[0]);
440
+ var minor = parseInt(version[1]);
441
+
442
+ // check if we're using jquery > 1.6
443
+ if ((major == 1 && minor > 6) || major > 1) {
444
+ inner.prop({ scrollTop: inner.prop("scrollHeight") });
445
+ }
446
+ else {
447
+ inner.attr({ scrollTop: inner.attr("scrollHeight") });
448
+ }
449
+ };
450
+
451
+ function cancelExecution() {
452
+ if(typeof config.cancelHandle == 'function') {
453
+ config.cancelHandle();
454
+ }
455
+ }
456
+
457
+ ////////////////////////////////////////////////////////////////////////
458
+ // Handle a command
459
+ function handleCommand() {
460
+ if (typeof config.commandHandle == 'function') {
461
+ disableInput();
462
+ addToHistory(promptText);
463
+ var text = promptText;
464
+ if (extern.continuedPrompt) {
465
+ if (continuedText)
466
+ continuedText += '\n' + promptText;
467
+ else continuedText = promptText;
468
+ } else continuedText = undefined;
469
+ if (continuedText) text = continuedText;
470
+ var ret = config.commandHandle(text,function(msgs){
471
+ commandResult(msgs);
472
+ });
473
+ if (extern.continuedPrompt && !continuedText)
474
+ continuedText = promptText;
475
+ if (typeof ret == 'boolean') {
476
+ if (ret) {
477
+ // Command succeeded without a result.
478
+ commandResult();
479
+ } else {
480
+ commandResult(
481
+ 'Command failed.',
482
+ "jquery-console-message-error"
483
+ );
484
+ }
485
+ } else if (typeof ret == "string") {
486
+ commandResult(ret,"jquery-console-message-success");
487
+ } else if (typeof ret == 'object' && ret.length) {
488
+ commandResult(ret);
489
+ } else if (extern.continuedPrompt) {
490
+ commandResult();
491
+ }
492
+ }
493
+ };
494
+
495
+ ////////////////////////////////////////////////////////////////////////
496
+ // Disable input
497
+ function disableInput() {
498
+ acceptInput = false;
499
+ };
500
+
501
+ // Enable input
502
+ function enableInput() {
503
+ acceptInput = true;
504
+ }
505
+
506
+ ////////////////////////////////////////////////////////////////////////
507
+ // Reset the prompt in invalid command
508
+ function commandResult(msg,className) {
509
+ column = -1;
510
+ updatePromptDisplay();
511
+ if (typeof msg == 'string') {
512
+ message(msg,className);
513
+ } else if ($.isArray(msg)) {
514
+ for (var x in msg) {
515
+ var ret = msg[x];
516
+ message(ret.msg,ret.className);
517
+ }
518
+ } else { // Assume it's a DOM node or jQuery object.
519
+ inner.append(msg);
520
+ }
521
+ newPromptBox();
522
+ };
523
+
524
+ ////////////////////////////////////////////////////////////////////////
525
+ // Display a message
526
+ function message(msg,className) {
527
+ var mesg = $('<div class="jquery-console-message"></div>');
528
+ if (className) mesg.addClass(className);
529
+ mesg.filledText(msg).hide();
530
+ inner.append(mesg);
531
+ mesg.show();
532
+ };
533
+
534
+ ////////////////////////////////////////////////////////////////////////
535
+ // Handle normal character insertion
536
+ // data can either be a number, which will be interpreted as the
537
+ // numeric value of a single character, or a string
538
+ typer.consoleInsert = function(data){
539
+ // TODO: remove redundant indirection
540
+ var text = isNaN(data) ? data : String.fromCharCode(data);
541
+ var before = promptText.substring(0,column);
542
+ var after = promptText.substring(column);
543
+ promptText = before + text + after;
544
+ moveColumn(text.length);
545
+ restoreText = promptText;
546
+ updatePromptDisplay();
547
+ };
548
+
549
+ ////////////////////////////////////////////////////////////////////////
550
+ // Move to another column relative to this one
551
+ // Negative means go back, positive means go forward.
552
+ function moveColumn(n){
553
+ if (column + n >= 0 && column + n <= promptText.length){
554
+ column += n;
555
+ return true;
556
+ } else return false;
557
+ };
558
+
559
+ function moveForward() {
560
+ if(moveColumn(1)) {
561
+ updatePromptDisplay();
562
+ return true;
563
+ }
564
+ return false;
565
+ };
566
+
567
+ function moveBackward() {
568
+ if(moveColumn(-1)) {
569
+ updatePromptDisplay();
570
+ return true;
571
+ }
572
+ return false;
573
+ };
574
+
575
+ function moveToStart() {
576
+ if (moveColumn(-column))
577
+ updatePromptDisplay();
578
+ };
579
+
580
+ function moveToEnd() {
581
+ if (moveColumn(promptText.length-column))
582
+ updatePromptDisplay();
583
+ };
584
+
585
+ function moveToNextWord() {
586
+ while(
587
+ column < promptText.length &&
588
+ !isCharAlphanumeric(promptText[column]) &&
589
+ moveForward()
590
+ ) {}
591
+ while(
592
+ column < promptText.length &&
593
+ isCharAlphanumeric(promptText[column]) &&
594
+ moveForward()
595
+ ) {}
596
+ };
597
+
598
+ function moveToPreviousWord() {
599
+ // Move backward until we find the first alphanumeric
600
+ while(
601
+ column -1 >= 0 &&
602
+ !isCharAlphanumeric(promptText[column-1]) &&
603
+ moveBackward()
604
+ ) {}
605
+ // Move until we find the first non-alphanumeric
606
+ while(
607
+ column -1 >= 0 &&
608
+ isCharAlphanumeric(promptText[column-1]) &&
609
+ moveBackward()
610
+ ) {}
611
+ };
612
+
613
+ function isCharAlphanumeric(charToTest) {
614
+ if(typeof charToTest == 'string') {
615
+ var code = charToTest.charCodeAt();
616
+ return (code >= 'A'.charCodeAt() && code <= 'Z'.charCodeAt()) ||
617
+ (code >= 'a'.charCodeAt() && code <= 'z'.charCodeAt()) ||
618
+ (code >= '0'.charCodeAt() && code <= '9'.charCodeAt());
619
+ }
620
+ return false;
621
+ };
622
+
623
+ function doComplete() {
624
+ if(typeof config.completeHandle == 'function') {
625
+ var completions = config.completeHandle(promptText);
626
+ var len = completions.length;
627
+ if (len === 1) {
628
+ extern.promptText(promptText + completions[0]);
629
+ } else if (len > 1 && config.cols) {
630
+ var prompt = promptText;
631
+ // Compute the number of rows that will fit in the width
632
+ var max = 0;
633
+ for (var i = 0;i < len;i++) {
634
+ max = Math.max(max, completions[i].length);
635
+ }
636
+ max += 2;
637
+ var n = Math.floor(config.cols / max);
638
+ var buffer = "";
639
+ var col = 0;
640
+ for (i = 0;i < len;i++) {
641
+ var completion = completions[i];
642
+ buffer += completions[i];
643
+ for (var j = completion.length;j < max;j++) {
644
+ buffer += " ";
645
+ }
646
+ if (++col >= n) {
647
+ buffer += "\n";
648
+ col = 0;
649
+ }
650
+ }
651
+ commandResult(buffer,"jquery-console-message-value");
652
+ extern.promptText(prompt);
653
+ }
654
+ }
655
+ };
656
+
657
+ function doNothing() {};
658
+
659
+ extern.promptText = function(text){
660
+ if (typeof text === 'string') {
661
+ promptText = text;
662
+ column = promptText.length;
663
+ updatePromptDisplay();
664
+ }
665
+ return promptText;
666
+ };
667
+
668
+ ////////////////////////////////////////////////////////////////////////
669
+ // Update the prompt display
670
+ function updatePromptDisplay(){
671
+ var line = promptText;
672
+ var html = '';
673
+ if (column > 0 && line == ''){
674
+ // When we have an empty line just display a cursor.
675
+ html = cursor;
676
+ } else if (column == promptText.length){
677
+ // We're at the end of the line, so we need to display
678
+ // the text *and* cursor.
679
+ html = htmlEncode(line) + cursor;
680
+ } else {
681
+ // Grab the current character, if there is one, and
682
+ // make it the current cursor.
683
+ var before = line.substring(0, column);
684
+ var current = line.substring(column,column+1);
685
+ if (current){
686
+ current =
687
+ '<span class="jquery-console-cursor">' +
688
+ htmlEncode(current) +
689
+ '</span>';
690
+ }
691
+ var after = line.substring(column+1);
692
+ html = htmlEncode(before) + current + htmlEncode(after);
693
+ }
694
+ prompt.html(html);
695
+ scrollToBottom();
696
+ };
697
+
698
+ // Simple HTML encoding
699
+ // Simply replace '<', '>' and '&'
700
+ // TODO: Use jQuery's .html() trick, or grab a proper, fast
701
+ // HTML encoder.
702
+ function htmlEncode(text){
703
+ return (
704
+ text.replace(/&/g,'&amp;')
705
+ .replace(/</g,'&lt;')
706
+ .replace(/</g,'&lt;')
707
+ .replace(/ /g,'&nbsp;')
708
+ .replace(/\n/g,'<br />')
709
+ );
710
+ };
711
+
712
+ return extern;
713
+ };
714
+ // Simple utility for printing messages
715
+ $.fn.filledText = function(txt){
716
+ $(this).text(txt);
717
+ $(this).html($(this).html().replace(/\n/g,'<br/>'));
718
+ return this;
719
+ };
720
+
721
+ // Alternative method for focus without scrolling
722
+ $.fn.focusWithoutScrolling = function(){
723
+ var x = window.scrollX, y = window.scrollY;
724
+ $(this).focus();
725
+ window.scrollTo(x, y);
726
+ };
727
+ })(jQuery);