wick 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'wick/io'
7
+
8
+ Wick::IO.bind(read: $stdin, write: $stdout) do |input|
9
+ input.map { |line| "Haha! You said “#{line.chomp}”." }
10
+ end
@@ -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
@@ -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
+
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ module Wick
2
+ class Bus < Stream
3
+ def consume!(stream)
4
+ stream.pipe!(self)
5
+ end
6
+ end
7
+ end
8
+
@@ -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
@@ -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: []