snarl-snp 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.
@@ -0,0 +1,138 @@
1
+ class Snarl
2
+ class SNP
3
+ module Action
4
+
5
+ NOTIFICATION_PARAM_ORDER = [:title, :text, :icon, :timeout, :class, :action, :app]
6
+
7
+ # Sends a SNP command "register" to Snarl. For registering +app+ to Snarl setting window.
8
+ # snp.register('Ruby-Snarl')
9
+ # +app+ :: an application name. Snarl uses it as an application ID.
10
+ # Snarl::SNP keeps +app+ for add_class method and notification method.
11
+ # +app+ default is SNP::DEFAULT_APP, 'Ruby-Snarl'.
12
+ # Returns SNP::Response object.
13
+ #
14
+ # Snarl sends back a casual error when +app+ is already registered.
15
+ # It is treated as SNP::SNPError::Casual::SNP_ERROR_ALREADY_REGISTERED.
16
+ def register(app=nil)
17
+ @app = app
18
+ cmds = {:action => 'register', :app => app}
19
+ request(Request.new(cmds))
20
+ end
21
+ alias :app= :register
22
+
23
+ # Sends a SNP command "add_class" to Snarl. For adding +classid+ class and its +classtitle+ nickname.
24
+ # snp.add_class('green')
25
+ # snp.add_class('red', 'failure popup')
26
+ # +classid+ :: classname ID on the registered application
27
+ # +classtitle+ :: display alias for +classname+, optional
28
+ # Returns SNP::Response object.
29
+ # Before adding a class, you should register the application.
30
+ #
31
+ # Snarl sends back a casual error when +classid+ is already added.
32
+ # It is treated as SNP::SNPError::Casual::SNP_ERROR_CLASS_ALREADY_EXISTS.
33
+ def add_class(classid, classtitle=nil)
34
+ # TODO: add_class(app=nil, classid, classtitle=nil)
35
+ # type=SNP#?version=1.0#?action=add_class#?class=t returns (107) Bad Packet
36
+ raise "#{self}#register(appname) required before add_class" unless @app
37
+ cmds = {:action => 'add_class', :app => @app, :class => classid.to_s, :title => classtitle}
38
+ request(Request.new(cmds))
39
+ end
40
+
41
+ # Sends SNP command "notification" to Snarl. For making a popup message itself.
42
+ # snp.notification('title', 'text', 'icon.jpg', 10, 'classA') # 10 is timeout Integer
43
+ # snp.notification(:title => 't', :text => 't', :icon => 'i.jpg', :timeout => 10, :class => 'claA')
44
+ # +title+ :: title of popup. String.
45
+ # +text+ :: text body of popup. String. linebreaks should be only "\n". "\r" confuses Snarl.
46
+ # +icon+ :: icon image path of popup. String/#to_s. path on Snarl machine or http URL. bmp/jpg/png/gif.
47
+ # +timeout+ :: display seconds of popup. Integer. if nil, DEFAULT_TIMEOUT 10. if 0, popup never closes automatically.
48
+ # +class+ :: classid of popup. String. It should have been added by "add_class" method when you use.
49
+ # notification(title, text, icon=nil, timeout=nil, classid=nil) or notification(keyword-hash).
50
+ # snp.notification('title', 'text', 10)
51
+ # snp.notification('title', 'text')
52
+ # snp.notification('text') # title == DEFAULT_APP == 'Ruby-Snarl'
53
+ def notification(*keyhash)
54
+ # TODO: priority
55
+ cmds = normalize_notification_params(keyhash)
56
+ request(Request.new(cmds))
57
+ end
58
+ alias :notify :notification
59
+
60
+ # Sends SNP command "unregister" to Snarl. For removing +app+ from Snarl setting window.
61
+ # snp.unregister('Ruby-Snarl')
62
+ # After this, Snarl users can not edit the settings for +app+ 's popup.
63
+ # If you allow users to edit settings, do not send unregister.
64
+ # Without sending unregister, the applications are always reseted when Snarl restarts.
65
+ #
66
+ # Snarl sends back a casual error when +app+ is not registered.
67
+ # It is treated as SNP::SNPError::Casual::SNP_ERROR_NOT_REGISTERED.
68
+ def unregister(app=nil)
69
+ app = app || @app
70
+ raise "#{self}#unregister requires appname." unless app
71
+ cmds = {:action => 'unregister', :app => app}
72
+ request(Request.new(cmds))
73
+ end
74
+
75
+ # Sends SNP command "hello".
76
+ # irb> Snarl::SNP.new('127.0.0.1').hello
77
+ # SNP/1.1/0/OK/Snarl R2.21
78
+ def hello
79
+ request(Request.new({:action => 'hello'}))
80
+ end
81
+
82
+ # Sends SNP command "version".
83
+ # irb> Snarl::SNP.new('127.0.0.1').version
84
+ # SNP/1.1/0/OK/40.15
85
+ def version
86
+ request(Request.new({:action => 'version'}))
87
+ end
88
+
89
+ # UTILS -----------------------------
90
+
91
+ private
92
+
93
+ def paramarray_to_hash(param_array)
94
+ title, text, icon, timeout, classid, action, app = param_array
95
+ pattern = {
96
+ :only_text => (param_array.size == 1),
97
+ :text_timeout => (param_array.size == 2) && title && text.kind_of?(Integer),
98
+ :title_text_timeout => (param_array.size == 3) && title && text && icon.kind_of?(Integer)
99
+ }
100
+ case
101
+ when pattern[:only_text] then # notify("msg")
102
+ {:title => nil, :text => title}
103
+ when pattern[:text_timeout] then # notify("msg", 10)
104
+ {:title => nil, :text => title, :timeout => text}
105
+ when pattern[:title_text_timeout] then # notify("tit", "msg", 10)
106
+ {:title => title, :text => text, :timeout => icon}
107
+ else
108
+ Hash[*NOTIFICATION_PARAM_ORDER.zip(param_array).flatten].delete_if{|k, v| v.nil?}
109
+ end
110
+ end
111
+
112
+ # we support for:
113
+ # notify( 'text')
114
+ # notify('title', 'text')
115
+ # notify( 'text', 10)
116
+ # notify('title', 'text', 10)
117
+ def normalize_notification_params(param)
118
+ res = Hash.new
119
+ if param[0].kind_of?(Hash) then
120
+ keyhash = param[0]
121
+ else
122
+ keyhash = paramarray_to_hash(param)
123
+ end
124
+ NOTIFICATION_PARAM_ORDER.each do |command|
125
+ keyhash_value = (keyhash[command.to_s] || keyhash[command])
126
+ res[command] = keyhash_value if keyhash_value
127
+ end
128
+ res[:action] = 'notification'
129
+ res[:app] = @app if (res[:app].nil? && @app) # default notification
130
+ res[:app] = nil if res[:app] == :anonymous # from snp.show_message
131
+ res[:title] = (@title || DEFAULT_TITLE) unless res[:title]
132
+ res[:timeout] = (@timeout || DEFAULT_TIMEOUT) unless res[:timeout]
133
+ res[:icon] = icon(res[:icon]) if res[:icon]
134
+ return res
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,32 @@
1
+
2
+ # when config file exists, require this config
3
+ class Snarl
4
+ class SNP
5
+ class Config
6
+
7
+ DEFAULT_HOST = '127.0.0.1'
8
+ DEFAULT_PORT = 9887
9
+ @host = nil
10
+ @port = nil
11
+
12
+ def self.host
13
+ @host || ENV['SNARL_HOST'] || DEFAULT_HOST
14
+ end
15
+ def self.host=(v)
16
+ @host = if v then v.to_s else nil end
17
+ end
18
+
19
+ def self.port
20
+ @port || ENV['SNARL_PORT'] || DEFAULT_PORT
21
+ end
22
+ def self.port=(v)
23
+ @port = if v then v.to_i else nil end
24
+ end
25
+
26
+ def self.reset
27
+ self.host = nil
28
+ self.port = nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,76 @@
1
+ class Snarl
2
+ class SNP
3
+ module Error
4
+ class SNPError < StandardError
5
+ def initialize(response, request=nil) # request is String or Request
6
+ @response, @request = response, request
7
+ end
8
+ attr_accessor :request, :response
9
+ def code ; @response.code ; end
10
+ def message ; "(#{code}) #{@response.message}" ; end
11
+ def ok? ; @response.ok? ; end
12
+ end
13
+
14
+ class Casual < SNPError ; end
15
+
16
+ class SNP_OK < Casual ; end
17
+ # (0) OK
18
+
19
+ class SNP_ERROR_NOT_REGISTERED < Casual ; end
20
+ # (202) The application hasn't been registered.
21
+
22
+ class SNP_ERROR_ALREADY_REGISTERED < Casual ; end
23
+ # (203) The application is already registered.
24
+
25
+ class SNP_ERROR_CLASS_ALREADY_EXISTS < Casual ; end
26
+ # (204) Class is already registered.
27
+
28
+ class Fatal < SNPError ; end
29
+
30
+ class SNP_ERROR_FAILED < Fatal ; end
31
+ # (101) An internal error occurred - usually this represents a fault within Snarl itself.
32
+
33
+ class SNP_ERROR_UNKNOWN_COMMAND < Fatal ; end
34
+ # (102) An unknown action was specified.
35
+
36
+ class SNP_ERROR_TIMED_OUT < Fatal ; end
37
+ # (103) The command sending (or subsequent reply) timed out.
38
+
39
+ class SNP_ERROR_BAD_PACKET < Fatal ; end
40
+ # (107) The command packet is wrongly formed.
41
+
42
+ class SNP_ERROR_NOT_RUNNING < Fatal ; end
43
+ # (201) Incoming network notification handling has been disabled by the user.
44
+
45
+ class RUBYSNARL_UNKNOWN_RESPONSE < Fatal ; end
46
+ # (???) Snarl returns unknown return code.
47
+
48
+ CODE_TO_OBJ = {
49
+ '0' => SNP_OK,
50
+ '101' => SNP_ERROR_FAILED,
51
+ '102' => SNP_ERROR_UNKNOWN_COMMAND,
52
+ '103' => SNP_ERROR_TIMED_OUT,
53
+ '107' => SNP_ERROR_BAD_PACKET,
54
+ '201' => SNP_ERROR_NOT_RUNNING,
55
+ '202' => SNP_ERROR_NOT_REGISTERED,
56
+ '203' => SNP_ERROR_ALREADY_REGISTERED,
57
+ '204' => SNP_ERROR_CLASS_ALREADY_EXISTS
58
+ }
59
+
60
+ def self.klass(response, request=nil)
61
+ if klass = Error::CODE_TO_OBJ[response.to_s] then
62
+ klass
63
+ elsif response.kind_of?(Error::SNPError)
64
+ response
65
+ else
66
+ Error::RUBYSNARL_UNKNOWN_RESPONSE
67
+ end
68
+ end
69
+
70
+ def self.raise(response, request)
71
+ Error.klass.new(response, request)
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,83 @@
1
+ class Snarl
2
+ class SNP
3
+ class Request
4
+ PROTOCOL_NAME = "SNP"
5
+ PROTOCOL_VERSION ="1.0"
6
+
7
+ SNP_ACTIONS = {
8
+ 'register' => %w(type version action app),
9
+ 'add_class' => %w(type version action app class title),
10
+ 'notification' => %w(type version action app class title text timeout icon priority),
11
+ 'unregister' => %w(type version action app),
12
+ 'hello' => %w(type version action),
13
+ 'version' => %w(type version action),
14
+ }
15
+
16
+ # You have to send non-ascii messages by "Windows" encoding.
17
+ ENCODING = 'cp932'
18
+
19
+ SNP_SEPARATOR = '#?'
20
+ SNP_TERMINAL_STRING = "\r\n"
21
+ SNP_HEADER = {'type' => PROTOCOL_NAME, 'version' => PROTOCOL_VERSION}
22
+
23
+ # make SNP request string from command hash and is Request object.
24
+ def initialize(cmd_hash={})
25
+ @commands = {}.update(cmd_hash)
26
+ end
27
+
28
+ attr_reader :commands
29
+
30
+ # Adds command key and value to Request
31
+ def []=(cmdkey, value) ; @commands[cmdkey] = value ; end # TODO: normalize
32
+ # Returns command value for command key
33
+ def [](cmdkey) ; @commands[cmdkey] ; end
34
+
35
+ # Returns Request query string with SNP_TERMINAL_STRING "\r\n"
36
+ def to_str ; query + SNP_TERMINAL_STRING ; end # FIXME: include "\r\n"?
37
+ alias :to_s :to_str
38
+ # Returns Request query string. has no SNP_TERMINAL_STRING "\r\n".
39
+ def inspect ; query ; end
40
+ def action ; @commands['action'] ; end
41
+
42
+ private
43
+
44
+ def order_command_pair
45
+ action = @commands['action']
46
+ @commands.to_a.sort_by{|pair| SNP_ACTIONS[action].index(pair[0])}
47
+ end
48
+
49
+ def query
50
+ normalize
51
+ order_command_pair.map{|pair| pair.join('=')}.join(SNP_SEPARATOR)
52
+ end
53
+
54
+ # normalize item pairs
55
+ # - symbol keys and upcase KEYS are normalized into downcased string keys.
56
+ # - query should not have any "\r". use "\n"
57
+ # - when value is nil, delete the item pair.
58
+ def normalize
59
+ norm_commands = Hash[*@commands.map{|k, v| [crlf2lf(k).downcase, crlf2lf(v)]}.flatten]
60
+ action = normalize_action(norm_commands['action'])
61
+ norm_commands['action'] = action
62
+ @commands = SNP_HEADER.dup
63
+ available_commands_in(action).each do |cmd|
64
+ @commands[cmd] = norm_commands[cmd] if norm_commands[cmd]
65
+ end
66
+ end
67
+
68
+ def normalize_action(action_type)
69
+ action_type.downcase.gsub(/-/){'_'}.sub(/\Aaddclass\Z/){'add_class'}
70
+ end
71
+
72
+ def available_commands_in(action)
73
+ SNP_ACTIONS[action] || {}
74
+ end
75
+
76
+ def crlf2lf(s)
77
+ s ? s.to_s.gsub(/\r\n/){"\n"}.gsub(/\r/){"\n"} : s
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+
@@ -0,0 +1,36 @@
1
+ class Snarl
2
+ class SNP
3
+ class Response
4
+ ResponseParseRegexp = /\ASNP\/[\d\.]+\/(\d+)\/(.+?)\Z/
5
+ def initialize(s)
6
+ if s.respond_to?(:get) then
7
+ @response = s.get
8
+ else
9
+ @response = s
10
+ end
11
+ parse_response
12
+ end
13
+
14
+ attr_reader :code, :message, :infomation, :response
15
+ attr_accessor :request
16
+ alias :status :code
17
+
18
+ def parse_response
19
+ if ResponseParseRegexp =~ @response.chomp then
20
+ @code = $1
21
+ @message, @infomation= $2.split(/\//)
22
+ else
23
+ raise Snarl::SNP::Error::RUBYSNARL_UNKNOWN_RESPONSE.new(self, nil)
24
+ end
25
+ raise error.new(self, nil) unless ok?
26
+ end
27
+
28
+ def error ; Error.klass(self.code) ; end
29
+
30
+ def to_s ; code ; end # puts response #=> "0"
31
+ def inspect ; @response.chomp ; end # p response #=> "SNP/1.1/0/OK/456"
32
+
33
+ def ok? ; code.to_i.zero? ; end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,142 @@
1
+ class Snarl # conpat for ruby-snarl
2
+
3
+ class SNP
4
+
5
+ include Action
6
+
7
+ # default "timeout command" value. popup disappers in 10 seconds.
8
+ DEFAULT_TIMEOUT = 10
9
+
10
+ # default title for "non-title" notification
11
+ DEFAULT_TITLE = 'Ruby-Snarl'
12
+
13
+ # Snarl::SNP.new('127.0.0.1', 9887)
14
+ def initialize(host=nil, port=nil, verbose=false)
15
+ @host = host
16
+ @port = port
17
+ @verbose = verbose
18
+ @logger = nil
19
+ @app = nil
20
+ @timeout = nil
21
+ @iconset = {}
22
+ @title = nil
23
+ end
24
+
25
+ # When you set it true, all unimportant SNP errors raise.
26
+ # Default is false, Snarl::SNP::Error::Casual are disabled.
27
+ attr_accessor :verbose
28
+
29
+ # a value of SNP command "app".
30
+ attr_reader :app
31
+
32
+ attr_accessor :title
33
+
34
+ # a value of SNP command "timeout".
35
+ attr_accessor :timeout
36
+
37
+ # set Logger object. It is used when sending request and getting response.
38
+ def logger=(logger)
39
+ @logger = (logger.kind_of?(Class) ? logger.new($stdout) : logger)
40
+ end
41
+
42
+ # send Snarl::SNP::Request/Hash/String and get Snarl::SNP::Response fron Snarl.
43
+ # When the response "fatal" response, raise errors.
44
+ # When method +verbose+ returns true, "casual" errors also raises.
45
+ def request(req)
46
+ req = Request.new(req) if req.kind_of?(Hash)
47
+ debug(req)
48
+ begin
49
+ action = req.kind_of?(Request) ? req.action : '(string)'
50
+ res = get_response(req)
51
+ info("#{action}: #{res.inspect}")
52
+ rescue Error::Casual => ex
53
+ info("#{action}: (ignored) #{ex.message}")
54
+ raise if verbose
55
+ rescue Error::Fatal => ex
56
+ info("#{action}: #{ex.message}")
57
+ raise
58
+ end
59
+ return res
60
+ end
61
+
62
+ # SHORTCUT METHODS ----------------
63
+
64
+ # add_classes('type1', 'type2', 'type3')
65
+ # add_classes(['type1', desc1], ['type2', desc2], ['type3', desc3])
66
+ def add_classes(*classes)
67
+ classes.each do |classpair|
68
+ classpair = [classpair, nil] if classpair.kind_of?(String)
69
+ add_class(*classpair)
70
+ end
71
+ end
72
+
73
+ # returns icon path if SNP knows. optional.
74
+ def icon(s)
75
+ if @iconset.has_key?(s) then @iconset[s] else s end
76
+ end
77
+
78
+ # set icons pair. quite optional.
79
+ # snp.iconset(:red => 'red.jpg')
80
+ # snp.notification('title', 'text', :red) #=> sends "icon=red.jpg"
81
+ # snp.notification('title', 'text', 'blue') #=> sends "icon=blue"
82
+ def iconset(icons)
83
+ @iconset = icons
84
+ end
85
+ alias :icons :iconset
86
+
87
+ def ping
88
+ notification(DEFAULT_TITLE, 'Ruby Snarl-SNP Ping Message', 3, nil)
89
+ end
90
+
91
+ alias :message :notification
92
+
93
+ def snarl_hello
94
+ hello.infomation
95
+ end
96
+
97
+ def snarl_version
98
+ version.infomation
99
+ end
100
+
101
+ # Snarl::SNP.open(host, port){|snp| snp.register ... }
102
+ # "ensure block" is empty. TCPSocket is closed per access.
103
+ def self.open(host=nil, port=nil, verbose=false, &block)
104
+ client = new(host, port, verbose)
105
+ yield(client) # socket always closed in TCPSocket#open{...}
106
+ client
107
+ end
108
+
109
+ # send message only. app is "anonymous".
110
+ # Snarl::SNP.show_message(host, 9887, title, text, tomeout, icon)
111
+ # Snarl::SNP.show_message(host, title, text, tomeout, icon)
112
+ def self.show_message(host, port, title=nil, text=nil, timeout=nil, icon=nil)
113
+ # TODO: (host, title, text, 10)
114
+ if port.kind_of?(String) && icon.nil? then
115
+ port, title, text, timeout, icon = nil, port, title, text, timeout
116
+ end
117
+ new(host, port).notification(:title => title, :text => text, :timeout => timeout, :icon => icon)
118
+ end
119
+
120
+ def self.ping(host=nil, port=nil)
121
+ new(host, port).ping
122
+ end
123
+ # ----------------- SHORTCUT METHODS
124
+
125
+ private
126
+
127
+ def get_response(req)
128
+ host = @host || Config.host
129
+ port = @port || Config.port
130
+ TCPSocket.open(host, port) do |s|
131
+ s.write(req)
132
+ res = Response.new(s.gets)
133
+ res.request = req
134
+ res
135
+ end
136
+ end
137
+
138
+ def info(m); @logger.info(m) if @logger; end
139
+ def debug(m); @logger.debug(m) if @logger; end
140
+
141
+ end
142
+ end