Class: Growl

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby-growl.rb

Overview

ruby-growl allows you to perform Growl notification via UDP 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 growl.info/

You’ll need a Mac to recieve Growl notifications, but you can send Growl notifications from any UDP-capable machine that runs Ruby.

See also the Ruby Growl bindings in Growl’s subversion repository: growl.info/documentation/growl-source-install.php

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.)

Synopsis

g = Growl.new "127.0.0.1", "ruby-growl",
              ["ruby-growl Notification"]
g.notify "ruby-growl Notification", "It Came From Ruby-Growl",
         "Greetings!"

Constant Summary collapse

BROKEN_PACK =

The Ruby that ships with Tiger has a broken #pack, so ‘v’ means network byte order instead of ‘n’.

[1].pack("n") != "\000\001"
LITTLE_ENDIAN =

Endianness of this machine

little_endian
VERSION =

ruby-growl Version

'2.1'
GNR_FORMAT =

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));
"CCnCCa*"
GNN_FORMAT =

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));
"CCnnnnna*"
GROWL_UDP_PORT =

Growl UDP Port

9887
GROWL_PROTOCOL_VERSION =

Growl Protocol Version

1
GROWL_TYPE_REGISTRATION =

Growl Registration Packet Id

0
GROWL_TYPE_NOTIFICATION =

Growl Notification Packet Id

1
STRING_BYTESIZE_METHOD =

String bytesize method – HACK for 1.8.6 support

("".respond_to? :bytesize) ? :bytesize : :length

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host, app_name, all_notifies, default_notifies = nil, password = nil) ⇒ Growl

Creates a new Growl notifier and automatically registers any notifications with the remote machine.

host is the host to contact.

app_name is the name of the application sending the notifications.

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.



317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/ruby-growl.rb', line 317

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

  register
end

Class Method Details

.listObject

List of hosts accessible via dnssd



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ruby-growl.rb', line 143

def self.list
  require 'dnssd'

  growls = []

  DNSSD.browse! '_growl._tcp' do |reply|
    next unless reply.flags.add?

    growls << reply

    break unless reply.flags.more_coming?
  end

  hosts = []

  growls.each do |growl|
    DNSSD.resolve! growl do |reply|
      hosts << reply.target
      break
    end
  end

  hosts.uniq
rescue LoadError
  raise 'you must gem install dnssd'
end

.notify(options) ⇒ Object

Sends a notification using options



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/ruby-growl.rb', line 173

def self.notify options
  message = options[:message]

  unless message then
    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.notify(notify_type, options[:title], message, options[:priority],
           options[:sticky])
end

.process_args(argv) ⇒ Object

Parses argv-style options from ARGV into an options hash



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/ruby-growl.rb', line 194

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,
  }

  opts = OptionParser.new do |o|
    o.program_name = File.basename $0
    o.version = Growl::VERSION
    o.release = nil

    o.banner = "Usage: \#{o.program_name} -H HOSTNAME [options]\n\nWhere possible, growl is compatible with growlnotify's arguments.\n(Except for -p, use --priority)\n\nSynopsis:\necho \\\"message\\\" | growl -H localhost\n\ngrowl -H localhost -m message\n\n"

    o.separator "Options:"

    o.on("-H", "--host HOSTNAME", "Send notifications to HOSTNAME") do |val|
      options[:host] = val
    end

    o.on("-n", "--name [NAME]", "Sending application name",
            "(Defaults to \"ruby-growl\")") do |val|
      options[:name] = val
    end

    o.on("-y", "--type [TYPE]", "Notification type",
            "(Defauts to \"Ruby Growl Notification\")") do |val|
      options[:notify_type] = val
    end

    o.on("-t", "--title [TITLE]", "Notification title") do |val|
      options[:title] = val
    end

    o.on("-m", "--message [MESSAGE]",
         "Send this message instead of reading STDIN") do |val|
      options[:message] = val
    end

    # HACK -p -1 raises
    o.on("--priority [PRIORITY]", Integer,
         "Notification priority",
         "Priority can be between -2 and 2") do |val|
      options[:priority] = val
    end

    o.on("-s", "--[no-]sticky", "Make the notification sticky") do |val|
      options[:sticky] = val
    end

    o.on("-P", "--password [PASSWORD]", "Growl UDP Password") do |val|
      options[:password] = val
    end

    o.on("--list", "List growl hosts using dnssd") do |val|
      options[:list] = true
    end
  end

  opts.parse! argv

  abort opts.to_s unless options[:host] or options[:list]

  options
end

.run(argv = ARGV) ⇒ Object

Command-line interface



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/ruby-growl.rb', line 282

def self.run argv = ARGV
  options = process_args argv

  if options[:list] then
    begin
      puts list
    rescue => e
      raise unless e.message =~ /gem install dnssd/

      abort "#{e.message} to use --list"
    end
    return
  end

  notify options
end

Instance Method Details

#notification_packet(name, title, description, priority, sticky) ⇒ Object

Builds a Growl notification packet



416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/ruby-growl.rb', line 416

def notification_packet(name, title, description, priority, sticky)
  flags = 0
  data = []

  packet = [
    GROWL_PROTOCOL_VERSION,
    GROWL_TYPE_NOTIFICATION,
  ]

  flags = 0
  flags |= ((0x7 & priority) << 1) # 3 bits for priority
  flags |= 1 if sticky # 1 bit for sticky

  packet << flags
  packet << name.send(STRING_BYTESIZE_METHOD)
  packet << title.length
  packet << description.send(STRING_BYTESIZE_METHOD)
  packet << @app_name.send(STRING_BYTESIZE_METHOD)

  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
end

#notify(notify_type, title, message, priority = 0, sticky = false) ⇒ Object

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.



343
344
345
346
347
348
# File 'lib/ruby-growl.rb', line 343

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)
end

#registerObject

Registers the notification types with host.



353
354
355
# File 'lib/ruby-growl.rb', line 353

def register
  send registration_packet
end

#registration_packetObject

Builds a Growl registration packet



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/ruby-growl.rb', line 369

def registration_packet
  length = 0
  data = []
  data_format = ""

  packet = [
    GROWL_PROTOCOL_VERSION,
    GROWL_TYPE_REGISTRATION
  ]

  packet << @app_name.send(STRING_BYTESIZE_METHOD)
  packet << @all_notifies.length
  packet << @default_notifies.length

  data << @app_name
  data_format = "a#{@app_name.send(STRING_BYTESIZE_METHOD)}"

  @all_notifies.each do |notify|
    data << notify.length
    data << notify
    data_format << "na#{notify.length}"
  end

  @default_notifies.each do |notify|
    data << @all_notifies.index(notify) if @all_notifies.include? notify
    data_format << "C"
  end

  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
end

#send(packet) ⇒ Object

Sends a Growl packet



360
361
362
363
364
# File 'lib/ruby-growl.rb', line 360

def send(packet)
  set_sndbuf packet.length
  @socket.send packet, 0
  @socket.flush
end

#set_sndbuf(length) ⇒ Object

Set the size of the send buffer – Is this truly necessary?



456
457
458
# File 'lib/ruby-growl.rb', line 456

def set_sndbuf(length)
  @socket.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDBUF, length
end