lib/ruby-growl.rb in ruby-growl-3.0 vs lib/ruby-growl.rb in ruby-growl-4.0

- old
+ new

@@ -1,151 +1,83 @@ -#!/usr/local/bin/ruby -w - require 'digest/md5' require 'socket' +begin + require 'dnssd' +rescue LoadError +end + ## -# ruby-growl allows you to perform Growl notification via UDP from machines -# without growl installed (for example, non-OSX machines). +# ruby-growl allows you to perform Growl notifications from machines without +# growl installed (for example, non-OSX machines). # -# What's Growl? Growl is a really cool "global notification system for Mac OS -# X". See http://growl.info/ +# In version 4, the Growl class is a wrapper for Growl::UDP and Growl::GNTP. +# The GNTP protocol allows setting icons for notifications and callbacks. To +# upgrade from version 3 replace the notification names passed to initialize +# with a call to #add_notification. # -# You'll need a Mac to recieve Growl notifications, but you can send Growl -# notifications from any UDP-capable machine that runs Ruby. +# Basic usage: # -# See also the Ruby Growl bindings in Growl's subversion repository: -# http://growl.info/documentation/growl-source-install.php +# require 'ruby-growl' # -# ruby-growl also contains a command-line notification tool named 'growl'. It -# is almost completely option-compatible with growlnotify. (All except for -p -# is supported, use --priority instead.) +# g = Growl.new "localhost", "ruby-growl" +# g.add_notification "ruby-growl Notification" +# g.notify "ruby-growl Notification", "It came from ruby-growl!", +# "Greetings!" # -# = Synopsis +# For GNTP users, ruby-growl ships with the Ruby icon from the {Ruby Visual +# Identity Team}[http://rubyidentity.org/]: # -# g = Growl.new "127.0.0.1", "ruby-growl", -# ["ruby-growl Notification"] -# g.notify "ruby-growl Notification", "It Came From Ruby-Growl", -# "Greetings!" +# require 'ruby-growl' +# require 'ruby-growl/ruby_logo' +# +# g = Growl.new "localhost", "ruby-growl" +# g.add_notification("notification", "ruby-growl Notification", +# Growl::RUBY_LOGO_PNG) +# g.notify "notification", "It came from ruby-growl", "Greetings!" +# +# See Growl::UDP and Growl::GNTP for protocol-specific API. class Growl ## - # The Ruby that ships with Tiger has a broken #pack, so 'v' means network - # byte order instead of 'n'. + # ruby-growl version - BROKEN_PACK = [1].pack("n") != "\000\001" # :nodoc: + VERSION = '4.0' - little_endian = [1].pack('V*') == [1].pack('L*') - little_endian = !little_endian if BROKEN_PACK - ## - # Endianness of this machine + # Growl error base class - LITTLE_ENDIAN = little_endian + class Error < RuntimeError + end ## - # ruby-growl Version + # Password for authenticating and encrypting requests. - VERSION = '3.0' + attr_accessor :password ## - # Growl Network Registration Packet +pack+ Format - #-- - # Format: - # - # struct GrowlNetworkRegistration { - # struct GrowlNetworkPacket { - # unsigned char version; - # unsigned char type; - # } __attribute__((packed)); - # unsigned short appNameLen; - # unsigned char numAllNotifications; - # unsigned char numDefaultNotifications; - # /* - # * Variable sized. Format: - # * <application name><all notifications><default notifications><checksum> - # * where <all notifications> is of the form (<length><name>){num} and - # * <default notifications> is an array of indices into the all notifications - # * array, each index being 8 bits. - # */ - # unsigned char data[]; - # } __attribute__((packed)); - - GNR_FORMAT = "CCnCCa*" - - GNR_FORMAT.gsub!(/n/, 'v') if BROKEN_PACK - - ## - # Growl Network Notification Packet +pack+ Format - #-- - # Format: - # - # struct GrowlNetworkNotification { - # struct GrowlNetworkPacket { - # unsigned char version; - # unsigned char type; - # } __attribute__((packed)); - # struct GrowlNetworkNotificationFlags { - # unsigned reserved: 12; - # signed priority: 3; - # unsigned sticky: 1; - # } __attribute__((packed)) flags; //size = 16 (12 + 3 + 1) - # unsigned short nameLen; - # unsigned short titleLen; - # unsigned short descriptionLen; - # unsigned short appNameLen; - # /* - # * Variable sized. Format: - # * <notification name><title><description><application name><checksum> - # */ - # unsigned char data[]; - # } __attribute__((packed)); - - GNN_FORMAT = "CCnnnnna*" - - GNN_FORMAT.gsub!(/n/, 'v') if BROKEN_PACK - - # For litle endian machines the NetworkNotificationFlags aren't in network - # byte order - - GNN_FORMAT.sub!((BROKEN_PACK ? 'v' : 'n'), 'v') if LITTLE_ENDIAN - - ## - # Growl UDP Port - - GROWL_UDP_PORT = 9887 - - ## - # Growl Protocol Version - - GROWL_PROTOCOL_VERSION = 1 - - ## - # Growl Registration Packet Id - - GROWL_TYPE_REGISTRATION = 0 - - ## - # Growl Notification Packet Id - - GROWL_TYPE_NOTIFICATION = 1 - - ## # List of hosts accessible via dnssd - def self.list - require 'dnssd' + def self.list type + raise 'you must gem install dnssd' unless Object.const_defined? :DNSSD + require 'timeout' + growls = [] - DNSSD.browse! '_growl._tcp' do |reply| - next unless reply.flags.add? + begin + Timeout.timeout 10 do + DNSSD.browse! type do |reply| + next unless reply.flags.add? - growls << reply + growls << reply - break unless reply.flags.more_coming? + break unless reply.flags.more_coming? + end + end + rescue Timeout::Error end hosts = [] growls.each do |growl| @@ -154,12 +86,10 @@ break end end hosts.uniq - rescue LoadError - raise 'you must gem install dnssd' end ## # Sends a notification using +options+ @@ -170,14 +100,14 @@ puts "Type your message and hit ^D" if $stdin.tty? message = $stdin.read end notify_type = options[:notify_type] - notify_types = [notify_type] - g = new(options[:host], options[:name], notify_types, notify_types, - options[:password]) + g = new options[:host], options[:name] + g.add_notification notify_type + g.password = options[:password] g.notify(notify_type, options[:title], message, options[:priority], options[:sticky]) end @@ -186,19 +116,19 @@ def self.process_args argv require 'optparse' options = { - :host => nil, - :message => nil, - :name => "ruby-growl", - :notify_type => "ruby-growl Notification", - :password => nil, - :priority => 0, - :sticky => false, - :title => "", - :list => false, + host: nil, + message: nil, + name: "ruby-growl", + notify_type: "ruby-growl Notification", + password: nil, + priority: 0, + sticky: false, + title: "", + list: false, } opts = OptionParser.new do |o| o.program_name = File.basename $0 o.version = Growl::VERSION @@ -275,11 +205,15 @@ def self.run argv = ARGV options = process_args argv if options[:list] then begin - puts list + puts 'Growl GNTP hosts:' + puts list '_gntp._tcp' + puts + puts 'Growl UDP hosts:' + puts list '_growl._tcp' rescue => e raise unless e.message =~ /gem install dnssd/ abort "#{e.message} to use --list" end @@ -288,167 +222,107 @@ notify options end ## - # Creates a new Growl notifier and automatically registers any notifications - # with the remote machine. + # Creates a new growl basic notifier for +host+ and +application_name+. # - # +host+ is the host to contact. + # +growl_type+ is used to specify the type of growl server to connect to. + # The following values are allowed: # - # +app_name+ is the name of the application sending the notifications. + # nil:: + # Automatically determine the growl type. If a GNTP server is not found + # then ruby-growl chooses UDP. + # 'GNTP':: + # Use GNTP connections. GNTP is supported by Growl 1.3 and newer and by + # Growl for Windows. + # 'UDP':: + # Uses the UDP growl protocol. UDP growl is supported by Growl 1.2 and + # older. # - # +all_notifies+ is a list of notification types your application sends. - # - # +default_notifies+ is a list of notification types that are turned on by - # default. - # - # I'm not sure about what +default_notifies+ is supposed to be set to, since - # there is a comment that says "not a subset of all_notifies" in the code. - # - # +password+ is the password needed to send notifications to +host+. + # You can use <tt>growl --list</tt> to see growl servers on your local + # network. - def initialize(host, app_name, all_notifies, default_notifies = nil, - password = nil) - @socket = UDPSocket.open - # FIXME This goes somewhere else - @socket.connect host, GROWL_UDP_PORT - @app_name = app_name - @all_notifies = all_notifies - @default_notifies = default_notifies.nil? ? all_notifies : default_notifies - @password = password + def initialize host, application_name, growl_type = nil + @host = host + @application_name = application_name - register + @notifications = {} + @password = nil + + @growl_type = choose_implementation growl_type end ## - # Sends a notification. - # - # +notify_type+ is the type of notification to send. - # - # +title+ is a title for the notification. - # - # +message+ is the body of the notification. - # - # +priority+ is the priorty of message to send. - # - # +sticky+ makes the notification stick until clicked. + # Adds a notification named +name+ to the basic notifier. For GNTP servers + # you may specify a +display_name+ and +icon+ and set the default +enabled+ + # status. - def notify(notify_type, title, message, priority = 0, sticky = false) - raise "Unknown Notification" unless @all_notifies.include? notify_type - raise "Invalid Priority" unless priority >= -2 and priority <= 2 - - send notification_packet(notify_type, title, message, priority, sticky) + def add_notification name, display_name = nil, icon = nil, enabled = true + @notifications[name] = display_name, icon, enabled end - ## - # Registers the notification types with +host+. + def choose_implementation type # :nodoc: + raise ArgumentError, + "type must be \"GNTP\", \"UDP\" or nil; was #{type.inspect}" unless + ['GNTP', 'UDP', nil].include? type - def register - send registration_packet - end + return type if type - ## - # Sends a Growl packet + TCPSocket.open @host, Growl::GNTP::PORT do end - def send(packet) - set_sndbuf packet.length - @socket.send packet, 0 - @socket.flush + 'GNTP' + rescue SystemCallError + 'UDP' end ## - # Builds a Growl registration packet + # Sends a notification of type +name+ with the given +title+, +message+, + # +priority+ and +sticky+ settings. - def registration_packet - length = 0 - data = [] - data_format = "" + def notify name, title, message, priority = 0, sticky = false + case @growl_type + when 'GNTP' then + notify_gntp name, title, message, priority, sticky + when 'UDP' then + notify_udp name, title, message, priority, sticky + else + raise Growl::Error, "bug, unknown growl type #{@growl_type.inspect}" + end - packet = [ - GROWL_PROTOCOL_VERSION, - GROWL_TYPE_REGISTRATION - ] + self + end - packet << @app_name.bytesize - packet << @all_notifies.length - packet << @default_notifies.length + def notify_gntp name, title, message, priority, sticky # :nodoc: + growl = Growl::GNTP.new @host, @application_name + growl.password = @password - data << @app_name - data_format = "a#{@app_name.bytesize}" - - @all_notifies.each do |notify| - data << notify.length - data << notify - data_format << "na#{notify.length}" + @notifications.each do |name, details| + growl.add_notification name, *details end - @default_notifies.each do |notify| - data << @all_notifies.index(notify) if @all_notifies.include? notify - data_format << "C" - end + growl.register - data_format.gsub!(/n/, 'v') if BROKEN_PACK - - data = data.pack data_format - - packet << data - - packet = packet.pack GNR_FORMAT - - checksum = Digest::MD5.new << packet - checksum.update @password unless @password.nil? - - packet << checksum.digest - - return packet + growl.notify name, title, message, priority, sticky end - ## - # Builds a Growl notification packet + def notify_udp name, title, message, priority, sticky # :nodoc: + all_notifications = @notifications.keys + default_notifications = @notifications.select do |name, (_, _, enabled)| + enabled + end.map do |name,| + name + end - def notification_packet(name, title, description, priority, sticky) - flags = 0 - data = [] + notification = all_notifications.first - packet = [ - GROWL_PROTOCOL_VERSION, - GROWL_TYPE_NOTIFICATION, - ] + growl = Growl::UDP.new(@host, @application_name, all_notifications, + default_notifications, @password) - flags = 0 - flags |= ((0x7 & priority) << 1) # 3 bits for priority - flags |= 1 if sticky # 1 bit for sticky - - packet << flags - packet << name.bytesize - packet << title.length - packet << description.bytesize - packet << @app_name.bytesize - - data << name - data << title - data << description - data << @app_name - - packet << data.join - packet = packet.pack GNN_FORMAT - - checksum = Digest::MD5.new << packet - checksum.update @password unless @password.nil? - - packet << checksum.digest - - return packet + growl.notify name, title, message, priority, sticky end - ## - # Set the size of the send buffer - #-- - # Is this truly necessary? - - def set_sndbuf(length) - @socket.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDBUF, length - end - end + +require 'ruby-growl/gntp' +require 'ruby-growl/udp'