wick 0.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/examples/calculator +22 -0
- data/examples/echo +10 -0
- data/examples/irc/basic +73 -0
- data/examples/irc/lib/irc_event.rb +60 -0
- data/examples/irc/lib/nice.rb +23 -0
- data/examples/irc/lib/nice/client.rb +58 -0
- data/examples/irc/lib/nice/manager.rb +23 -0
- data/examples/irc/lib/nice/ui.rb +135 -0
- data/examples/irc/lib/nice/user_command.rb +45 -0
- data/examples/irc/nice +38 -0
- data/examples/telnet +20 -0
- data/lib/wick.rb +39 -0
- data/lib/wick/bus.rb +8 -0
- data/lib/wick/io.rb +41 -0
- data/lib/wick/stream.rb +125 -0
- metadata +59 -0
data/examples/calculator
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << File.expand_path('../../lib', __FILE__)
|
4
|
+
|
5
|
+
require 'wick/io'
|
6
|
+
|
7
|
+
Wick::IO.bind(read: $stdin, write: $stdout) do |input|
|
8
|
+
initial = {total: 0}
|
9
|
+
|
10
|
+
state = input.scan(initial) { |s, line|
|
11
|
+
if line.strip.empty?
|
12
|
+
{total: 0, message: "Total: #{s[:total]}"}
|
13
|
+
else
|
14
|
+
{total: s[:total] + line.to_f}
|
15
|
+
end
|
16
|
+
}
|
17
|
+
|
18
|
+
output = state.select { |s| s.has_key?(:message) }
|
19
|
+
.map { |s| s[:message] + "\n\n" }
|
20
|
+
|
21
|
+
output
|
22
|
+
end
|
data/examples/echo
ADDED
data/examples/irc/basic
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
unless ARGV.length >= 1
|
4
|
+
$stderr.puts "Usage: #{$0} HOST [PORT [NICK]]"
|
5
|
+
exit 1
|
6
|
+
end
|
7
|
+
|
8
|
+
$LOAD_PATH << File.expand_path('../../../lib', __FILE__)
|
9
|
+
$LOAD_PATH << File.expand_path('../lib', __FILE__)
|
10
|
+
|
11
|
+
require 'wick/io'
|
12
|
+
require 'irc_event'
|
13
|
+
require 'socket'
|
14
|
+
require 'colored'
|
15
|
+
|
16
|
+
def main
|
17
|
+
host = ARGV.fetch(0)
|
18
|
+
port = ARGV.fetch(1) { "6667" }
|
19
|
+
nick = ARGV.fetch(2) { "frippery" }
|
20
|
+
|
21
|
+
socket = TCPSocket.new(host, port)
|
22
|
+
|
23
|
+
Wick::IO.bind(
|
24
|
+
read: [socket, $stdin],
|
25
|
+
write: [socket, $stdout]
|
26
|
+
) do |network_in, user_in|
|
27
|
+
server_events = network_in.map { |line| IRCEvent.parse(line) }
|
28
|
+
|
29
|
+
client = Client.new(nick)
|
30
|
+
client_commands = client.transform(server_events)
|
31
|
+
network_out = user_in.merge(client_commands)
|
32
|
+
|
33
|
+
ui = UI.new
|
34
|
+
user_out = ui.transform(client_commands, server_events)
|
35
|
+
|
36
|
+
[network_out, user_out]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Client < Struct.new(:nick)
|
41
|
+
def transform(server_events)
|
42
|
+
nick_and_user = Wick.from_array([
|
43
|
+
"NICK #{nick}",
|
44
|
+
"USER #{nick} () * #{nick}"
|
45
|
+
])
|
46
|
+
|
47
|
+
ping = server_events.select { |msg| msg.command == "PING" }
|
48
|
+
pong = ping.map { |msg| "PONG " + msg.params.join(" ") }
|
49
|
+
|
50
|
+
nick_and_user.merge(pong)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class UI
|
55
|
+
def transform(client_commands, server_events)
|
56
|
+
incoming_messages = server_events.select { |event| event.command == "PRIVMSG" }
|
57
|
+
other_events = server_events.select { |event| event.command != "PRIVMSG" }
|
58
|
+
|
59
|
+
message_lines = incoming_messages.map { |event|
|
60
|
+
channel = event.params[0]
|
61
|
+
user = "<#{event.user}>"
|
62
|
+
message = event.params[1]
|
63
|
+
|
64
|
+
"#{channel.green} #{user.yellow} #{message}"
|
65
|
+
}
|
66
|
+
|
67
|
+
message_lines
|
68
|
+
.merge(other_events.map { |event| "< #{event.line}".magenta })
|
69
|
+
.merge(client_commands.map { |line| "> #{line}".magenta })
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
main
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Copied from https://github.com/Nerdmaster/ruby-irc-yail/blob/develop/lib/net/yail/message_parser.rb
|
2
|
+
|
3
|
+
class IRCEvent
|
4
|
+
attr_reader :line, :nick, :user, :host, :prefix, :command, :params, :servername
|
5
|
+
|
6
|
+
USER = /\S+?/
|
7
|
+
NICK = /[\w\d\\|`'^{}\]\[-]+?/
|
8
|
+
HOST = /\S+?/
|
9
|
+
SERVERNAME = /\S+?/
|
10
|
+
|
11
|
+
# This is automatically grouped for ease of use in the parsing. Group 1 is
|
12
|
+
# the full prefix; 2, 3, and 4 are nick/user/host; 1 is also servername if
|
13
|
+
# there was no match to populate 2, 3, and 4.
|
14
|
+
PREFIX = /((#{NICK})!(#{USER})@(#{HOST})|#{SERVERNAME})/
|
15
|
+
COMMAND = /(\w+|\d{3})/
|
16
|
+
TRAILING = /\:\S*?/
|
17
|
+
MIDDLE = /(?: +([^ :]\S*))/
|
18
|
+
|
19
|
+
MESSAGE = /^(?::#{PREFIX} +)?#{COMMAND}(.*)$/
|
20
|
+
|
21
|
+
def self.parse(line)
|
22
|
+
new(line)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(line)
|
26
|
+
line = line.sub(/[\r\n]+$/, '')
|
27
|
+
|
28
|
+
@line = line
|
29
|
+
@params = []
|
30
|
+
|
31
|
+
if line =~ MESSAGE
|
32
|
+
matches = Regexp.last_match
|
33
|
+
|
34
|
+
@prefix = matches[1]
|
35
|
+
if (matches[2])
|
36
|
+
@nick = matches[2]
|
37
|
+
@user = matches[3]
|
38
|
+
@host = matches[4]
|
39
|
+
else
|
40
|
+
@servername = matches[1]
|
41
|
+
end
|
42
|
+
|
43
|
+
@command = matches[5]
|
44
|
+
|
45
|
+
# Args are a bit tricky. First off, we know there must be a single
|
46
|
+
# space before the arglist, so we need to strip that. Then we have to
|
47
|
+
# separate the trailing arg as it can contain nearly any character. And
|
48
|
+
# finally, we split the "middle" args on space.
|
49
|
+
arglist = matches[6].sub(/^ +/, '')
|
50
|
+
arglist.sub!(/^:/, ' :')
|
51
|
+
(middle_args, trailing_arg) = arglist.split(/ +:/, 2)
|
52
|
+
@params.push(middle_args.split(/ +/)) if middle_args
|
53
|
+
@params.push(trailing_arg) if trailing_arg
|
54
|
+
@params.compact!
|
55
|
+
@params.flatten!
|
56
|
+
end
|
57
|
+
|
58
|
+
freeze
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'wick/io'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
require 'nice/client'
|
5
|
+
require 'nice/ui'
|
6
|
+
require 'nice/manager'
|
7
|
+
|
8
|
+
module Nice
|
9
|
+
def self.run!(host, port, nick)
|
10
|
+
client = Client.new(nick)
|
11
|
+
ui = UI.new(nick)
|
12
|
+
manager = Manager.new(client, ui)
|
13
|
+
|
14
|
+
socket = TCPSocket.new(host, port)
|
15
|
+
|
16
|
+
Wick::IO.bind(
|
17
|
+
read: [socket, $stdin],
|
18
|
+
write: [socket, $stdout]
|
19
|
+
) do |network_in, user_in|
|
20
|
+
manager.transform(network_in, user_in)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'irc_event'
|
2
|
+
|
3
|
+
module Nice
|
4
|
+
class Client < Struct.new(:nick)
|
5
|
+
def initialize(*args)
|
6
|
+
super(*args)
|
7
|
+
freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
def transform(network_in, user_commands)
|
11
|
+
server_events = network_in.map { |line| IRCEvent.parse(line) }
|
12
|
+
|
13
|
+
nick_and_user = Wick.from_array([
|
14
|
+
"NICK #{nick}",
|
15
|
+
"USER #{nick} () * #{nick}"
|
16
|
+
])
|
17
|
+
|
18
|
+
ping = server_events.select { |msg| msg.command == "PING" }
|
19
|
+
pong = ping.map { |msg| "PONG " + msg.params.join(" ") }
|
20
|
+
|
21
|
+
outgoing = process_user_commands(user_commands)
|
22
|
+
|
23
|
+
network_out = outgoing.merge(nick_and_user).merge(pong)
|
24
|
+
|
25
|
+
server_events.log!("server_events")
|
26
|
+
network_out.log!("network_out")
|
27
|
+
|
28
|
+
[network_out, server_events]
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_user_commands(user_commands)
|
32
|
+
user_commands.map { |cmd|
|
33
|
+
case cmd.action
|
34
|
+
when nil
|
35
|
+
if cmd.channel
|
36
|
+
"PRIVMSG #{cmd.channel} :#{cmd.argument}"
|
37
|
+
else
|
38
|
+
cmd.argument
|
39
|
+
end
|
40
|
+
when :me
|
41
|
+
cmd.channel && "PRIVMSG #{cmd.channel} :\x01ACTION #{cmd.argument}\x01"
|
42
|
+
when :msg
|
43
|
+
"PRIVMSG #{cmd.channel} :#{cmd.argument}"
|
44
|
+
when :join
|
45
|
+
"JOIN #{cmd.channel}"
|
46
|
+
when :part
|
47
|
+
"PART #{cmd.channel}"
|
48
|
+
when :quit
|
49
|
+
"QUIT :#{cmd.argument}"
|
50
|
+
when :raw
|
51
|
+
cmd.argument
|
52
|
+
else
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
}.compact
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Nice
|
2
|
+
class Manager
|
3
|
+
def initialize(client, ui)
|
4
|
+
@client = client
|
5
|
+
@ui = ui
|
6
|
+
freeze
|
7
|
+
end
|
8
|
+
|
9
|
+
def transform(network_in, user_in)
|
10
|
+
server_events_bus = Wick::Bus.new
|
11
|
+
user_commands_bus = Wick::Bus.new
|
12
|
+
|
13
|
+
network_out, server_events = @client.transform(network_in, user_commands_bus)
|
14
|
+
user_out, user_commands = @ui.transform(user_in, server_events_bus)
|
15
|
+
|
16
|
+
server_events_bus.consume!(server_events)
|
17
|
+
user_commands_bus.consume!(user_commands)
|
18
|
+
|
19
|
+
[network_out, user_out]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'nice/user_command'
|
2
|
+
require 'colored'
|
3
|
+
|
4
|
+
module Nice
|
5
|
+
class UI
|
6
|
+
class ChannelState < Struct.new(:joined_channels, :channel_index)
|
7
|
+
def current_channel_name
|
8
|
+
channel_index && joined_channels[channel_index]
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_channel_index(inc)
|
12
|
+
ChannelState.new(
|
13
|
+
joined_channels,
|
14
|
+
(channel_index + inc) % joined_channels.length)
|
15
|
+
end
|
16
|
+
|
17
|
+
def join_channel(name)
|
18
|
+
new_list = joined_channels | [name]
|
19
|
+
new_idx = new_list.length-1
|
20
|
+
|
21
|
+
ChannelState.new(new_list, new_idx)
|
22
|
+
end
|
23
|
+
|
24
|
+
def part_channel(name)
|
25
|
+
new_list = joined_channels - [name]
|
26
|
+
new_idx = [channel_index, new_list.length-1].min
|
27
|
+
|
28
|
+
ChannelState.new(new_list, new_idx)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(username)
|
33
|
+
@username = username
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def transform(user_in, server_events)
|
38
|
+
user_commands_without_channel = user_in.map { |line| UserCommand.parse(line) }.log!("user_commands_without_channel")
|
39
|
+
|
40
|
+
channel_state = get_channel_state(user_commands_without_channel, server_events).log!("channel_state")
|
41
|
+
|
42
|
+
user_commands = user_commands_without_channel.sampling(channel_state) { |cmd, cs|
|
43
|
+
cmd.with_channel(cs.current_channel_name)
|
44
|
+
}.log!("user_commands")
|
45
|
+
|
46
|
+
message_log = get_message_log(user_commands, server_events).log!("message log")
|
47
|
+
|
48
|
+
user_out = render_output(channel_state, message_log)
|
49
|
+
|
50
|
+
[user_out, user_commands]
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_channel_state(user_commands_without_channel, server_events)
|
54
|
+
manual_changes = get_manual_state_changes(user_commands_without_channel)
|
55
|
+
automatic_changes = get_automatic_state_changes(server_events)
|
56
|
+
|
57
|
+
initial_state = ChannelState.new([], nil)
|
58
|
+
|
59
|
+
manual_changes.merge(automatic_changes)
|
60
|
+
.scan(initial_state) { |cs, change| change.call(cs) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_manual_state_changes(user_commands_without_channel)
|
64
|
+
user_commands_without_channel.select { |cmd| cmd.action == :next or cmd.action == :prev }
|
65
|
+
.map { |cmd|
|
66
|
+
proc { |cs|
|
67
|
+
if cmd.action == :next
|
68
|
+
cs.update_channel_index(+1)
|
69
|
+
elsif cmd.action == :prev
|
70
|
+
cs.update_channel_index(-1)
|
71
|
+
end
|
72
|
+
}
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_automatic_state_changes(server_events)
|
77
|
+
server_events.select { |event| event.user == @username }
|
78
|
+
.select { |event| event.command == "JOIN" or event.command == "PART" }
|
79
|
+
.map { |event|
|
80
|
+
proc { |cs|
|
81
|
+
if event.command == "JOIN"
|
82
|
+
cs.join_channel(event.params.first)
|
83
|
+
elsif event.command == "PART"
|
84
|
+
cs.part_channel(event.params.first)
|
85
|
+
end
|
86
|
+
}
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_message_log(user_commands, server_events)
|
91
|
+
outgoing_messages = user_commands.select { |cmd| cmd.action.nil? }
|
92
|
+
.map { |cmd| [cmd.channel, @username, cmd.argument] }
|
93
|
+
|
94
|
+
incoming_messages = server_events.select { |event| event.command == "PRIVMSG" }
|
95
|
+
.map { |event| [event.params[0], event.user, event.params[1]] }
|
96
|
+
|
97
|
+
outgoing_messages.merge(incoming_messages).scan(Hash.new([])) { |map, triple|
|
98
|
+
channel, user, message = *triple
|
99
|
+
map.merge(channel => map[channel] + ["<#{user}>".yellow + " #{message}"])
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def render_output(channel_state, message_log)
|
104
|
+
message_log.combine(channel_state) { |current_log, cs|
|
105
|
+
if current_log and cs
|
106
|
+
tabs = cs.joined_channels.map { |c|
|
107
|
+
if c == cs.current_channel_name
|
108
|
+
c.green
|
109
|
+
else
|
110
|
+
c
|
111
|
+
end
|
112
|
+
}
|
113
|
+
|
114
|
+
channel_log = current_log[cs.current_channel_name].last(20)
|
115
|
+
|
116
|
+
clear_screen + move_to(1,1) + tabs.join(" ") + "\n" + channel_log.join("\n")
|
117
|
+
else
|
118
|
+
""
|
119
|
+
end
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def clear_screen
|
124
|
+
ansi("2J")
|
125
|
+
end
|
126
|
+
|
127
|
+
def move_to(x, y)
|
128
|
+
ansi("#{x};#{y}H")
|
129
|
+
end
|
130
|
+
|
131
|
+
def ansi(str)
|
132
|
+
"\e[#{str}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Nice
|
2
|
+
class UserCommand < Struct.new(:action, :argument, :channel)
|
3
|
+
REGEX = /
|
4
|
+
^
|
5
|
+
\/ # leading slash
|
6
|
+
(\w+) # action
|
7
|
+
(
|
8
|
+
\s+ # space
|
9
|
+
(.+) # argument
|
10
|
+
)?
|
11
|
+
$
|
12
|
+
/x
|
13
|
+
|
14
|
+
def self.parse(line)
|
15
|
+
line = line.chomp
|
16
|
+
|
17
|
+
if match = line.match(REGEX)
|
18
|
+
action = match[1].downcase.to_sym
|
19
|
+
argument = match[3]
|
20
|
+
channel = nil
|
21
|
+
|
22
|
+
if [:msg, :join, :part].include?(action)
|
23
|
+
channel, argument = argument.split(/\s+/, 2)
|
24
|
+
end
|
25
|
+
|
26
|
+
new(action, argument, channel)
|
27
|
+
else
|
28
|
+
new(nil, line, nil)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(*args)
|
33
|
+
super(*args)
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_channel(channel_name)
|
38
|
+
if channel
|
39
|
+
self
|
40
|
+
else
|
41
|
+
UserCommand.new(action, argument, channel_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/examples/irc/nice
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << File.expand_path('../../../lib', __FILE__)
|
4
|
+
$LOAD_PATH << File.expand_path('../lib', __FILE__)
|
5
|
+
|
6
|
+
require 'slop'
|
7
|
+
require 'nice'
|
8
|
+
|
9
|
+
opts = Slop.parse(help: true) do
|
10
|
+
banner "Usage: #{$0} HOST [OPTIONS]"
|
11
|
+
|
12
|
+
on 'port=', 'Port to connect to', default: '6667'
|
13
|
+
on 'nick=', 'Nick (and username) to use', default: 'frippery'
|
14
|
+
on 'log=', 'Filename to log to (default: irc.log, stderr if blank)', default: 'irc.log'
|
15
|
+
end
|
16
|
+
|
17
|
+
if ARGV.length != 1
|
18
|
+
puts opts
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
|
22
|
+
host = ARGV.fetch(0)
|
23
|
+
|
24
|
+
if opts[:log] != ''
|
25
|
+
$stderr.reopen File.open(opts[:log], 'a')
|
26
|
+
$stderr.sync = true
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
Nice.run!(host, opts[:port], opts[:nick])
|
31
|
+
rescue
|
32
|
+
if $stderr.is_a?(File)
|
33
|
+
puts "Something went wrong. Check #{$stderr.path}, or run again with --log '' to output to stderr."
|
34
|
+
end
|
35
|
+
|
36
|
+
raise
|
37
|
+
end
|
38
|
+
|
data/examples/telnet
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
unless ARGV.length >= 1
|
4
|
+
$stderr.puts "Usage: #{$0} HOST [PORT]"
|
5
|
+
exit 1
|
6
|
+
end
|
7
|
+
|
8
|
+
$LOAD_PATH << File.expand_path('../../lib', __FILE__)
|
9
|
+
|
10
|
+
require 'wick/io'
|
11
|
+
require 'socket'
|
12
|
+
|
13
|
+
socket = TCPSocket.new(ARGV[0], ARGV[1] || 23)
|
14
|
+
|
15
|
+
Wick::IO.bind(
|
16
|
+
read: [socket, $stdin],
|
17
|
+
write: [socket, $stdout]
|
18
|
+
) do |network_in, user_in|
|
19
|
+
[user_in, network_in]
|
20
|
+
end
|
data/lib/wick.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'wick/stream'
|
2
|
+
require 'wick/bus'
|
3
|
+
|
4
|
+
module Wick
|
5
|
+
START = Object.new.freeze
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def from_array(array)
|
9
|
+
s = Stream.new
|
10
|
+
on_next_tick do
|
11
|
+
array.each do |item|
|
12
|
+
s << item
|
13
|
+
end
|
14
|
+
end
|
15
|
+
s
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_loop!(&block)
|
19
|
+
while true
|
20
|
+
tick!
|
21
|
+
block.call()
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_next_tick(&callback)
|
26
|
+
@tick_callbacks ||= []
|
27
|
+
@tick_callbacks.push(callback)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def tick!
|
33
|
+
return unless @tick_callbacks
|
34
|
+
while callback = @tick_callbacks.shift
|
35
|
+
callback.call()
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/wick/bus.rb
ADDED
data/lib/wick/io.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'wick'
|
2
|
+
|
3
|
+
module Wick
|
4
|
+
module IO
|
5
|
+
def self.bind(options, &block)
|
6
|
+
readables_array = [options[:read]].flatten
|
7
|
+
writables_array = [options[:write]].flatten
|
8
|
+
|
9
|
+
read_map = {}
|
10
|
+
readables_array.each do |io|
|
11
|
+
read_map[io] = Stream.new
|
12
|
+
end
|
13
|
+
|
14
|
+
block_arg = read_map.values
|
15
|
+
|
16
|
+
write_streams = [block.call(*block_arg)].flatten
|
17
|
+
write_map = Hash[writables_array.zip(write_streams)]
|
18
|
+
|
19
|
+
write_map.each_pair do |io, stream|
|
20
|
+
stream.each do |line|
|
21
|
+
io.puts(line)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
read_map.values.each do |stream|
|
26
|
+
stream.start!
|
27
|
+
end
|
28
|
+
|
29
|
+
Wick.run_loop! do
|
30
|
+
ready = ::IO.select(read_map.keys)
|
31
|
+
ready[0].each do |io|
|
32
|
+
io.read_nonblock(1_000_000).each_line do |line|
|
33
|
+
read_map[io] << line
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
rescue EOFError
|
38
|
+
puts "A connection was closed. Shutting down."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/wick/stream.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
module Wick
|
2
|
+
class Stream
|
3
|
+
def initialize
|
4
|
+
@handlers = []
|
5
|
+
@start_callbacks = []
|
6
|
+
|
7
|
+
Wick.on_next_tick do
|
8
|
+
self.start!
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(data)
|
13
|
+
@handlers.each do |handler|
|
14
|
+
handler.call(data)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def each(&handler)
|
19
|
+
@handlers << handler
|
20
|
+
end
|
21
|
+
|
22
|
+
def at_start
|
23
|
+
s = Stream.new
|
24
|
+
@start_callbacks.push(proc { s << Wick::START })
|
25
|
+
s
|
26
|
+
end
|
27
|
+
|
28
|
+
def start!
|
29
|
+
@start_callbacks.each(&:call)
|
30
|
+
end
|
31
|
+
|
32
|
+
def map(&transformer)
|
33
|
+
s = Stream.new
|
34
|
+
self.each do |msg|
|
35
|
+
s << transformer.call(msg)
|
36
|
+
end
|
37
|
+
s
|
38
|
+
end
|
39
|
+
|
40
|
+
def flat_map(&transformer)
|
41
|
+
s = Stream.new
|
42
|
+
self.each do |msg|
|
43
|
+
transformer.call(msg).pipe!(s)
|
44
|
+
end
|
45
|
+
s
|
46
|
+
end
|
47
|
+
|
48
|
+
def select(&predicate)
|
49
|
+
s = Stream.new
|
50
|
+
self.each do |msg|
|
51
|
+
s << msg if predicate.call(msg)
|
52
|
+
end
|
53
|
+
s
|
54
|
+
end
|
55
|
+
|
56
|
+
def compact
|
57
|
+
select { |msg| not msg.nil? }
|
58
|
+
end
|
59
|
+
|
60
|
+
def merge(other)
|
61
|
+
s = Stream.new
|
62
|
+
self.pipe!(s)
|
63
|
+
other.pipe!(s)
|
64
|
+
s
|
65
|
+
end
|
66
|
+
|
67
|
+
def combine(other, &combiner)
|
68
|
+
latest_self = nil
|
69
|
+
latest_other = nil
|
70
|
+
|
71
|
+
s = Stream.new
|
72
|
+
|
73
|
+
self.each do |msg|
|
74
|
+
latest_self = msg
|
75
|
+
s << combiner.call(msg, latest_other)
|
76
|
+
end
|
77
|
+
|
78
|
+
other.each do |msg|
|
79
|
+
latest_other = msg
|
80
|
+
s << combiner.call(latest_self, msg)
|
81
|
+
end
|
82
|
+
|
83
|
+
s
|
84
|
+
end
|
85
|
+
|
86
|
+
def sampling(other, &combiner)
|
87
|
+
latest_other = nil
|
88
|
+
|
89
|
+
other.each do |msg|
|
90
|
+
latest_other = msg
|
91
|
+
end
|
92
|
+
|
93
|
+
s = Stream.new
|
94
|
+
|
95
|
+
self.each do |msg|
|
96
|
+
s << combiner.call(msg, latest_other)
|
97
|
+
end
|
98
|
+
|
99
|
+
s
|
100
|
+
end
|
101
|
+
|
102
|
+
def scan(initial, &scanner)
|
103
|
+
s = Wick.from_array([initial])
|
104
|
+
last = initial
|
105
|
+
self.each do |msg|
|
106
|
+
last = scanner.call(last, msg)
|
107
|
+
s << last
|
108
|
+
end
|
109
|
+
s
|
110
|
+
end
|
111
|
+
|
112
|
+
def log!(prefix)
|
113
|
+
self.each do |msg|
|
114
|
+
$stderr.puts("[#{prefix}] " + msg.inspect)
|
115
|
+
end
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
def pipe!(other)
|
120
|
+
self.each do |msg|
|
121
|
+
other << msg
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wick
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Aanand Prasad
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-03 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: aanand.prasad@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- examples/calculator
|
21
|
+
- examples/echo
|
22
|
+
- examples/irc/basic
|
23
|
+
- examples/irc/lib/irc_event.rb
|
24
|
+
- examples/irc/lib/nice.rb
|
25
|
+
- examples/irc/lib/nice/client.rb
|
26
|
+
- examples/irc/lib/nice/manager.rb
|
27
|
+
- examples/irc/lib/nice/ui.rb
|
28
|
+
- examples/irc/lib/nice/user_command.rb
|
29
|
+
- examples/irc/nice
|
30
|
+
- examples/telnet
|
31
|
+
- lib/wick.rb
|
32
|
+
- lib/wick/bus.rb
|
33
|
+
- lib/wick/io.rb
|
34
|
+
- lib/wick/stream.rb
|
35
|
+
homepage: https://github.com/aanand/wick
|
36
|
+
licenses: []
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.8.23
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: Functional reactive programming library
|
59
|
+
test_files: []
|