tamarillo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +19 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +22 -0
  5. data/README.md +83 -0
  6. data/Rakefile +23 -0
  7. data/bin/tam +4 -0
  8. data/features/config.feature +44 -0
  9. data/features/step_definitions/tamarillo_steps.rb +50 -0
  10. data/features/support/env.rb +5 -0
  11. data/features/tamarillo.feature +47 -0
  12. data/lib/tamarillo/clock.rb +33 -0
  13. data/lib/tamarillo/command.rb +68 -0
  14. data/lib/tamarillo/config.rb +95 -0
  15. data/lib/tamarillo/controller.rb +113 -0
  16. data/lib/tamarillo/monitor.rb +27 -0
  17. data/lib/tamarillo/notification/bell.rb +15 -0
  18. data/lib/tamarillo/notification/growl.rb +13 -0
  19. data/lib/tamarillo/notification/none.rb +13 -0
  20. data/lib/tamarillo/notification/speech.rb +13 -0
  21. data/lib/tamarillo/notification/touch.rb +16 -0
  22. data/lib/tamarillo/notification.rb +36 -0
  23. data/lib/tamarillo/storage.rb +118 -0
  24. data/lib/tamarillo/tomato.rb +96 -0
  25. data/lib/tamarillo/tomato_file.rb +60 -0
  26. data/lib/tamarillo/version.rb +3 -0
  27. data/lib/tamarillo.rb +7 -0
  28. data/spec/lib/tamarillo/clock_spec.rb +39 -0
  29. data/spec/lib/tamarillo/command_spec.rb +17 -0
  30. data/spec/lib/tamarillo/config_spec.rb +133 -0
  31. data/spec/lib/tamarillo/controller_spec.rb +137 -0
  32. data/spec/lib/tamarillo/monitor_spec.rb +27 -0
  33. data/spec/lib/tamarillo/notification/bell_spec.rb +7 -0
  34. data/spec/lib/tamarillo/notification/growl_spec.rb +7 -0
  35. data/spec/lib/tamarillo/notification/speech_spec.rb +7 -0
  36. data/spec/lib/tamarillo/notification_spec.rb +22 -0
  37. data/spec/lib/tamarillo/storage_spec.rb +171 -0
  38. data/spec/lib/tamarillo/tomato_file_spec.rb +41 -0
  39. data/spec/lib/tamarillo/tomato_spec.rb +116 -0
  40. data/spec/support/invalid-config.yml +1 -0
  41. data/spec/support/sample-config.yml +3 -0
  42. data/tamarillo.gemspec +26 -0
  43. metadata +178 -0
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .rbenv-version
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ vendor/bundle
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ # uncomment this line if your project needs to run something other than `rake`:
6
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Tim Uruski
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Tamarillo
2
+
3
+ A command line [pomodoro/tomato](http://www.pomodorotechnique.com/) timer.
4
+
5
+ Currently this will just keep track of whether you are in the middle of
6
+ a tomato. If you are it will display how much time remains. If you
7
+ complete to the tomato without being interrupted, it will log it as
8
+ completed for the day. You can also interrupt a tomato, which will
9
+ freeze it for later analysis.
10
+
11
+ It also makes it easy to includes the current tomato status in your
12
+ prompt. Tomatoes are stored in `~/.tamarillo` by default.
13
+
14
+ [![Build Status](https://secure.travis-ci.org/timuruski/tamarillo.png)](http://travis-ci.org/timuruski/tamarillo)
15
+
16
+
17
+ ## Why Tamarillo?
18
+
19
+ The tamarillo is a cousin to the tomato, which is also related to
20
+ eggplants, potatoes and the deadly nightshade. When tomatoes were
21
+ first introduced to Europe, they were not popular because people
22
+ associated them with the deadly poisons of their cousins.
23
+
24
+ Also I wasn't clever enough to come up with Tomatillo at the time. In
25
+ any case, tamarillos are delicious if you can find them. I recommend
26
+ stewing them and then serving over vanilla ice cream; home made if you
27
+ can.
28
+
29
+
30
+ ## Examples
31
+
32
+ These examples are just thought experiments, this interface has not been
33
+ implemented yet.
34
+
35
+ ### Starting and stopping a tomato
36
+
37
+ ```
38
+ $ tam start
39
+ > tamarillo started
40
+
41
+ $ tam stop
42
+ > tomato stopped around ~17m
43
+
44
+ $ tam pause
45
+ > tomato paused around ~16m
46
+
47
+ $ tam interrupt
48
+ > tomato interrupted around ~14m
49
+ ```
50
+
51
+ ### Status of current tomato
52
+
53
+ ```
54
+ $ tam status
55
+ > ~19m # rough time only, don't sweat the seconds
56
+
57
+ $ tam
58
+ > ~19m
59
+
60
+ $ tam status --full
61
+ > active 19:21
62
+ ```
63
+
64
+ ### Configuration
65
+
66
+ ```
67
+ $ tam config --duration=25
68
+ > tamarillo duration is 25 minutes
69
+
70
+ $ tam config --alert=growl
71
+ > tamarillo will use Growl for notifications
72
+
73
+ $ tam config --daemon ~/.tamarillo/pid
74
+ > tamarillo will monitor the current tomato from here
75
+ ```
76
+
77
+
78
+ ## Future ideas
79
+
80
+ * task management, tomatoes are assigned to a task
81
+ * daemon process for monitoring the current tomato
82
+ * notification helper app for various environments
83
+ * instaweb view of history
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/setup'
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ # require 'cucumber'
6
+ require 'cucumber/rake/task'
7
+
8
+ desc "Interactive pomodoro console"
9
+ task :console do
10
+ exec("irb -Ilib -r'bundler/setup' -r'tamarillo'")
11
+ end
12
+
13
+ RSpec::Core::RakeTask.new(:spec) do |t|
14
+ t.rspec_opts = '--color'
15
+ end
16
+
17
+ Cucumber::Rake::Task.new(:features) do |t|
18
+ tag_opts = ' --tags ~@pending'
19
+ tag_opts = " --tags #{ENV['TAGS']}" if ENV['TAGS']
20
+ t.cucumber_opts = "features --format progress -x -s#{tag_opts}"
21
+ t.fork = false
22
+ end
23
+ task :default => [:spec, :features]
data/bin/tam ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tamarillo/command'
4
+ Tamarillo::Command.new.execute(*ARGV)
@@ -0,0 +1,44 @@
1
+ Feature: configuration
2
+
3
+ Scenario: Outputting the current config
4
+ Given the default configuration
5
+ When I run `tam config`
6
+ Then the exit status should be 0
7
+ Then the output should contain:
8
+ """
9
+ duration: 25
10
+ """
11
+
12
+ Scenario: Setting the tomato duration
13
+ Given the default configuration
14
+ When I run `tam config duration=15`
15
+ And I run `tam config`
16
+ Then the exit status should be 0
17
+ Then the output should contain:
18
+ """
19
+ duration: 15
20
+ """
21
+
22
+ Scenario: Setting invalid duration
23
+ Given the default configuration
24
+ When I run `tam config duration=invalid_input`
25
+ And I run `tam config`
26
+ Then the exit status should be 0
27
+ Then the output should contain:
28
+ """
29
+ duration: 25
30
+ """
31
+
32
+ Scenario: Changing the tomato duration
33
+ Given there is no active tomato
34
+ When I run `tam config duration=5`
35
+ And I run `tam start`
36
+ And I run `tam`
37
+ Then the output should contain:
38
+ """
39
+ About 5 minutes
40
+ """
41
+ And the output should not contain:
42
+ """
43
+ About 25 minutes
44
+ """
@@ -0,0 +1,50 @@
1
+ After do
2
+ # Kill any forked monitor processes.
3
+ storage = Tamarillo::Storage.new(tamarillo_path)
4
+ if monitor_pid = storage.read_monitor
5
+ Process.kill('QUIT', monitor_pid)
6
+ end
7
+ end
8
+
9
+ def tamarillo_path
10
+ Pathname.new("#{current_dir}/tamarillo")
11
+ end
12
+
13
+ def clear_tomatoes
14
+ # Not happy with how specific to implementation this is.
15
+ # Giving the Storage system a way to remove tomatoes might be good.
16
+ in_current_dir do
17
+ year = Time.new.year
18
+ FileUtils.remove_dir("tamarillo/#{year}", :force)
19
+ end
20
+ end
21
+
22
+ Given /^the default configuration$/ do
23
+ Tamarillo::Storage.new(tamarillo_path)
24
+ Tamarillo::Config.new.write(tamarillo_path.join('config.yml'))
25
+ end
26
+
27
+ Given /^there is no active tomato$/ do
28
+ clear_tomatoes
29
+ end
30
+
31
+ Given /^there is an active tomato$/ do
32
+ clock = Tamarillo::Clock.now
33
+ tomato = Tamarillo::Tomato.new(25 * 60, clock)
34
+ storage = Tamarillo::Storage.new(tamarillo_path)
35
+ storage.write_tomato(tomato)
36
+ end
37
+
38
+ Given /^there is a completed tomato$/ do
39
+ clear_tomatoes
40
+ time = Time.now - (25 * 60)
41
+ clock = Tamarillo::Clock.new(time)
42
+ tomato = Tamarillo::Tomato.new(25 * 60, clock)
43
+ storage = Tamarillo::Storage.new(tamarillo_path)
44
+ storage.write_tomato(tomato)
45
+ end
46
+
47
+
48
+ Then /^the output should be empty$/ do
49
+ assert_exact_output('', all_output)
50
+ end
@@ -0,0 +1,5 @@
1
+ require 'bundler/setup'
2
+ require 'aruba/cucumber'
3
+ require 'tamarillo'
4
+
5
+ ENV['TAMARILLO_PATH'] = 'tamarillo'
@@ -0,0 +1,47 @@
1
+ Feature: tamarillo
2
+
3
+ Scenario: First usage
4
+ Given there is no active tomato
5
+ When I run `tam`
6
+ Then the exit status should be 0
7
+ Then the output should be empty
8
+
9
+ Scenario: Starting a tomato
10
+ Given the default configuration
11
+ And there is no active tomato
12
+ When I run `tam start`
13
+ Then the output should match /About \d+ minutes/
14
+ And the exit status should be 0
15
+
16
+ Scenario: Tomato status
17
+ Given there is an active tomato
18
+ When I run `tam`
19
+ Then the output should match /About \d+ minutes/
20
+
21
+ Scenario: Interrupting a tomato
22
+ Given there is an active tomato
23
+ When I run `tam interrupt`
24
+ And I run `tam`
25
+ Then the output should be empty
26
+
27
+ Scenario: A tomato is completed
28
+ Given there is a completed tomato
29
+ When I run `tam`
30
+ Then the output should be empty
31
+
32
+ Scenario: Invalid command
33
+ When I run `tam blah`
34
+ Then the exit status should be 1
35
+ And the output should contain:
36
+ """
37
+ Invalid command 'blah'
38
+ """
39
+
40
+ Scenario: Tomato status for prompt
41
+ Given there is an active tomato
42
+ When I run `tam status --prompt`
43
+ Then the output should match:
44
+ """
45
+ \d\d:\d\d \d{4} \d{4}
46
+ """
47
+
@@ -0,0 +1,33 @@
1
+ require 'date'
2
+
3
+ # Internal: Keeps track of time elapsed.
4
+ #
5
+ # The Clock model reprensents the amount of time elapsed since
6
+ # the time a Pomodoro/Tomato began, typically measured in
7
+ # 15-25 minute blocks.
8
+ #
9
+ module Tamarillo
10
+ class Clock
11
+ attr_reader :start_time, :start_date
12
+
13
+ # Public: Initialize a new clock.
14
+ #
15
+ # start_time - A Time instance when the clock was started.
16
+ def initialize(start_time)
17
+ @start_time = start_time
18
+ @start_date = start_time.to_date
19
+ end
20
+
21
+ # Public: Returns a clock starting at the current time.
22
+ def self.now
23
+ new(Time.now)
24
+ end
25
+
26
+ # Public: Calculate time elapsed.
27
+ #
28
+ # Returns the number of seconds since the clock started.
29
+ def elapsed
30
+ Time.now.to_i - @start_time.to_i
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,68 @@
1
+ require 'tamarillo'
2
+
3
+ module Tamarillo
4
+ class Command
5
+ DEFAULT_PATH = '~/.tamarillo'
6
+ DEFAULT_COMMAND = 'status'
7
+
8
+ VALID_COMMANDS = %w[status config start stop interrupt]
9
+
10
+ def initialize
11
+ config = Tamarillo::Config.load(config_path)
12
+ storage = Storage.new(tamarillo_path, config)
13
+ @controller = Controller.new(config, storage)
14
+ end
15
+
16
+ def execute(*args)
17
+ command = parse_command_name!(args)
18
+ send(command.to_sym, *args.drop(1))
19
+ end
20
+
21
+ def status(*args)
22
+ format = Formats::HUMAN
23
+ format = Formats::PROMPT if args.include?('--prompt')
24
+
25
+ status = @controller.status(format)
26
+ puts status unless status.nil?
27
+ end
28
+
29
+ def start(*args)
30
+ tomato = @controller.start_new_tomato
31
+ status unless tomato.nil?
32
+ end
33
+
34
+ def interrupt(*args)
35
+ @controller.interrupt_current_tomato
36
+ end
37
+ alias :stop :interrupt
38
+
39
+ def config(*args)
40
+ params = Hash[args.map { |pair| pair.split('=', 2) }]
41
+ @controller.update_config(params)
42
+ puts @controller.config.to_yaml
43
+ end
44
+
45
+ private
46
+
47
+ def parse_command_name!(args)
48
+ name = args.first || DEFAULT_COMMAND
49
+ unless VALID_COMMANDS.include?(name)
50
+ puts "Invalid command '#{name}'"
51
+ exit 1
52
+ end
53
+
54
+ name
55
+ end
56
+
57
+ def tamarillo_path
58
+ path = ENV['TAMARILLO_PATH'] || DEFAULT_PATH
59
+ Pathname.new(File.expand_path(path))
60
+ end
61
+
62
+ def config_path
63
+ tamarillo_path.join('config.yml')
64
+ end
65
+
66
+ end
67
+ end
68
+
@@ -0,0 +1,95 @@
1
+ require 'yaml'
2
+ require 'tamarillo/notification'
3
+
4
+ module Tamarillo
5
+ # Internal: This configures the Tamarillo CLI, which uses values from
6
+ # this config when generating and interacting with Tomatoes. Currently
7
+ # it just holds the duration of each tomato, but it will later
8
+ # configure things like daemon process location and storage interface.
9
+ class Config
10
+ DEFAULT_DURATION_IN_MINUTES = 25
11
+
12
+ # Public: Gets/Sets the duration of each Tomato in minutes.
13
+ attr_accessor :duration_in_minutes
14
+
15
+ # Public: Initializes a new config.
16
+ #
17
+ # options - The hash used to configure Tamarillo.
18
+ # :duration_in_minutes - The duration in minutes.
19
+ def initialize(options = {})
20
+ self.duration_in_minutes = options[:duration_in_minutes]
21
+ self.notifier = options[:notifier]
22
+ end
23
+
24
+ # Public: Returns the duration of each tomato in minutes.
25
+ def duration_in_minutes
26
+ @duration_in_minutes
27
+ end
28
+ alias :duration :duration_in_minutes
29
+
30
+ # Public: Sets the duration of each tomato in minutes.
31
+ #
32
+ # Uses the default value if the value is invalid.
33
+ def duration_in_minutes=(value)
34
+ value = value.to_i
35
+ @duration_in_minutes = (value > 0) ? value : DEFAULT_DURATION_IN_MINUTES
36
+ end
37
+ alias :duration= :duration_in_minutes=
38
+
39
+ # Public: Returns the duration of each tomato in seconds.
40
+ def duration_in_seconds
41
+ @duration_in_minutes.to_i * 60
42
+ end
43
+
44
+ # Public: Returns the notifier type.
45
+ def notifier
46
+ @notifier
47
+ end
48
+
49
+ # Public: Sets the notifier type.
50
+ def notifier=(value)
51
+ new_value = Notification.valid!(value)
52
+ new_value ||= @notifier
53
+ new_value ||= Notification.default
54
+
55
+ @notifier = new_value
56
+ end
57
+
58
+ # Public: Initializes a config from a YAML file.
59
+ #
60
+ # This tries to read config data from YAML. If the YAML is missing
61
+ # values or is invalid, defaults are used. If the file doesn't
62
+ # exist, then a default configuration is created.
63
+ #
64
+ # path - String or Pathname to the YAML file.
65
+ #
66
+ # Returns: A config instance.
67
+ def self.load(path)
68
+ options = {}
69
+
70
+ if File.exist?(path)
71
+ yaml = YAML.load( File.read(path.to_s) ) || {}
72
+ options[:duration_in_minutes] = yaml['duration']
73
+ options[:notifier] = yaml['notifier']
74
+ end
75
+
76
+ new(options)
77
+ end
78
+
79
+ # Public: Write a config out to a file.
80
+ #
81
+ # path - a String or Pathname to the destination file.
82
+ def write(path)
83
+ File.open(path, 'w') { |f| f << to_yaml }
84
+ end
85
+
86
+ # Public: Returns config in YAML format.
87
+ def to_yaml
88
+ options = {
89
+ 'duration' => duration_in_minutes,
90
+ 'notifier' => notifier.to_s }
91
+ YAML.dump(options)
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,113 @@
1
+ require 'tamarillo'
2
+
3
+ module Tamarillo
4
+ # Formats for tomatoes
5
+ module Formats
6
+ HUMAN = :human.freeze
7
+ PROMPT = :prompt.freeze
8
+ end
9
+
10
+ # Public: This is intended to provide an environment for tomatoes to
11
+ # work from. It integrates the storage, config and monitor into a
12
+ # single coherant object.
13
+ class Controller
14
+ # Initializes a new controller.
15
+ def initialize(config, storage)
16
+ @config = config
17
+ @storage = storage
18
+ end
19
+
20
+ # Public: Formats and returns the status of the current tomato.
21
+ # Returns nil if no tomato is found.
22
+ def status(format)
23
+ tomato = @storage.latest
24
+ return unless tomato && tomato.active?
25
+
26
+ case format
27
+ when Formats::HUMAN then format_approx_time(tomato.remaining)
28
+ when Formats::PROMPT then format_prompt(tomato)
29
+ else raise "Invalid format"
30
+ end
31
+ end
32
+
33
+ # Public: Starts a new tomato if one is not already running.
34
+ def start_new_tomato
35
+ tomato = @storage.latest
36
+ return if tomato && tomato.active?
37
+
38
+ tomato = Tomato.new(@config.duration_in_seconds, Clock.now)
39
+ @storage.write_tomato(tomato)
40
+ start_monitor(tomato)
41
+
42
+ tomato
43
+ end
44
+
45
+ # Public: Interrupts the current tomato if one is running.
46
+ def interrupt_current_tomato
47
+ stop_monitor
48
+
49
+ tomato = @storage.latest
50
+ return if tomato.nil?
51
+
52
+ tomato.interrupt!
53
+ @storage.write_tomato(tomato)
54
+ end
55
+
56
+ # Public: Returns the current config.
57
+ def config
58
+ @config
59
+ end
60
+
61
+ # Public: Updates the current config.
62
+ def update_config(options = {})
63
+ valid_config_options(options).each do |key,value|
64
+ @config.send("#{key}=".to_sym, value)
65
+ end
66
+
67
+ @storage.write_config
68
+ end
69
+
70
+ private
71
+
72
+ def valid_config_options(options)
73
+ valid_keys = %w[duration notifier]
74
+ options.select { |k,_| valid_keys.include?(k.to_s) }
75
+ end
76
+
77
+ def format_approx_time(time)
78
+ minutes = (time / 60).round
79
+ 'About %d minutes' % minutes
80
+ end
81
+
82
+ def format_time(time)
83
+ minutes = (time / 60).floor
84
+ seconds = time % 60
85
+ "%02d:%02d" % [minutes, seconds]
86
+ end
87
+
88
+ def format_prompt(tomato)
89
+ [format_time(tomato.remaining), tomato.remaining, tomato.duration].join(' ')
90
+ end
91
+
92
+ def start_monitor(tomato)
93
+ notifier = Notification.for(@config.notifier)
94
+ monitor = Monitor.new(tomato, notifier)
95
+ monitor.start
96
+ @storage.write_monitor(monitor)
97
+ end
98
+
99
+ def stop_monitor
100
+ if monitor_pid = @storage.read_monitor
101
+ begin
102
+ Process.kill('QUIT', monitor_pid)
103
+ rescue Errno::ESRCH
104
+ @storage.clear_monitor
105
+ rescue Errno::EPERM
106
+ warn "No privilege to kill monitor process."
107
+ end
108
+ end
109
+ end
110
+
111
+ end
112
+ end
113
+
@@ -0,0 +1,27 @@
1
+ module Tamarillo
2
+ class Monitor
3
+ # The time between checks.
4
+ SLEEP_TIME = 0.3
5
+ attr_reader :pid
6
+
7
+ # Public: Initializes a new monitor.
8
+ def initialize(tomato, notifier)
9
+ @tomato = tomato
10
+ @notifier = notifier
11
+ end
12
+
13
+ # Public: Starts watching a tomato for completion.
14
+ def start
15
+ @pid = fork do
16
+ until @tomato.completed?
17
+ sleep SLEEP_TIME
18
+ end
19
+
20
+ @notifier.call
21
+ end
22
+
23
+ Process.detach(@pid)
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module Tamarillo
2
+ module Notification
3
+ class Bell
4
+ CHIME_COUNT = 3
5
+ CHIME_COMMAND = 'tput bel; sleep 0.2'
6
+
7
+ # Public: executes the notification.
8
+ def call
9
+ cmd = (1..CHIME_COUNT).map { CHIME_COMMAND }.join(';')
10
+ system(cmd)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module Tamarillo
2
+ module Notification
3
+ class Growl
4
+ GROWL_COMMAND = %Q{/usr/bin/env growlnotify --message 'Tomato complete.' --sticky}
5
+
6
+ # Public: executes the notification.
7
+ def call
8
+ system(GROWL_COMMAND)
9
+ end
10
+
11
+ end
12
+ end
13
+ end