jschat 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +23 -0
- data/README.textile +71 -0
- data/bin/jschat-client +3 -0
- data/bin/jschat-server +18 -0
- data/bin/jschat-web +7 -0
- data/lib/jschat/client.rb +745 -0
- data/lib/jschat/errors.rb +41 -0
- data/lib/jschat/flood_protection.rb +39 -0
- data/lib/jschat/http/config/sprockets.yml +7 -0
- data/lib/jschat/http/config.ru +12 -0
- data/lib/jschat/http/jschat.rb +264 -0
- data/lib/jschat/http/public/favicon.ico +0 -0
- data/lib/jschat/http/public/images/emoticons/angry.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/arr.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/blink.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/blush.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/brucelee.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/btw.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/chuckle.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/clap.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/cool.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/drool.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/drunk.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/dry.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/eek.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/flex.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/happy.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/holmes.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/huh.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/laugh.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/lol.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/mad.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/mellow.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/noclue.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/oh.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/ohmy.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/ph34r.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/pimp.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/punch.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/realmad.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/rock.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/rofl.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/rolleyes.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/sad.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/scratch.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/shifty.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/shock.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/shrug.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/sleep.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/sleeping.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/smile.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/suicide.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/sweat.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/thumbs.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/tongue.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/unsure.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/w00t.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/wacko.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/whistling.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/wink.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/worship.gif +0 -0
- data/lib/jschat/http/public/images/emoticons/yucky.gif +0 -0
- data/lib/jschat/http/public/images/jschat.gif +0 -0
- data/lib/jschat/http/public/images/shadow.png +0 -0
- data/lib/jschat/http/public/javascripts/app/controllers/chat_controller.js +191 -0
- data/lib/jschat/http/public/javascripts/app/controllers/signon_controller.js +56 -0
- data/lib/jschat/http/public/javascripts/app/helpers/emote_helper.js +23 -0
- data/lib/jschat/http/public/javascripts/app/helpers/form_helpers.js +37 -0
- data/lib/jschat/http/public/javascripts/app/helpers/link_helper.js +47 -0
- data/lib/jschat/http/public/javascripts/app/helpers/page_helper.js +27 -0
- data/lib/jschat/http/public/javascripts/app/helpers/text_helper.js +92 -0
- data/lib/jschat/http/public/javascripts/app/lib/split.js +78 -0
- data/lib/jschat/http/public/javascripts/app/models/cookie.js +27 -0
- data/lib/jschat/http/public/javascripts/app/protocol/change.js +15 -0
- data/lib/jschat/http/public/javascripts/app/protocol/chat_request.js +13 -0
- data/lib/jschat/http/public/javascripts/app/protocol/display.js +147 -0
- data/lib/jschat/http/public/javascripts/app/ui/commands.js +55 -0
- data/lib/jschat/http/public/javascripts/app/ui/tab_completion.js +122 -0
- data/lib/jschat/http/public/javascripts/init.js +19 -0
- data/lib/jschat/http/public/stylesheets/iphone.css +3 -0
- data/lib/jschat/http/public/stylesheets/screen.css +68 -0
- data/lib/jschat/http/script/sprockets.rb +14 -0
- data/lib/jschat/http/tmp/restart.txt +0 -0
- data/lib/jschat/http/views/index.erb +23 -0
- data/lib/jschat/http/views/iphone.erb +29 -0
- data/lib/jschat/http/views/layout.erb +29 -0
- data/lib/jschat/http/views/message_form.erb +15 -0
- data/lib/jschat/server.rb +503 -0
- data/test/server_test.rb +175 -0
- data/test/stateless_test.rb +33 -0
- data/test/test_helper.rb +61 -0
- metadata +223 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'eventmachine'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'ncurses'
|
|
5
|
+
require 'optparse'
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
options = {}
|
|
9
|
+
|
|
10
|
+
ARGV.clone.options do |opts|
|
|
11
|
+
script_name = File.basename($0)
|
|
12
|
+
opts.banner = "Usage: #{$0} [options]"
|
|
13
|
+
|
|
14
|
+
opts.separator ""
|
|
15
|
+
|
|
16
|
+
opts.on("-h", "--hostname=host", String, "JsChat server hostname") { |o| options['hostname'] = o }
|
|
17
|
+
opts.on("-p", "--port=port", String, "JsChat server port number") { |o| options['port'] = o }
|
|
18
|
+
opts.on("-r", "--room=#room", String, "Channel to auto-join: remember to escape the hash") { |o| options['room'] = o }
|
|
19
|
+
opts.on("-n", "--nick=name", String, "Your name") { |o| options['nick'] = o }
|
|
20
|
+
opts.on("--help", "-H", "This text") { puts opts; exit 0 }
|
|
21
|
+
|
|
22
|
+
opts.parse!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ClientConfig = {
|
|
26
|
+
:port => options['port'] || '6789',
|
|
27
|
+
:ip => options['hostname'] || '0.0.0.0',
|
|
28
|
+
:name => options['nick'] || ENV['LOGNAME'],
|
|
29
|
+
:auto_join => options['room'] || '#jschat'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module Ncurses
|
|
33
|
+
KEY_DELETE = ?\C-h
|
|
34
|
+
KEY_TAB = 9
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module JsChat
|
|
38
|
+
class Protocol
|
|
39
|
+
class Response
|
|
40
|
+
attr_reader :message, :time
|
|
41
|
+
def initialize(message, time)
|
|
42
|
+
@message, @time = message, time
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(connection)
|
|
47
|
+
@connection = connection
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def legal_commands
|
|
51
|
+
%w(messages message joined quit error join names part identified part_notice quit_notice join_notice)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def legal_change_commands
|
|
55
|
+
%w(user)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def legal?(command)
|
|
59
|
+
legal_commands.include? command
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def legal_change?(command)
|
|
63
|
+
legal_change_commands.include? command
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def identified? ; @identified ; end
|
|
67
|
+
|
|
68
|
+
def change_user(json)
|
|
69
|
+
original_name = json['name'].keys().first
|
|
70
|
+
new_name = json['name'].values.first
|
|
71
|
+
@connection.names.delete original_name
|
|
72
|
+
@connection.names << new_name
|
|
73
|
+
"* User #{original_name} is now known as #{new_name}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def message(json)
|
|
77
|
+
if json['room']
|
|
78
|
+
"[#{json['room']}] <#{json['user']}> #{json['message']}"
|
|
79
|
+
else
|
|
80
|
+
"PRIVATE <#{json['user']}> #{json['message']}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def messages(messages)
|
|
85
|
+
results = []
|
|
86
|
+
messages.each do |json|
|
|
87
|
+
results << process_message(json)
|
|
88
|
+
end
|
|
89
|
+
results
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def get_command(json)
|
|
93
|
+
if json.has_key? 'display' and legal? json['display']
|
|
94
|
+
'display'
|
|
95
|
+
elsif json.has_key? 'change' and legal_change? json['change']
|
|
96
|
+
'change'
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def get_time(json)
|
|
101
|
+
if json.kind_of? Hash and json.has_key? 'time'
|
|
102
|
+
Time.parse(json['time']).getlocal
|
|
103
|
+
else
|
|
104
|
+
Time.now.localtime
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def protocol_method_name(command, method_name)
|
|
109
|
+
if command == 'display'
|
|
110
|
+
method_name
|
|
111
|
+
elsif command == 'change'
|
|
112
|
+
"change_#{method_name}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def process_message(json)
|
|
117
|
+
command = get_command json
|
|
118
|
+
|
|
119
|
+
if command
|
|
120
|
+
time = get_time json[json[command]]
|
|
121
|
+
response = send(protocol_method_name(command, json[command]), json[json[command]])
|
|
122
|
+
case response
|
|
123
|
+
when Array
|
|
124
|
+
response
|
|
125
|
+
when String
|
|
126
|
+
Response.new(response, time)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def join(json)
|
|
132
|
+
@connection.send_names json['room']
|
|
133
|
+
"* User #{json['user']} joined #{json['room']}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def join_notice(json)
|
|
137
|
+
@connection.names << json['user']
|
|
138
|
+
"* User #{json['user']} joined #{json['room']}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def part(json)
|
|
142
|
+
"* You left #{json['room']}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def part_notice(json)
|
|
146
|
+
@connection.names.delete json['user']
|
|
147
|
+
"* #{json['user']} left #{json['room']}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def quit(json)
|
|
151
|
+
@connection.names.delete json['user']
|
|
152
|
+
"* User #{json['user']} left #{json['room']}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def names(json)
|
|
156
|
+
@connection.names = json.collect { |u| u['name'] }
|
|
157
|
+
"* In this room: #{@connection.names.join(', ')}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def identified(json)
|
|
161
|
+
if ClientConfig[:auto_join] and !@auto_joined
|
|
162
|
+
@connection.send_join ClientConfig[:auto_join]
|
|
163
|
+
@auto_joined = true
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
@identified = true
|
|
167
|
+
|
|
168
|
+
"* You are now known as #{json['name']}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def error(json)
|
|
172
|
+
"* [ERROR] #{json['message']}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
alias_method :quit_notice, :quit
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
module JsClient
|
|
180
|
+
class TabComplete
|
|
181
|
+
attr_accessor :matched, :index
|
|
182
|
+
|
|
183
|
+
def initialize
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def run(names, field, form, cursor_x)
|
|
187
|
+
form.form_driver Ncurses::Form::REQ_BEG_LINE
|
|
188
|
+
text = field.field_buffer(0).dup.strip
|
|
189
|
+
if text.size > 0 and cursor_x > 0
|
|
190
|
+
if @matched.nil?
|
|
191
|
+
source = text.slice(0, cursor_x)
|
|
192
|
+
source = text.split(' ').last if text.match(' ')
|
|
193
|
+
@index = 0
|
|
194
|
+
else
|
|
195
|
+
source = @matched
|
|
196
|
+
@index += 1
|
|
197
|
+
end
|
|
198
|
+
names = names.sort.find_all { |name| name.match /^#{source}/i }
|
|
199
|
+
@index = 0 if @index >= names.size
|
|
200
|
+
name = names[@index]
|
|
201
|
+
if name and (@matched or source.size == text.size)
|
|
202
|
+
@matched = source
|
|
203
|
+
field.set_field_buffer(0, "#{name}: ")
|
|
204
|
+
form.form_driver Ncurses::Form::REQ_END_LINE
|
|
205
|
+
form.form_driver Ncurses::Form::REQ_NEXT_CHAR
|
|
206
|
+
else
|
|
207
|
+
@matched = nil
|
|
208
|
+
@index = 0
|
|
209
|
+
(0..cursor_x - 1).each do
|
|
210
|
+
form.form_driver Ncurses::Form::REQ_NEXT_CHAR
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def match(input)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def reset
|
|
220
|
+
@matched = nil
|
|
221
|
+
@index = 0
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def keyboard=(keyboard)
|
|
226
|
+
@keyboard = keyboard
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# This should take room into account
|
|
230
|
+
def names=(names)
|
|
231
|
+
@names = names
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def names
|
|
235
|
+
@names
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
module KeyboardInput
|
|
239
|
+
def setup_screen
|
|
240
|
+
Ncurses.initscr
|
|
241
|
+
@windows = {}
|
|
242
|
+
|
|
243
|
+
Ncurses.raw
|
|
244
|
+
Ncurses.start_color
|
|
245
|
+
Ncurses.noecho
|
|
246
|
+
Ncurses.use_default_colors
|
|
247
|
+
Ncurses.init_pair 2, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLUE
|
|
248
|
+
Ncurses.init_pair 3, Ncurses::COLOR_CYAN, -1
|
|
249
|
+
Ncurses.init_pair 4, Ncurses::COLOR_YELLOW, -1
|
|
250
|
+
Ncurses.init_pair 5, Ncurses::COLOR_BLACK, -1
|
|
251
|
+
Ncurses.init_pair 6, Ncurses::COLOR_RED, -1
|
|
252
|
+
|
|
253
|
+
@history_position = 0
|
|
254
|
+
@history = []
|
|
255
|
+
@lastlog = []
|
|
256
|
+
@room_name = ''
|
|
257
|
+
|
|
258
|
+
setup_windows
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def room_name=(room_name)
|
|
262
|
+
@room_name = room_name
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def setup_windows
|
|
266
|
+
Ncurses.refresh
|
|
267
|
+
|
|
268
|
+
display_windows
|
|
269
|
+
|
|
270
|
+
Thread.new do
|
|
271
|
+
loop do
|
|
272
|
+
display_time
|
|
273
|
+
sleep 60 - Time.now.sec
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
Signal.trap('SIGWINCH') do
|
|
278
|
+
resize
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def remove_windows_and_forms
|
|
283
|
+
remove_windows
|
|
284
|
+
remove_forms
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def remove_windows
|
|
288
|
+
@windows.each do |name, window|
|
|
289
|
+
window.delete
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def remove_forms
|
|
294
|
+
@input_field.free_field
|
|
295
|
+
@input_form.free_form
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def update_windows
|
|
299
|
+
rows, cols = get_window_size
|
|
300
|
+
remove_windows_and_forms
|
|
301
|
+
display_windows
|
|
302
|
+
display_room_name
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def display_windows
|
|
306
|
+
rows, cols = get_window_size
|
|
307
|
+
@windows[:text] = Ncurses.newwin(rows - 1, cols, 0, 0)
|
|
308
|
+
@windows[:info] = Ncurses.newwin(rows - 1, cols, rows - 2, 0)
|
|
309
|
+
@windows[:input] = Ncurses.newwin(rows, cols, rows - 1, 0)
|
|
310
|
+
@windows[:text].scrollok(true)
|
|
311
|
+
@windows[:info].bkgd Ncurses.COLOR_PAIR(2)
|
|
312
|
+
@windows[:input].keypad(true)
|
|
313
|
+
@windows[:input].nodelay(true)
|
|
314
|
+
|
|
315
|
+
@windows[:text].refresh
|
|
316
|
+
@windows[:info].refresh
|
|
317
|
+
@windows[:input].refresh
|
|
318
|
+
display_input
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def display_input
|
|
322
|
+
offset = @room_name.size > 0 ? @room_name.size + 3 : 0
|
|
323
|
+
@input_field = Ncurses::Form::FIELD.new(1, Ncurses.COLS - offset, 0, offset, 0, 0)
|
|
324
|
+
Ncurses::Form.field_opts_off(@input_field, Ncurses::Form::O_AUTOSKIP)
|
|
325
|
+
Ncurses::Form.field_opts_off(@input_field, Ncurses::Form::O_STATIC)
|
|
326
|
+
@input_form = Ncurses::Form::FORM.new([@input_field])
|
|
327
|
+
@input_form.set_form_win @windows[:input]
|
|
328
|
+
@input_form.post_form
|
|
329
|
+
@input_field.set_field_buffer 0, ''
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def display_room_name
|
|
333
|
+
if @room_name
|
|
334
|
+
display_input
|
|
335
|
+
@windows[:input].mvprintw(0, 0, "[#{@room_name}] ")
|
|
336
|
+
@windows[:input].refresh
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def display_time
|
|
341
|
+
@windows[:info].move 0, 0
|
|
342
|
+
@windows[:info].addstr "[#{Time.now.strftime('%H:%M')}]\n"
|
|
343
|
+
@windows[:info].refresh
|
|
344
|
+
@windows[:input].refresh
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def resize
|
|
348
|
+
# Save the user input
|
|
349
|
+
@input_form.form_driver Ncurses::Form::REQ_BEG_LINE
|
|
350
|
+
input = @input_field.field_buffer(0)
|
|
351
|
+
input.rstrip!
|
|
352
|
+
|
|
353
|
+
Ncurses.def_prog_mode
|
|
354
|
+
Ncurses.endwin
|
|
355
|
+
Ncurses.reset_prog_mode
|
|
356
|
+
|
|
357
|
+
update_windows
|
|
358
|
+
|
|
359
|
+
cols = get_window_size[0]
|
|
360
|
+
if @lastlog.size > 0
|
|
361
|
+
lastlog_start = cols > @lastlog.size ? @lastlog.size : cols
|
|
362
|
+
@lastlog[-lastlog_start..-1].each do |message|
|
|
363
|
+
display_text message[0], message[1]
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
@input_field.set_field_buffer(0, input)
|
|
368
|
+
@windows[:input].addstr input
|
|
369
|
+
@input_form.form_driver Ncurses::Form::REQ_END_LINE
|
|
370
|
+
|
|
371
|
+
Ncurses.refresh
|
|
372
|
+
rescue Exception => exception
|
|
373
|
+
puts exception
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# FIXME: This doesn't work after resize
|
|
377
|
+
# I've tried other ruby ncurses programs and they don't either
|
|
378
|
+
def get_window_size
|
|
379
|
+
Ncurses.refresh
|
|
380
|
+
cols, rows = [], []
|
|
381
|
+
Ncurses.stdscr.getmaxyx rows, cols
|
|
382
|
+
[rows.first, cols.first]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def get_history_text(offset = 1)
|
|
386
|
+
offset_position = @history_position + offset
|
|
387
|
+
if offset_position >= 0 and offset_position < @history.size
|
|
388
|
+
@history_position = offset_position
|
|
389
|
+
@history[@history_position]
|
|
390
|
+
else
|
|
391
|
+
if @history_position > -1 and @history_position < @history.size
|
|
392
|
+
@history_position = offset_position
|
|
393
|
+
end
|
|
394
|
+
''
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def arrow_keys(data)
|
|
399
|
+
c = data[0]
|
|
400
|
+
if @sequence
|
|
401
|
+
@sequence << c
|
|
402
|
+
history_text = ''
|
|
403
|
+
|
|
404
|
+
if data == 'A'
|
|
405
|
+
@sequence = nil
|
|
406
|
+
history_text = get_history_text(-1)
|
|
407
|
+
elsif data == 'B'
|
|
408
|
+
@sequence = nil
|
|
409
|
+
history_text = get_history_text
|
|
410
|
+
elsif data == 'O'
|
|
411
|
+
return true
|
|
412
|
+
elsif data == 'D'
|
|
413
|
+
# left
|
|
414
|
+
@sequence = nil
|
|
415
|
+
@input_form.form_driver Ncurses::Form::REQ_PREV_CHAR
|
|
416
|
+
@windows[:input].refresh
|
|
417
|
+
return true
|
|
418
|
+
elsif data == 'C'
|
|
419
|
+
# right
|
|
420
|
+
@input_form.form_driver Ncurses::Form::REQ_NEXT_CHAR
|
|
421
|
+
@windows[:input].refresh
|
|
422
|
+
@sequence = nil
|
|
423
|
+
return true
|
|
424
|
+
else
|
|
425
|
+
@sequence = nil
|
|
426
|
+
return true
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
begin
|
|
430
|
+
@input_form.form_driver Ncurses::Form::REQ_CLR_FIELD
|
|
431
|
+
@input_field.set_field_buffer(0, history_text)
|
|
432
|
+
@windows[:input].addstr history_text
|
|
433
|
+
@windows[:input].refresh
|
|
434
|
+
@input_form.form_driver Ncurses::Form::REQ_END_LINE
|
|
435
|
+
rescue Exception => exception
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
return true
|
|
439
|
+
elsif c == 27
|
|
440
|
+
@sequence = c
|
|
441
|
+
return true
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def cursor_position
|
|
446
|
+
cursor_position = Ncurses.getcurx(@windows[:input]) - 10
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def receive_data(data)
|
|
450
|
+
@clipboard ||= ''
|
|
451
|
+
c = data[0]
|
|
452
|
+
|
|
453
|
+
if arrow_keys data
|
|
454
|
+
return
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
if c != Ncurses::KEY_TAB
|
|
458
|
+
@tab_completion.reset
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
case c
|
|
462
|
+
when -1
|
|
463
|
+
# Return
|
|
464
|
+
when Ncurses::KEY_TAB
|
|
465
|
+
@tab_completion.run @connection.names, @input_field, @input_form, cursor_position
|
|
466
|
+
when Ncurses::KEY_ENTER, ?\n, ?\r
|
|
467
|
+
@input_form.form_driver Ncurses::Form::REQ_BEG_LINE
|
|
468
|
+
line = @input_field.field_buffer(0)
|
|
469
|
+
line.rstrip!
|
|
470
|
+
|
|
471
|
+
if !line.empty? and line.length > 0
|
|
472
|
+
@history << line.dup
|
|
473
|
+
@history_position = @history.size
|
|
474
|
+
manage_commands line
|
|
475
|
+
end
|
|
476
|
+
@input_form.form_driver Ncurses::Form::REQ_CLR_FIELD
|
|
477
|
+
when ?\C-l
|
|
478
|
+
# Refresh
|
|
479
|
+
resize
|
|
480
|
+
when Ncurses::KEY_BACKSPACE, ?\C-h, 127
|
|
481
|
+
# Backspace
|
|
482
|
+
@input_form.form_driver Ncurses::Form::REQ_DEL_PREV
|
|
483
|
+
@input_form.form_driver Ncurses::Form::REQ_CLR_EOL
|
|
484
|
+
when ?\C-d
|
|
485
|
+
@input_form.form_driver Ncurses::Form::REQ_DEL_CHAR
|
|
486
|
+
when Ncurses::KEY_LEFT, ?\C-b
|
|
487
|
+
@input_form.form_driver Ncurses::Form::REQ_PREV_CHAR
|
|
488
|
+
when Ncurses::KEY_RIGHT, ?\C-f
|
|
489
|
+
@input_form.form_driver Ncurses::Form::REQ_NEXT_CHAR
|
|
490
|
+
when ?\C-a
|
|
491
|
+
@input_form.form_driver Ncurses::Form::REQ_BEG_LINE
|
|
492
|
+
when ?\C-e
|
|
493
|
+
@input_form.form_driver Ncurses::Form::REQ_END_LINE
|
|
494
|
+
when ?\C-k
|
|
495
|
+
@input_form.form_driver Ncurses::Form::REQ_CLR_EOL
|
|
496
|
+
when ?\C-u
|
|
497
|
+
@input_form.form_driver Ncurses::Form::REQ_BEG_LINE
|
|
498
|
+
@clipboard = @input_field.field_buffer(0)
|
|
499
|
+
@input_form.form_driver Ncurses::Form::REQ_CLR_FIELD
|
|
500
|
+
when ?\C-y
|
|
501
|
+
unless @clipboard.empty?
|
|
502
|
+
cursor_position = Ncurses.getcurx(@windows[:input])
|
|
503
|
+
|
|
504
|
+
text = @input_field.field_buffer(0).insert(cursor_position - 9, @clipboard)
|
|
505
|
+
@input_field.set_field_buffer(0, text)
|
|
506
|
+
|
|
507
|
+
@windows[:text].addstr "#{cursor_position}\n #{text}"
|
|
508
|
+
@windows[:text].refresh
|
|
509
|
+
end
|
|
510
|
+
when ?\C-c
|
|
511
|
+
quit
|
|
512
|
+
when ?\C-w
|
|
513
|
+
@input_form.form_driver Ncurses::Form::REQ_PREV_CHAR
|
|
514
|
+
@input_form.form_driver Ncurses::Form::REQ_DEL_WORD
|
|
515
|
+
else
|
|
516
|
+
@input_form.form_driver c
|
|
517
|
+
end
|
|
518
|
+
@windows[:input].refresh
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def show_message(messages, time = Time.now.localtime)
|
|
522
|
+
messages.split("\n").each do |message|
|
|
523
|
+
@lastlog << [message.dup, time]
|
|
524
|
+
@lastlog.shift if @lastlog.size > 250
|
|
525
|
+
display_text message, time
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def display_text(message, time = Time.now.localtime)
|
|
530
|
+
message = message.dup
|
|
531
|
+
@windows[:text].addstr "#{time.strftime('%H:%M')} "
|
|
532
|
+
|
|
533
|
+
if message.match /^\*/
|
|
534
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(3))
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
if message.match /^\* \[ERROR\]/
|
|
538
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(6))
|
|
539
|
+
elsif message.match /^\[/
|
|
540
|
+
channel_info = message.split(']').first.sub /\[/, ''
|
|
541
|
+
message.sub! "[#{channel_info}]", ''
|
|
542
|
+
|
|
543
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(5))
|
|
544
|
+
@windows[:text].addstr "["
|
|
545
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(4))
|
|
546
|
+
@windows[:text].addstr "#{channel_info}"
|
|
547
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(5))
|
|
548
|
+
@windows[:text].addstr "] "
|
|
549
|
+
|
|
550
|
+
name = message.split('>').first.sub(/</, '').strip
|
|
551
|
+
message.sub!("<#{name}> ", '')
|
|
552
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(5))
|
|
553
|
+
@windows[:text].addstr '<'
|
|
554
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(0))
|
|
555
|
+
@windows[:text].addstr "#{name}"
|
|
556
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(5))
|
|
557
|
+
@windows[:text].addstr '>'
|
|
558
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(0))
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
@windows[:text].addstr "#{message}\n"
|
|
562
|
+
@windows[:text].refresh
|
|
563
|
+
@windows[:input].refresh
|
|
564
|
+
|
|
565
|
+
display_time
|
|
566
|
+
|
|
567
|
+
if message.match /^\*/
|
|
568
|
+
@windows[:text].attrset(Ncurses.COLOR_PAIR(0))
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def quit
|
|
573
|
+
Ncurses.endwin
|
|
574
|
+
exit
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def help_text
|
|
578
|
+
<<-TEXT
|
|
579
|
+
*** JsChat Help ***
|
|
580
|
+
Commands start with a forward slash. Parameters in square brackets are optional.
|
|
581
|
+
/name new_name - Change your name. Alias: /nick
|
|
582
|
+
/names [room name] - List the people in a room.
|
|
583
|
+
/join #room - Join a room. Alias: /j
|
|
584
|
+
/switch #room - Speak in a different room. Alias: /s
|
|
585
|
+
/part #room - Leave a room. Alias: /p
|
|
586
|
+
/message person - Send a private message. Alias: /m
|
|
587
|
+
/lastlog - Display the last 100 messages for a room.
|
|
588
|
+
/quit - Quit JsChat
|
|
589
|
+
*** End Help ***
|
|
590
|
+
|
|
591
|
+
TEXT
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def manage_commands(line)
|
|
595
|
+
operand = strip_command line
|
|
596
|
+
case line
|
|
597
|
+
when %r{^/switch}, %r{^/s}
|
|
598
|
+
@connection.switch_room operand
|
|
599
|
+
when %r{^/names}
|
|
600
|
+
@connection.send_names operand
|
|
601
|
+
when %r{^/name}, %r{^/nick}
|
|
602
|
+
@connection.send_change_name operand
|
|
603
|
+
when %r{^/quit}
|
|
604
|
+
quit
|
|
605
|
+
when %r{^/join}, %r{^/j}
|
|
606
|
+
@connection.send_join operand
|
|
607
|
+
when %r{^/part}, %r{^/p}
|
|
608
|
+
@connection.send_part operand
|
|
609
|
+
when %r{^/lastlog}
|
|
610
|
+
@connection.send_lastlog
|
|
611
|
+
when %r{^/clear}
|
|
612
|
+
@windows[:text].clear
|
|
613
|
+
@windows[:text].refresh
|
|
614
|
+
display_time
|
|
615
|
+
when %r{^/message}, %r{^/m}
|
|
616
|
+
if operand and operand.size > 0
|
|
617
|
+
message = operand.match(/([^ ]*)\s+(.*)/)
|
|
618
|
+
if message
|
|
619
|
+
@connection.send_private_message message[1], message[2]
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
when %r{^/help}
|
|
623
|
+
help_text.split("\n").each do |message|
|
|
624
|
+
display_text message
|
|
625
|
+
end
|
|
626
|
+
when %r{^/}
|
|
627
|
+
display_text '* Command not found. Try using /help'
|
|
628
|
+
else
|
|
629
|
+
@connection.send_message(line)
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def strip_command(line)
|
|
634
|
+
matches = line.match(%r{/[a-zA-Z][^ ]*(.*)})
|
|
635
|
+
if matches
|
|
636
|
+
matches[1].strip
|
|
637
|
+
else
|
|
638
|
+
line
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def connection=(connection)
|
|
643
|
+
@connection = connection
|
|
644
|
+
@tab_completion = JsClient::TabComplete.new
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
include EM::Protocols::LineText2
|
|
649
|
+
|
|
650
|
+
def receive_line(data)
|
|
651
|
+
data.split("\n").each do |line|
|
|
652
|
+
json = JSON.parse(line.strip)
|
|
653
|
+
# Execute the json
|
|
654
|
+
protocol_response = @protocol.process_message json
|
|
655
|
+
if protocol_response
|
|
656
|
+
if protocol_response.kind_of? Array
|
|
657
|
+
protocol_response.each do |response|
|
|
658
|
+
@keyboard.show_message response.message, response.time
|
|
659
|
+
end
|
|
660
|
+
else
|
|
661
|
+
@keyboard.show_message protocol_response.message, protocol_response.time
|
|
662
|
+
end
|
|
663
|
+
elsif json.has_key? 'notice'
|
|
664
|
+
@keyboard.show_message "* #{json['notice']}"
|
|
665
|
+
else
|
|
666
|
+
@keyboard.show_message "* [SERVER] #{line}"
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
rescue Exception => exception
|
|
670
|
+
@keyboard.show_message "* [CLIENT ERROR] #{exception}"
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def send_join(room)
|
|
674
|
+
@current_room = room
|
|
675
|
+
@keyboard.room_name = room
|
|
676
|
+
@keyboard.display_room_name
|
|
677
|
+
send_data({ 'join' => room }.to_json + "\n")
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def switch_room(room)
|
|
681
|
+
@current_room = room
|
|
682
|
+
@keyboard.room_name = room
|
|
683
|
+
@keyboard.display_room_name
|
|
684
|
+
@keyboard.show_message "* Switched room to: #{room}"
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def send_part(room = nil)
|
|
688
|
+
room = @current_room if room.nil?
|
|
689
|
+
send_data({ 'part' => room }.to_json + "\n")
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def send_names(room = nil)
|
|
693
|
+
room = @current_room if room.nil? or room.strip.empty?
|
|
694
|
+
send_data({ 'names' => room }.to_json + "\n")
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def send_lastlog(room = nil)
|
|
698
|
+
room = @current_room if room.nil? or room.strip.empty?
|
|
699
|
+
send_data({ 'lastlog' => room }.to_json + "\n")
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def send_message(line)
|
|
703
|
+
send_data({ 'to' => @current_room, 'send' => line }.to_json + "\n")
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def send_private_message(user, message)
|
|
707
|
+
send_data({ 'to' => user, 'send' => message }.to_json + "\n")
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def send_identify(username)
|
|
711
|
+
send_data({ 'identify' => username }.to_json + "\n")
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def send_change_name(username)
|
|
715
|
+
if @protocol.identified?
|
|
716
|
+
send_data({ 'change' => 'user', 'user' => { 'name' => username } }.to_json + "\n")
|
|
717
|
+
else
|
|
718
|
+
send_identify username
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def unbind
|
|
723
|
+
Ncurses.endwin
|
|
724
|
+
Ncurses.clear
|
|
725
|
+
puts "Disconnected from server"
|
|
726
|
+
exit
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def post_init
|
|
730
|
+
# When connected
|
|
731
|
+
@protocol = JsChat::Protocol.new self
|
|
732
|
+
send_identify ClientConfig[:name]
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
EM.run do
|
|
737
|
+
puts "Connecting to: #{ClientConfig[:ip]}"
|
|
738
|
+
connection = EM.connect ClientConfig[:ip], ClientConfig[:port], JsClient
|
|
739
|
+
|
|
740
|
+
EM.open_keyboard(JsClient::KeyboardInput) do |keyboard|
|
|
741
|
+
keyboard.connection = connection
|
|
742
|
+
keyboard.setup_screen
|
|
743
|
+
connection.keyboard = keyboard
|
|
744
|
+
end
|
|
745
|
+
end
|