Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

merged content of macruby-0.4 branch into master. Old master going to…

… be renamed xcode
  • Loading branch information...
commit 759cc80fd2ad4793865c110649ce35127c6199f7 1 parent b1a0a3c
@reborg authored
Showing with 13,503 additions and 0 deletions.
  1. +7 −0 .gitignore
  2. +17 −0 README.rdoc
  3. +22 −0 Rakefile
  4. +9 −0 config/build.yml
  5. +14 −0 knownissues.todo
  6. +13 −0 lib/main.rb
  7. +36 −0 lib/menu.rb
  8. +3 −0  lib/pomodori.rb
  9. +28 −0 lib/pomodori/controllers/charts_controller.rb
  10. +18 −0 lib/pomodori/controllers/history_controller.rb
  11. +33 −0 lib/pomodori/controllers/modal_button_controller.rb
  12. +77 −0 lib/pomodori/controllers/pomodori_controller.rb
  13. +96 −0 lib/pomodori/controllers/timer_controller.rb
  14. +30 −0 lib/pomodori/countdown.rb
  15. +21 −0 lib/pomodori/extensions/timestamp.rb
  16. +55 −0 lib/pomodori/kirby_storage.rb
  17. +32 −0 lib/pomodori/migration.rb
  18. +68 −0 lib/pomodori/models/chart.rb
  19. +17 −0 lib/pomodori/models/pomodoro.rb
  20. +39 −0 lib/pomodori/models/pomodoro_count_by_day.rb
  21. +26 −0 lib/pomodori/models/pomodoros_by_tag.rb
  22. +22 −0 lib/pomodori/storage.rb
  23. +85 −0 lib/pomodori/views/chart_view.rb
  24. +137 −0 lib/pomodori/views/history_view.rb
  25. +167 −0 lib/pomodori/views/main_view.rb
  26. +104 −0 lib/pomodori/views/summary_widget.rb
  27. +3,896 −0 lib/thirdparties/kirbybase.rb
  28. BIN  resources/bell-high.aif
  29. BIN  resources/bell.aif
  30. +98 −0 resources/charts/summary_template.html
  31. BIN  resources/pomodori.icns
  32. BIN  resources/pomodori75.gif
  33. +3 −0  script/console
  34. +4 −0 script/init.rb
  35. +1 −0  test/all_tests.rb
  36. +23 −0 test/pomodori/controllers/charts_controller_test.rb
  37. +23 −0 test/pomodori/controllers/history_controller_test.rb
  38. +38 −0 test/pomodori/controllers/modal_button_controller_test.rb
  39. +54 −0 test/pomodori/controllers/pomodori_controller_test.rb
  40. +86 −0 test/pomodori/controllers/timer_controller_test.rb
  41. +47 −0 test/pomodori/countdown_test.rb
  42. +31 −0 test/pomodori/exensions/timestamp_test.rb
  43. +72 −0 test/pomodori/kirby_storage_test.rb
  44. +49 −0 test/pomodori/migration_test.rb
  45. +16 −0 test/pomodori/models/another_template.html
  46. +53 −0 test/pomodori/models/chart_test.rb
  47. +35 −0 test/pomodori/models/pomodoro_count_by_day_test.rb
  48. +27 −0 test/pomodori/models/pomodoro_test.rb
  49. +28 −0 test/pomodori/models/pomodoros_by_tag_test.rb
  50. +1 −0  test/pomodori/models/summary_template.html
  51. +33 −0 test/pomodori/pomodoro_test_data.txt
  52. +55 −0 test/pomodori/storage_test.rb
  53. +38 −0 test/pomodori/views/chart_view_test.rb
  54. +57 −0 test/pomodori/views/history_view_test.rb
  55. +80 −0 test/pomodori/views/main_view_test.rb
  56. +37 −0 test/pomodori/views/summary_widget_test.rb
  57. +48 −0 test/test_helper.rb
  58. +1 −0  test/work/.gitignore
  59. +1 −0  vendor/hotcocoa-0.5.1+patch
  60. +5 −0 vendor/mhennemeyer-matchy-0.3.3/History.txt
  61. +20 −0 vendor/mhennemeyer-matchy-0.3.3/License.txt
  62. +25 −0 vendor/mhennemeyer-matchy-0.3.3/Manifest.txt
  63. +7 −0 vendor/mhennemeyer-matchy-0.3.3/PostInstall.txt
  64. +118 −0 vendor/mhennemeyer-matchy-0.3.3/README.rdoc
  65. +4 −0 vendor/mhennemeyer-matchy-0.3.3/Rakefile
  66. +73 −0 vendor/mhennemeyer-matchy-0.3.3/config/hoe.rb
  67. +15 −0 vendor/mhennemeyer-matchy-0.3.3/config/requirements.rb
  68. +56 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy.rb
  69. +31 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/built_in/change_expectations.rb
  70. +41 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/built_in/enumerable_expectations.rb
  71. +66 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/built_in/error_expectations.rb
  72. +42 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/built_in/operator_expectations.rb
  73. +146 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/built_in/truth_expectations.rb
  74. +10 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/def_matcher.rb
  75. +9 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/expectation_builder.rb
  76. +46 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/matcher_builder.rb
  77. +34 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/modals.rb
  78. +9 −0 vendor/mhennemeyer-matchy-0.3.3/lib/matchy/version.rb
  79. +53 −0 vendor/mhennemeyer-matchy-0.3.3/matchy.gemspec
  80. +1,585 −0 vendor/mhennemeyer-matchy-0.3.3/setup.rb
  81. +63 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_change_expectation.rb
  82. +100 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_def_matcher.rb
  83. +91 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_enumerable_expectations.rb
  84. +144 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_error_expectations.rb
  85. +28 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_expectation_builder.rb
  86. +72 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_matcher_builder.rb
  87. +25 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_minitest_compatibility.rb
  88. +39 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_modals.rb
  89. +157 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_operator_expectations.rb
  90. +373 −0 vendor/mhennemeyer-matchy-0.3.3/test/test_truth_expectations.rb
  91. +52 −0 vendor/microspec/lib/microspec.rb
  92. +3 −0  vendor/mocha-0.9.5/COPYING
  93. +7 −0 vendor/mocha-0.9.5/MIT-LICENSE
  94. +37 −0 vendor/mocha-0.9.5/README
  95. +269 −0 vendor/mocha-0.9.5/RELEASE
  96. +207 −0 vendor/mocha-0.9.5/Rakefile
  97. +49 −0 vendor/mocha-0.9.5/lib/mocha.rb
  98. +55 −0 vendor/mocha-0.9.5/lib/mocha/any_instance_method.rb
  99. +21 −0 vendor/mocha-0.9.5/lib/mocha/argument_iterator.rb
  100. +17 −0 vendor/mocha-0.9.5/lib/mocha/backtrace_filter.rb
  101. +95 −0 vendor/mocha-0.9.5/lib/mocha/cardinality.rb
  102. +27 −0 vendor/mocha-0.9.5/lib/mocha/central.rb
  103. +19 −0 vendor/mocha-0.9.5/lib/mocha/change_state_side_effect.rb
  104. +87 −0 vendor/mocha-0.9.5/lib/mocha/class_method.rb
  105. +60 −0 vendor/mocha-0.9.5/lib/mocha/configuration.rb
  106. +22 −0 vendor/mocha-0.9.5/lib/mocha/deprecation.rb
  107. +17 −0 vendor/mocha-0.9.5/lib/mocha/exception_raiser.rb
  108. +476 −0 vendor/mocha-0.9.5/lib/mocha/expectation.rb
  109. +15 −0 vendor/mocha-0.9.5/lib/mocha/expectation_error.rb
  110. +50 −0 vendor/mocha-0.9.5/lib/mocha/expectation_list.rb
  111. +19 −0 vendor/mocha-0.9.5/lib/mocha/in_state_ordering_constraint.rb
  112. +67 −0 vendor/mocha-0.9.5/lib/mocha/inspect.rb
  113. +16 −0 vendor/mocha-0.9.5/lib/mocha/instance_method.rb
  114. +9 −0 vendor/mocha-0.9.5/lib/mocha/is_a.rb
  115. +15 −0 vendor/mocha-0.9.5/lib/mocha/logger.rb
  116. +13 −0 vendor/mocha-0.9.5/lib/mocha/metaclass.rb
  117. +21 −0 vendor/mocha-0.9.5/lib/mocha/method_matcher.rb
  118. +50 −0 vendor/mocha-0.9.5/lib/mocha/mini_test_adapter.rb
  119. +200 −0 vendor/mocha-0.9.5/lib/mocha/mock.rb
  120. +181 −0 vendor/mocha-0.9.5/lib/mocha/mockery.rb
  121. +16 −0 vendor/mocha-0.9.5/lib/mocha/module_method.rb
  122. +20 −0 vendor/mocha-0.9.5/lib/mocha/multiple_yields.rb
  123. +53 −0 vendor/mocha-0.9.5/lib/mocha/names.rb
  124. +11 −0 vendor/mocha-0.9.5/lib/mocha/no_yields.rb
  125. +187 −0 vendor/mocha-0.9.5/lib/mocha/object.rb
  126. +27 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers.rb
  127. +42 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/all_of.rb
  128. +47 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/any_of.rb
  129. +40 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/any_parameters.rb
  130. +33 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/anything.rb
  131. +15 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/base.rb
  132. +42 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/equals.rb
  133. +45 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/has_entries.rb
  134. +56 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/has_entry.rb
  135. +42 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/has_key.rb
  136. +42 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/has_value.rb
  137. +40 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/includes.rb
  138. +42 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/instance_of.rb
  139. +42 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/is_a.rb
  140. +42 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/kind_of.rb
  141. +42 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/not.rb
  142. +15 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/object.rb
  143. +55 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/optionally.rb
  144. +43 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/regexp_matches.rb
  145. +43 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/responds_with.rb
  146. +43 −0 vendor/mocha-0.9.5/lib/mocha/parameter_matchers/yaml_equivalent.rb
  147. +37 −0 vendor/mocha-0.9.5/lib/mocha/parameters_matcher.rb
  148. +28 −0 vendor/mocha-0.9.5/lib/mocha/pretty_parameters.rb
  149. +31 −0 vendor/mocha-0.9.5/lib/mocha/return_values.rb
  150. +42 −0 vendor/mocha-0.9.5/lib/mocha/sequence.rb
  151. +17 −0 vendor/mocha-0.9.5/lib/mocha/single_return_value.rb
  152. +18 −0 vendor/mocha-0.9.5/lib/mocha/single_yield.rb
  153. +166 −0 vendor/mocha-0.9.5/lib/mocha/standalone.rb
  154. +91 −0 vendor/mocha-0.9.5/lib/mocha/state_machine.rb
  155. +16 −0 vendor/mocha-0.9.5/lib/mocha/stubbing_error.rb
  156. +103 −0 vendor/mocha-0.9.5/lib/mocha/test_case_adapter.rb
  157. +18 −0 vendor/mocha-0.9.5/lib/mocha/unexpected_invocation.rb
  158. +31 −0 vendor/mocha-0.9.5/lib/mocha/yield_parameters.rb
  159. +2 −0  vendor/mocha-0.9.5/lib/mocha_standalone.rb
  160. +4 −0 vendor/mocha-0.9.5/lib/stubba.rb
View
7 .gitignore
@@ -0,0 +1,7 @@
+tags
+*.log
+.DS_Store
+Pomodori.app
+docs
+website
+*.swp
View
17 README.rdoc
@@ -0,0 +1,17 @@
+= Pomodori
+
+http://reborg.github.com/pomodori/resources/general-view.png
+
+* Homepage: http://reborg.github.com/pomodori
+* Main Tracking: http://github.com/reborg/pomodori/issues (for issues, suggestions, features)
+* Alternative tracking (but github preferred): http://reborg.lighthouseapp.com/projects/25822-pomodori/overview
+* Blog: http://blog.reborg.net
+* Contact: reborg -at- reborg.net
+
+== DESCRIPTION:
+
+Pomodori is a tool based on the pomodoro technique (PT) by Francesco Cirillo only available for Mac Os X. Pomodori measures the pomodoro time and stores pomodoro descriptions. Pomodori has metrics to help you plan future activities and detailed charts. For more information about the technique please visit http://www.pomodorotechnique.com. For release notes, installing and other info visit the home site of the app at http://reborg.github.com/pomodori.
+
+== Pomodoro Talk:
+
+I recorded a Pomodoro Technique introduction talk which is available here: http://www.vimeo.com/4546375. Other resources available at the Pomodori main web site: http://reborg.github.com/pomodori
View
22 Rakefile
@@ -0,0 +1,22 @@
+$LOAD_PATH.unshift(File.dirname(__FILE__) + "/vendor/hotcocoa-0.5.1+patch/lib")
+require 'hotcocoa/application_builder'
+require 'hotcocoa/standard_rake_tasks'
+
+task :default => [:test]
+
+task :test do
+ `macruby test/all_tests.rb`
+end
+
+task :embed => [:deploy] do
+ `rm -rf ./#{AppConfig.name}.app/Contents/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/bin`
+ `rm -rf ./#{AppConfig.name}.app/Contents/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/libmacruby-static.a`
+ `rm -rf ./#{AppConfig.name}.app/Contents/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/ruby/Gems`
+ `rm -rf ./#{AppConfig.name}.app/Contents/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/ruby/1.9.0/rubygems`
+ `rm -rf ./#{AppConfig.name}.app/Contents/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/ruby/1.9.0/irb`
+ `rm -rf ./#{AppConfig.name}.app/Contents/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/ruby/1.9.0/rdoc`
+ `rm -rf ./#{AppConfig.name}.app/Contents/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/share`
+ `rm -rf ./#{AppConfig.name}.app/Contents/Frameworks/MacRuby.framework/Versions/0.5`
+ `find ./#{AppConfig.name}.app/Contents -name "*.rbo" -exec install_name_tool -change /Library/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/libmacruby.dylib @executable_path/../Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/libmacruby.dylib {} \\;`
+ `find ./#{AppConfig.name}.app/Contents -name "*.bundle" -exec install_name_tool -change /Library/Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/libmacruby.dylib @executable_path/../Frameworks/MacRuby.framework/Versions/#{MACRUBY_VERSION}/usr/lib/libmacruby.dylib {} \\;`
+end
View
9 config/build.yml
@@ -0,0 +1,9 @@
+name: Pomodori
+load: lib/main.rb
+version: "0.6 © ReBorg reborg@reborg.net"
+icon: resources/Pomodori.icns
+resources:
+ - resources/**/*.*
+sources:
+ - lib/**/*.rb
+ - vendor/hotcocoa-0.5.1+patch/**/*.rb
View
14 knownissues.todo
@@ -0,0 +1,14 @@
+- connect the 'return' key pressed event to the submit button 2 (5) I spent some time on this but with my scarce knowledge of Cocoa I couldn't find a solution. I tried to tweak on a MacRuby example trying to intercept a key pressed event without success. I think it's better I move on hoping that in the future I'll be able to fix this quickly.
+
+- Encoding from text field: the string you obtain from a text_field instance is UTF-16 (text_field.to_s.encoding). UTF-16 is not even in the list of supported Ruby 1.9 encodings (Encoding.list) but there is a BE/LE specification. If you call the match operator on a UTF-16 string you have a "`=~': regexp preprocess failed: too short escaped multibyte character (ArgumentError)". So I had to convert the string and since neither force_encoding nor encode! works in MacRuby, I had to move the string to bytes and back to string. This is not a final solution of course, because I'm stripping relevant part of multibyte strings. The fix is in pomodoros.rb class fix_encoding method.
+
+- Embedding MacRuby: I tried with the install_name_tool
+ -change /Library/Frameworks/MacRuby.framework/Versions/0.4/usr/lib/libmacruby.dylib
+ ../Frameworks/MacRuby.framework/Versions/0.4/usr/lib/libmacruby.dylib
+ Pomodori.app/Contents/MacOS/Pomodori
+which changes the dylib dependencies of the executable. The problem is that it doesn't work with the relative path passed. But if you want to go absolute, then it depend from where the application is installed. If /Applications, I can stuck the absolute path there, but I don't like it. So the plan is to wait some more time and hopefully MacRuby will be part of Leopard snow edition.
+- view_layout: man, was not able to understand how it works or how to use it. Reverted back to absolute position for everything. The main complaint is that sometimes all widget disappears if I try to expand in just one direction. You should probably formulate an example and post it to the ML.
+
+Resizeable Cell in Table View:
+- If one day you want to have the table view to soft-wrap the text in the cell for long pomodoro descriptions you can use this http://evanjones.ca/software/osx-tableview.html as a starting point
+- basically you subclass the table view and intercept the cell draw event to grab cell dimension and wrap the text accordingly
View
13 lib/main.rb
@@ -0,0 +1,13 @@
+require File.dirname(__FILE__) + "/../vendor/hotcocoa-0.5.1+patch/lib/hotcocoa"
+require File.dirname(__FILE__) + '/pomodori'
+require 'pomodori/migration'
+require 'pomodori/views/main_view'
+
+Migration.init_db
+
+include HotCocoa
+application do |app|
+ main_view = MainView.new
+ main_view.render.will_close {exit}
+ main_view.running_mode
+end
View
36 lib/menu.rb
@@ -0,0 +1,36 @@
+module HotCocoa
+ def application_menu
+ menu do |main|
+ main.submenu :apple do |apple|
+ apple.item :about, :title => "About Pomodori"
+ # apple.separator
+ # apple.item :preferences, :key => ","
+ apple.separator
+ apple.submenu :services
+ apple.separator
+ apple.item :hide, :title => "Hide Pomodori", :key => "h"
+ apple.item :hide_others, :title => "Hide Others", :key => "h", :modifiers => [:command, :alt]
+ apple.item :show_all, :title => "Show All"
+ apple.separator
+ apple.item :quit, :title => "Quit Pomodori", :key => "q"
+ end
+ main.submenu :edit do |edit|
+ edit.item :undo, :key => "z", :modifiers => [:command], :action => "undo:"
+ edit.item :redo, :key => "z", :modifiers => [:command, :shift], :action => "redo:"
+ edit.separator
+ edit.item :cut, :key => "x", :action => "cut:"
+ edit.item :copy, :key => "c", :action => "copy:"
+ edit.item :paste, :key => "v", :action => "paste:"
+ end
+ main.submenu :window do |win|
+ win.item :minimize, :key => "m"
+ win.item :zoom
+ win.separator
+ win.item :bring_all_to_front, :title => "Bring All to Front", :key => "o"
+ end
+ main.submenu :help do |help|
+ help.item :help, :title => "Pomodori Help"
+ end
+ end
+ end
+end
View
3  lib/pomodori.rb
@@ -0,0 +1,3 @@
+$:.unshift(File.dirname(__FILE__)) unless
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
+require "pomodori/extensions/timestamp"
View
28 lib/pomodori/controllers/charts_controller.rb
@@ -0,0 +1,28 @@
+require 'pomodori/models/chart'
+require 'pomodori/views/chart_view'
+
+class ChartsController
+ attr_accessor :chart_view
+
+ def initialize(params = {})
+ @chart_view = params[:chart_view]
+ end
+
+ ##
+ # Returns the local file URL containing the last
+ # generation of the summary chart.
+ #
+ def on_load_view
+ @chart_view.load_chart(Chart.new.process)
+ end
+
+ def on_open_report
+ @chart_view = ChartView.new(:charts_controller => self)
+ @chart_view.render
+ end
+
+ def on_reload_chart
+ on_load_view
+ end
+
+end
View
18 lib/pomodori/controllers/history_controller.rb
@@ -0,0 +1,18 @@
+require 'pomodori/views/history_view'
+require 'pomodori/controllers/history_controller'
+
+class HistoryController
+
+ def initialize(params = {})
+ @history_view = params[:history_view]
+ @pomodori_controller = params[:pomodori_controller] ||= PomodoriController.new
+ end
+
+ def on_open_history(what)
+ @history_view = HistoryView.new(:history_controller => self)
+ @history_view.render
+ @history_view.update_title(what)
+ @history_view.refresh(@pomodori_controller.send(:"#{what}_pomodoros"))
+ end
+
+end
View
33 lib/pomodori/controllers/modal_button_controller.rb
@@ -0,0 +1,33 @@
+class ModalButtonController
+ attr_reader :main_view
+
+ def initialize(opts)
+ @main_view = opts[:main_view]
+ on_click("Void", :on_click_void)
+ end
+
+ def on_click_void(sender)
+ on_click("Restart", :on_click_restart, :break_mode)
+ end
+
+ def on_click_restart(sender)
+ on_click("Void", :on_click_void, :running_mode)
+ end
+
+ def on_click_submit(sender)
+ on_click_void(sender)
+ end
+
+ private
+
+ ##
+ # The mode default to nil? is because I want to send something
+ # that doesn't do anything.
+ #
+ def on_click(label, action, mode = :nil?)
+ @main_view.update_modal_button_label(label)
+ @main_view.update_modal_button_action(method(action))
+ @main_view.send(mode)
+ end
+
+end
View
77 lib/pomodori/controllers/pomodori_controller.rb
@@ -0,0 +1,77 @@
+require 'pomodori/views/main_view'
+require 'pomodori/kirby_storage'
+require 'pomodori/models/pomodoro'
+
+##
+# Pomodoro related operations triggered by
+# the UI
+#
+class PomodoriController
+ attr_accessor :storage
+ attr_accessor :main_view
+
+ ##
+ # Controllers must be initialized with related views.
+ #
+ def initialize(params = {})
+ @main_view = params[:main_view]
+ end
+
+ ##
+ # Creates a new Pomodoro
+ #
+ def create(params)
+ pomodoro = Pomodoro.new(params)
+ storage.save(pomodoro)
+ storage.invalidate_caches
+ end
+
+ ##
+ # Returns the count of Pomodoros stored on
+ # yesterday.
+ #
+ def yesterday_pomodoros
+ storage.find_all_day_before(Pomodoro, Time.now)
+ end
+
+ ##
+ # Returns the count of Pomodoros stored today.
+ #
+ def today_pomodoros
+ storage.find_all_by_date(Pomodoro, Time.now)
+ end
+
+ def storage
+ @storage ||= KirbyStorage.new
+ end
+
+ ##
+ # Calculates the daily average pomodoros. The rounding
+ # is not really important here. I consider only days with more
+ # than 6 pomodoros. If less I don't consider them a real
+ # day of work but more a few tasks on a holiday.
+ # Why 6? 6 can be reasonably an underestimated half a day
+ #
+ def average_pomodoros
+ all = PomodoroCountByDay.find_all
+ sum = 0
+ valid_day = 0
+ all.each do |day|
+ if day.count > 5
+ valid_day+=1
+ sum = sum + day.count
+ end
+ end
+ valid_day > 0 ? sum/valid_day : 0
+ end
+
+ def last_tags
+ last = storage.last(Pomodoro)
+ last.nil? ? [""] : last.tags
+ end
+
+ def total_count
+ storage.find_all(Pomodoro).size
+ end
+
+end
View
96 lib/pomodori/controllers/timer_controller.rb
@@ -0,0 +1,96 @@
+require 'pomodori/countdown'
+
+##
+# FIXME: rename me TimerController and move me to the controller folder
+#
+class TimerController
+ attr_accessor :countdown
+ attr_reader :start_time, :state, :timer
+ attr_reader :main_view
+ include HotCocoa
+
+ POMODORO = 25 * 60
+ BREAK = 5 * 60
+ # POMODORO = 5
+ # BREAK = 3
+
+ RUNNING = :running
+ DONE = :done
+
+ def initialize(options = {})
+ @main_view = options[:main_view]
+ @state = DONE
+ # Had to use NSTimer direct call instead of HotCocoa mapping that is
+ # affected by a crash after the application start. Returns the hotcocoa
+ # rendering
+ @timer = NSTimer.scheduledTimerWithTimeInterval(
+ 1,
+ target:self,
+ selector:'on_timer_tick',
+ userInfo:nil,
+ repeats:true)
+ end
+
+ def time
+ "#{normalize(@countdown.mins)}:#{normalize(@countdown.secs)}"
+ end
+
+ def on_pomodoro_start
+ start_timer(POMODORO, method(:on_pomodoro_done))
+ end
+
+ def on_break_start
+ start_timer(BREAK, method(:on_break_done))
+ end
+
+ def on_timer_tick
+ if(@state == RUNNING)
+ @countdown.tick
+ @main_view.update_timer(time)
+ else
+ @main_view.update_timer("Done!")
+ end
+ end
+
+ def on_pomodoro_done
+ on_timer_done("submit_mode")
+ end
+
+ def on_break_done
+ on_timer_done("running_mode")
+ end
+
+ def ring
+ bell = sound(
+ :file => File.join(NSBundle.mainBundle.resourcePath.fileSystemRepresentation, 'bell.aif'),
+ :by_reference => true)
+ bell.play if bell
+ end
+
+ private
+
+ def normalize(number)
+ return "0#{number}" if number < 10
+ return number
+ end
+
+ ##
+ # Generic call to start the internal timer with the given number
+ # of seconds and an optional callback
+ #
+ def start_timer(seconds, callback = lambda {})
+ @countdown = Countdown.new(seconds, callback)
+ @start_time = Time.now
+ @state = RUNNING
+ end
+
+ ##
+ # Generic callback that redirect the message to the main_window
+ #
+ def on_timer_done(message)
+ @state = DONE
+ ring
+ @main_view.send(message)
+ end
+
+end
View
30 lib/pomodori/countdown.rb
@@ -0,0 +1,30 @@
+##
+# Counts from the given number of seconds down to zero. You can give
+# an optional block to the countdown to call when zero is reached.
+#
+class Countdown
+ attr_accessor :secs, :callback
+
+ def initialize(secs, block = lambda {})
+ @secs = secs
+ @callback = block
+ end
+
+ def mins
+ @secs / 60
+ end
+
+ def secs
+ @secs % 60
+ end
+
+ def tick
+ if @secs > 1
+ @secs -= 1
+ else
+ @secs = 0
+ @callback.call
+ end
+ end
+
+end
View
21 lib/pomodori/extensions/timestamp.rb
@@ -0,0 +1,21 @@
+require 'time'
+
+class String
+
+ TIMESTAMP = /^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\s[-+]\d{4}$/
+
+ def flatten_date
+ return self[0..9].gsub("-", "") if self.timestamp?
+ ""
+ end
+
+ def timestamp?
+ self =~ TIMESTAMP ? true : false
+ end
+
+ def strftime(format)
+ time = Time.parse(self)
+ time.strftime(format)
+ end
+
+end
View
55 lib/pomodori/kirby_storage.rb
@@ -0,0 +1,55 @@
+require 'thirdparties/kirbybase'
+require 'pomodori/models/pomodoro'
+framework 'Foundation'
+
+class KirbyStorage
+ attr_accessor :path, :db
+ @@pomodoros = nil
+
+ DB_PATH = File.expand_path("~/Library/Application Support/Pomodori")
+ SECS_IN_DAY = 60 * 60 * 24
+
+ def initialize(path = DB_PATH)
+ @path = path
+ @db = KirbyBase.new(:local, nil, nil, path)
+ end
+
+ def save(object)
+ table_for(object).insert(:text => object.text, :timestamp => object.timestamp)
+ end
+
+ def find_all(clazz)
+ #start = Time.now
+ @@pomodoros ||= table_for(clazz).select
+ #NSLog(" [PERF]: find_all took '#{Time.now - start}'")
+ @@pomodoros
+ end
+
+ def invalidate_caches
+ @@pomodoros = nil
+ end
+
+ def find_all_day_before(clazz, today)
+ all = find_all_by_date(clazz, today - SECS_IN_DAY)
+ end
+
+ def find_all_by_date(clazz, date)
+ all = table_for(clazz).select { |r| r.timestamp.flatten_date == date.to_s.flatten_date }
+ end
+
+ def last(clazz)
+ table = table_for(clazz)
+ last_id = @db.engine.get_header_vars(table)[1]
+ fields = table[last_id]
+ Pomodoro.new(:text => fields[:text], :timestamp => fields[:timestamp]) if fields
+ end
+
+ private
+
+ def table_for(object)
+ sym = object.class.to_s.downcase.to_sym
+ sym = object.to_s.downcase.to_sym if object.class == Class
+ @db.get_table(sym)
+ end
+
+end
View
32 lib/pomodori/migration.rb
@@ -0,0 +1,32 @@
+require 'pomodori/kirby_storage'
+require 'thirdparties/kirbybase'
+
+class Migration
+
+ TIMESTAMP_REGEXP = /\|timestamp:Time/
+
+ def self.migrate(file)
+ content = File.read(file)
+ File.open(file, 'w+') do |f|
+ f << content.gsub(TIMESTAMP_REGEXP, "|timestamp:String")
+ end
+ end
+
+ ##
+ # Database migration lives here. Called by main.rb
+ # at startup.
+ #
+ def self.init_db(path = KirbyStorage::DB_PATH)
+ FileUtils.mkdir(path) unless File.exists?(path)
+ db = KirbyBase.new(:local, nil, nil, path)
+ if db.table_exists?(:pomodoro)
+ migrate(path + "/pomodoro.tbl")
+ else
+ db.create_table(:pomodoro,
+ :text, :String,
+ :timestamp, :String) { |obj| obj.encrypt = false }
+ end
+ db
+ end
+
+end
View
68 lib/pomodori/models/chart.rb
@@ -0,0 +1,68 @@
+require 'fileutils'
+require 'tempfile'
+require 'pomodori/models/pomodoro_count_by_day'
+
+##
+# Chart will initialize on the default path
+# with the summary template as default chart. The actual
+# load is done by load_template with optional overriding
+# path and chart template. The template contains methods
+# call to this class that will expand into HTML for the
+# rendered template. So for example <%= pomodoro_summary %>
+# will call the pomodoro_summary method and use the output
+# in place of the call in the template HTML file.
+#
+class Chart
+
+ attr_reader :template_name, :template_path
+
+ TAG_REGEXP = /\<\%\=(.+?)\%\>/
+ DEFAULT_PATH = File.join(NSBundle.mainBundle.resourcePath.fileSystemRepresentation, "charts")
+
+ def initialize(opts = {})
+ @template_name = opts[:template_name] ||= "summary_template.html"
+ @template_path = opts[:template_path] ||= DEFAULT_PATH
+ end
+
+ def process(path = template_path, name = template_name)
+ processed = ""
+ open(File.join(path, name)).each do |line|
+ processed << process_line(line)
+ end
+ create_temp_file(processed)
+ end
+
+ def extract_tag(line)
+ line.match(TAG_REGEXP)[1].strip if line.match(TAG_REGEXP)
+ end
+
+ def process_line(line)
+ extract_tag(line) ? send(extract_tag(line)) : line
+ end
+
+ ##
+ # Produces HTML relevant to the pomodoro summary
+ # chart template. The % is multiplied by 5 because
+ # I assume there will never be more than 20 pomos a day.
+ #
+ def pomodoro_summary
+ html = ""
+ pomos = PomodoroCountByDay.find_all
+ pomos.each do |pomo|
+ html << "<li>"
+ html << "<span class=\"date\">#{pomo.date.strftime('%Y-%m-%d')}</span>"
+ html << "<span class=\"count\">#{pomo.count}</span>"
+ html << "<span class=\"index\" style=\"width: #{pomo.count * 4}%\"></span>"
+ html << "</li>"
+ end
+ html
+ end
+
+ def create_temp_file(content)
+ out = Tempfile.new("pomodori_chart")
+ out << content
+ out.close
+ out.path
+ end
+
+end
View
17 lib/pomodori/models/pomodoro.rb
@@ -0,0 +1,17 @@
+class Pomodoro
+ attr_accessor :text, :timestamp
+
+ def initialize(params = {})
+ @text = params[:text]
+ @timestamp = params[:timestamp]
+ end
+
+ def timestamp
+ @timestamp ||= Time.now.to_s
+ end
+
+ def tags
+ @text.scan(/@\w+/)
+ end
+
+end
View
39 lib/pomodori/models/pomodoro_count_by_day.rb
@@ -0,0 +1,39 @@
+require 'pomodori/kirby_storage'
+
+class PomodoroCountByDay
+ attr_reader :date, :pomodoros
+ include Comparable
+
+ def initialize(date, pomodoros = [])
+ @date = date
+ @pomodoros = pomodoros
+ end
+
+ def count
+ @pomodoros.size
+ end
+
+ def <<(pomodoro)
+ @pomodoros << pomodoro
+ end
+
+ def <=>(pomodoro_count)
+ date <=> pomodoro_count.date
+ end
+
+ ##
+ # FIXME: filtering logic should go down to kirby_storage
+ # if loading all pomos is overkill.
+ #
+ def self.find_all
+ hash = {}
+ all_pomodoros = KirbyStorage.new.find_all(Pomodoro)
+ all_pomodoros.each do |pomodoro|
+ ts = pomodoro.timestamp.flatten_date
+ hash[ts] = PomodoroCountByDay.new(pomodoro.timestamp) unless hash[ts]
+ hash[ts] << pomodoro
+ end
+ hash.values.sort.reverse
+ end
+
+end
View
26 lib/pomodori/models/pomodoros_by_tag.rb
@@ -0,0 +1,26 @@
+class PomodorosByTag
+
+ def initialize
+ @hash = Hash.new
+ end
+
+ def count(tag)
+ pomodoros_for_tag(tag).size
+ end
+
+ def add(pomodoro)
+ tags = extract_from(pomodoro)
+ tags.each do |tag|
+ pomodoros_for_tag(tag) << pomodoro
+ end
+ end
+
+ def extract_from(pomodoro)
+ ['tag', 'something']
+ end
+
+ def pomodoros_for_tag(tag)
+ @hash[tag] ||= Array.new
+ end
+
+end
View
22 lib/pomodori/storage.rb
@@ -0,0 +1,22 @@
+require 'yaml'
+
+##
+# Old storage mechanism on plain files. Probably removable.
+#
+class Storage
+ attr_accessor :path
+ TIMESTAMP_FORMAT = '%Y%m%d%H%M%S'
+
+ def initialize(path = File.dirname(__FILE__) + "/")
+ @path = path
+ end
+
+ def save(model)
+ open(@path + timestamp, 'w') { |f| YAML.dump(model, f) }
+ end
+
+ def timestamp
+ DateTime.now.strftime(TIMESTAMP_FORMAT)
+ end
+
+end
View
85 lib/pomodori/views/chart_view.rb
@@ -0,0 +1,85 @@
+framework 'WebKit'
+require 'pomodori/controllers/charts_controller'
+require 'hotcocoa'
+
+##
+# UI components for the chart view.
+#
+class ChartView
+ include HotCocoa
+ attr_reader :hc_top_view, :hc_bottom_view, :hc_reload_button
+ attr_reader :hc_close_button, :hc_web_view, :hc_chart_window
+
+ def initialize(opts = {})
+ @charts_controller = opts[:charts_controller]
+ end
+
+ def load_chart(url)
+ hc_web_view.url = url
+ end
+
+ def render
+ hc_chart_window
+ end
+
+ private
+
+ def hc_chart_window
+ @hc_chart_window ||= window(:frame => [100, 100, 500, 400], :title => "Reports") do |win|
+ win << hc_bottom_view
+ win << hc_top_view
+ @charts_controller.on_load_view
+ end
+ end
+
+ def hc_top_view
+ @hc_top_view ||= layout_view(
+ :mode => :horizontal,
+ :layout => {:expand => [:width, :height]},
+ :margin => 0, :spacing => 0) do |view|
+ view << hc_web_view
+ end
+ end
+
+ def hc_bottom_view
+ @hc_bottom_view ||= layout_view(
+ :mode => :horizontal,
+ :margin => 0,
+ :spacing => 4,
+ :layout => {:expand => [:width], :padding => 0},
+ :frame => [0, 0, 0, 30]) do |view|
+ view << hc_reload_button
+ view << hc_close_button
+ end
+ end
+
+ def hc_reload_button
+ @hc_reload_button ||= button(
+ :title => "Reload",
+ :bezel => :textured_square,
+ :frame => [0, 0, 66, 28],
+ :on_action => reload_button_action)
+ end
+
+ def reload_button_action
+ lambda {|sender| @charts_controller.on_reload_chart}
+ end
+
+ def hc_close_button
+ @hc_close_button ||= button(
+ :title => "Close",
+ :bezel => :textured_square,
+ :frame => [0, 0, 66, 28],
+ :on_action => close_window_action)
+ end
+
+ def close_window_action
+ lambda { |sender| hc_chart_window.close }
+ end
+
+ def hc_web_view
+ @hc_web_view ||= web_view(
+ :layout => {:expand => [:width, :height]})
+ end
+
+end
View
137 lib/pomodori/views/history_view.rb
@@ -0,0 +1,137 @@
+class HistoryView
+ include HotCocoa
+
+ DESCR_COLUMN_SIZE = 50
+
+ def initialize(opts = {})
+ end
+
+ def update_title(title)
+ @hc_history_window.title = title.to_s
+ end
+
+ ##
+ # Formats the time for display on the table cell.
+ #
+ def format_time(time)
+ time.strftime('%I:%M%p').downcase
+ end
+
+ ##
+ # It splits a sentece into multiple newline separated lines
+ # so that it fits into a given character lenght
+ # The regular expression is explained here:
+ # http://blog.macromates.com/2006/wrapping-text-with-regular-expressions
+ #
+ def smart_split(text, chars)
+ text.gsub(/(.{1,#{chars}})( +|$\n?)|(.{1,#{chars}})/, "\\1\\3\n").chomp
+ end
+
+
+ ##
+ # Convert an array of pomodoros into an array
+ # of hashified pomodoros
+ #
+ def hashify(pomodoros)
+ return [{:text => "\nThe next pomodoro will go better!", :timestamp => "None"}] unless pomodoros.size > 0
+ pomodoros.inject([]) do |array, pomodoro|
+ array << {
+ :text => smart_split(pomodoro.text, DESCR_COLUMN_SIZE),
+ :timestamp => format_time(pomodoro.timestamp)
+ }
+ end
+ end
+
+ def refresh(pomodoros)
+ hc_table_view.data = hashify(pomodoros)
+ hc_table_view.reloadData
+ end
+
+ def render
+ hc_history_window
+ end
+
+ private
+
+ def hc_history_window
+ @hc_history_window ||= window(
+ :frame => [100, 100, 500, 400],
+ # :style => :borderless,
+ :title => "History") do |win|
+ win << hc_bottom_layout_view
+ win << hc_scroll_view
+ end
+ end
+
+ def hc_bottom_layout_view
+ @hc_bottom_layout_view ||= layout_view(
+ :mode => :horizontal,
+ :frame => [0,0,0,30],
+ :margin => 0,
+ :spacing => 4,
+ :layout => {:expand => [:width], :padding => 0}) do |v|
+ v << hc_close_button
+ end
+ end
+
+ def hc_scroll_view
+ @hc_scroll_view ||= scroll_view(
+ :layout => {:expand => [:width, :height]},
+ :vertical_scroller => true, :horizontal_scroller => false) do |scroll|
+ scroll << hc_table_view
+ end
+ end
+
+ def hc_table_view
+ return @hc_table_view unless @hc_table_view.nil?
+ @hc_table_view = table_view(
+ :columns => [
+ hc_timestamp_column,
+ column(:id => :text, :title => "What")
+ ]
+ )
+ # A single line is 17 pixels by default, these are 3 lines.
+ @hc_table_view.setRowHeight(51)
+ @hc_table_view.setUsesAlternatingRowBackgroundColors(true)
+ @hc_table_view.setHeaderView(nil)
+ @hc_table_view
+ end
+
+ def hc_timestamp_column
+ c = column(:id => :timestamp, :title => "When")
+ c.setMaxWidth(80)
+ c.setMinWidth(80)
+ c.setDataCell(TimeStampCell.new)
+ c
+ end
+
+ def hc_close_button
+ @close_button ||= button(
+ :title => "Close",
+ :bezel => :textured_square,
+ :frame => [0, 0, 66, 28],
+ :on_action => close_window_action)
+ end
+
+ def close_window_action
+ lambda {|sender| hc_history_window.close}
+ end
+
+end
+
+##
+# Thanks!
+# http://everburning.com/news/heating-up-with-hotcocoa-part-iii/
+#
+class TimeStampCell < NSCell
+ def drawInteriorWithFrame(frame, inView:view)
+ NSColor.colorWithCalibratedRed("fa".hex/ 255.0, green:"8c".hex/255.0, blue:"40".hex/255.0, alpha:100).set
+ NSRectFill(frame)
+
+ rank_frame = NSMakeRect(frame.origin.x + (frame.size.width / 2) - 26,
+ frame.origin.y + (frame.size.height / 2) - 6, frame.size.width, 17)
+
+ objectValue.to_s.drawInRect(rank_frame, withAttributes:nil)
+ end
+end
+
View
167 lib/pomodori/views/main_view.rb
@@ -0,0 +1,167 @@
+require 'pomodori/controllers/modal_button_controller'
+require 'pomodori/controllers/timer_controller'
+require 'pomodori/controllers/pomodori_controller'
+require 'pomodori/controllers/charts_controller'
+require 'pomodori/controllers/history_controller'
+require 'pomodori/views/chart_view'
+require 'pomodori/views/summary_widget'
+
+##
+# Main window UI handling
+#
+class MainView
+ include HotCocoa
+
+ def initialize(opts = {})
+ @modal_button_controller = opts[:modal_button_controller] ||= ModalButtonController.new(:main_view => self)
+ @timer_controller = opts[:timer_controller] ||= TimerController.new(:main_view => self)
+ @pomodori_controller = opts[:pomodori_controller] ||= PomodoriController.new(:main_view => self)
+ @charts_controller = opts[:charts_controller] ||= ChartsController.new
+ @history_controller = opts[:history_controller] ||= HistoryController.new
+ end
+
+ def render
+ hc_main_view
+ end
+
+ ##
+ # Change what the timer label is displaying on screen
+ #
+ def update_timer(str)
+ hc_timer_label.text = str
+ end
+
+ ##
+ # Change the main window title
+ #
+ def update_window_title(new_title)
+ hc_main_view.title = new_title
+ end
+
+ ##
+ # Updates the modal button label
+ #
+ def update_modal_button_label(label)
+ hc_modal_button.title = label
+ end
+
+ def update_modal_button_action(action)
+ hc_modal_button.on_action = action
+ end
+
+ ##
+ # This switches the interface to accept a pomodoro
+ #
+ # description to be saved
+ #
+ def submit_mode
+ enable_input_box
+ hc_modal_button.title = "Submit"
+ hc_modal_button.on_action = lambda do |sender|
+ @pomodori_controller.create(:text => hc_pomodoro_input_box.to_s)
+ @modal_button_controller.on_click_submit(sender)
+ end
+ end
+
+ ##
+ # Switches the interface to display statistics and running
+ # the timer for a new pomodoro.
+ #
+ def running_mode
+ update_window_title("#{@pomodori_controller.total_count} pomodoros and counting!")
+ disable_input_box
+ hc_modal_button.title = "Void"
+ hc_modal_button.on_action = @modal_button_controller.method(:on_click_void)
+ @timer_controller.on_pomodoro_start
+ end
+
+ def break_mode
+ update_window_title("Break...")
+ disable_input_box
+ hc_modal_button.title = "Restart"
+ hc_modal_button.on_action = @modal_button_controller.method(:on_click_restart)
+ @timer_controller.on_break_start
+ end
+
+ private
+
+ ##
+ # Hide the input box and show the summary widget
+ #
+ def disable_input_box
+ hc_pomodoro_input_box.setHidden(true)
+ summary_widget.show
+ summary_widget.update_yesterday_count(@pomodori_controller.yesterday_pomodoros.size)
+ summary_widget.update_today_count(@pomodori_controller.today_pomodoros.size)
+ summary_widget.update_average_count(@pomodori_controller.average_pomodoros)
+ end
+
+ ##
+ # Hides the summary widget and shows the input box
+ #
+ def enable_input_box
+ summary_widget.hide
+ hc_pomodoro_input_box.setHidden(false)
+ hc_pomodoro_input_box.text = @pomodori_controller.last_tags.join(" ") + " "
+ end
+
+ def hc_main_view
+ @hc_main_view ||= window(
+ :frame => [0, 0, 389, 140],
+ :title => "Pomodori",
+ :view => :nolayout,
+ :style => [:titled, :closable, :miniaturizable, :textured]) do |win|
+ win << hc_pomodoro_icon
+ win << summary_widget.render
+ win << hc_pomodoro_input_box
+ win << hc_modal_button
+ win << hc_timer_label
+ end
+ end
+
+ def hc_pomodoro_icon
+ @hc_pomodoro_icon ||= image_view(
+ :frame => [20, 60, 75, 75],
+ :file => File.join(NSBundle.mainBundle.resourcePath.fileSystemRepresentation,'pomodori75.gif'))
+ end
+
+ def hc_pomodoro_input_box
+ @hc_pomodoro_input_box ||= text_field(
+ :text => "",
+ :layout => {:start => false},
+ :frame => [115, 60, 250, 62])
+ end
+
+ def summary_widget
+ @summary_widget ||= SummaryWidget.new(
+ :yesterday_count => @pomodori_controller.yesterday_pomodoros,
+ :today_count => @pomodori_controller.today_pomodoros,
+ :average_count => @pomodori_controller.average_pomodoros,
+ :on_click_yesterday => lambda {|sender| @history_controller.on_open_history(:yesterday)},
+ :on_click_today => lambda {|sender| @history_controller.on_open_history(:today)},
+ :on_click_average => open_chart_action)
+ end
+
+ ##
+ # The action is initialized as part of the connection to
+ # the modal_button_controller, no need to do on_action= here
+ #
+ def hc_modal_button
+ @hc_modal_button ||= button(
+ :frame => [264, 12, 100, 28],
+ :bezel => :textured_square,
+ :layout => {:start => false})
+ end
+
+ def hc_timer_label
+ @hc_timer_label ||= label(
+ :frame => [20, 8, 96, 35],
+ :text => "25:00",
+ :font => font(:name => "Monaco", :size => 26))
+ end
+
+ def open_chart_action
+ lambda {|sender| @charts_controller.on_open_report}
+ end
+
+end
View
104 lib/pomodori/views/summary_widget.rb
@@ -0,0 +1,104 @@
+##
+# Creating a view guidelines.
+# - For each HotCocoa element there should be an accessor method
+# - All accessors are private but the view exposes business methods to change things without
+# exposing the internals of the implementation.
+# - The accessor method can lazy initialize based on the need to save the element as
+# part of the instance for later access
+# - Accessors are conventionally pre-fixed with hc to avoid name collision with the HotCocoa
+# helpers (i.e. window, button, label cannot be used as names for accessors)
+# - The constructor usually accepts a hash of controllers instances (dependency injection)
+# which will be used to grab data
+class SummaryWidget
+ include HotCocoa
+
+ def initialize(opts = {})
+ update_yesterday_count(opts[:yesterday_count])
+ update_today_count(opts[:today_count])
+ update_average_count(opts[:average_count])
+ hc_yesterday_count_button.on_action = opts[:on_click_yesterday]
+ hc_today_count_button.on_action = opts[:on_click_today]
+ hc_average_count_button.on_action = opts[:on_click_average]
+ end
+
+ def render
+ rows
+ end
+
+ def update_yesterday_count(count)
+ hc_yesterday_count_button.title = count.to_s
+ end
+
+ def update_today_count(count)
+ hc_today_count_button.title = count.to_s
+ end
+
+ def update_average_count(count)
+ hc_average_count_button.title = count.to_s
+ end
+
+ def show
+ rows.setHidden(false)
+ end
+
+ def hide
+ rows.setHidden(true)
+ end
+
+ private
+
+ def rows
+ @rows ||= layout_view(
+ :mode => :vertical,
+ :margin => 0,
+ :spacing => 0,
+ :layout => {:start => false},
+ :frame => [264, 65, 100, 60]) do |v|
+ v << hc_row_layout_view([0,0,0,20], hc_average_count_button, hc_label("Average:"))
+ v << hc_row_layout_view([0,0,0,20], hc_today_count_button, hc_label("Today:"))
+ v << hc_row_layout_view([0,0,0,20], hc_yesterday_count_button, hc_label("Yesterday:"))
+ end
+ end
+
+ def hc_yesterday_count_button
+ @hc_yesterday_count_button ||= hc_count_button
+ end
+
+ def hc_today_count_button
+ @hc_today_count_button ||= hc_count_button
+ end
+
+ def hc_average_count_button
+ @hc_average_count_button ||= hc_count_button
+ end
+
+ def hc_count_button
+ button(
+ :frame => [0,0,25,20],
+ :bezel => :shadowless_square,
+ :font => font(:name => "Andale Mono", :size => 12),
+ :layout => {:start => false})
+ end
+
+ def hc_row_layout_view(aframe, abutton, alabel)
+ layout_view(
+ :mode => :horizontal,
+ :frame => aframe,
+ :margin => 0,
+ :spacing => 0,
+ :layout => {
+ :expand => [:width],
+ :padding => 0}) do |v|
+ v << abutton
+ v << alabel
+ end
+ end
+
+ def hc_label(text = "")
+ label(
+ :text => text,
+ :layout => {:expand => [:width]},
+ :font => font(:name => "Andale Mono", :size => 12))
+ end
+
+end
View
3,896 lib/thirdparties/kirbybase.rb
3,896 additions, 0 deletions not shown
View
BIN  resources/bell-high.aif
Binary file not shown
View
BIN  resources/bell.aif
Binary file not shown
View
98 resources/charts/summary_template.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>Pomodoro Summary</title>
+ <style type="text/css">
+
+ <!--
+
+ * {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ }
+ body {
+ font-family: Helvetica, Arial, sans-serif;
+ color: #333;
+ }
+ a {
+ color: #2D7BB2;
+ text-decoration: none;
+ font-weight: bold;
+ }
+ a:hover {
+ color: #333;
+ }
+ h2, h3, h4 {
+ clear: both;
+ margin: 0 0 0.6em 0;
+ }
+ h3 {
+ color: #666;
+ }
+ .section {
+ float: left;
+ clear: left;
+ padding: 1em 2em;
+ }
+
+ .chartlist {
+ float: left;
+ border-top: 1px solid #EEE;
+ width: 25em;
+ }
+ .chartlist li {
+ position: relative;
+ display: block;
+ border-bottom: 1px solid #EEE;
+ _zoom: 1;
+ }
+ .chartlist .date {
+ display: block;
+ padding: 0.4em 4.5em 0.4em 0.5em;
+ position: relative;
+ z-index: 2;
+ color: #999;
+ }
+ .chartlist .count {
+ display: block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ margin: 0 0.3em;
+ text-align: right;
+ font-weight: bold;
+ font-size: 0.875em;
+ line-height: 2em;
+ }
+ .chartlist .index {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background: #B8E4F5;
+ text-indent: -9999px;
+ overflow: hidden;
+ line-height: 2em;
+ }
+ .chartlist li:hover {
+ background: #EFEFEF;
+ }
+
+ -->
+
+ </style>
+</head>
+<body>
+ <div class="section">
+ <h3>Pomodoro Count By Day</h3>
+
+ <ul class="chartlist">
+ <%= pomodoro_summary %>
+ </ul>
+ </div>
+</body>
+</html>
View
BIN  resources/pomodori.icns
Binary file not shown
View
BIN  resources/pomodori75.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
3  script/console
@@ -0,0 +1,3 @@
+#!/bin/bash
+echo "Loading Pomodori Interactive Console"
+macirb -f -r script/init.rb --simple-prompt
View
4 script/init.rb
@@ -0,0 +1,4 @@
+require File.dirname(__FILE__) + "/../vendor/hotcocoa-0.5.1+patch/lib/hotcocoa"
+include HotCocoa
+require File.dirname(__FILE__) + '/../lib/pomodori'
+Dir.glob(File.join(File.dirname(__FILE__), '../lib/pomodori/**/*.rb')).each {|f| require f}
View
1  test/all_tests.rb
@@ -0,0 +1 @@
+Dir.glob(File.join(File.dirname(__FILE__), 'pomodori/**/*_test.rb')).each {|f| require f}
View
23 test/pomodori/controllers/charts_controller_test.rb
@@ -0,0 +1,23 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/controllers/charts_controller'
+require 'pomodori/models/chart'
+
+class ChartsControllerTest < Test::Unit::TestCase
+
+ def setup
+ @chart = mock(:process => "/tmp/something.html")
+ Chart.stubs(:new).returns(@chart)
+ @chart_view = mock
+ @charts_controller = ChartsController.new(:chart_view => @chart_view)
+ end
+
+ def test_should_load_default_chart_on_load_view
+ @chart_view.expects(:load_chart).with("/tmp/something.html")
+ @charts_controller.on_load_view
+ end
+
+ def test_should_tell_the_view_to_reload_url
+ test_should_load_default_chart_on_load_view
+ end
+
+end
View
23 test/pomodori/controllers/history_controller_test.rb
@@ -0,0 +1,23 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/controllers/history_controller'
+require 'hotcocoa'
+
+class HistoryControllerTest < Test::Unit::TestCase
+ include HotCocoa
+
+ def setup
+ @pomodoros = pomodoros
+ @history_view = mock(:render => true)
+ @pomodori_controller = mock
+ @history_controller = HistoryController.new(
+ :history_view => @history_view,
+ :pomodori_controller => @pomodori_controller)
+ end
+
+# it "should dispatch on correct method on load view" do
+# @pomodori_controller.expects(:yesterday_pomodoros).returns(@pomodoros)
+# @history_view.expects(:refresh).with(@pomodoros)
+# @history_controller.on_open_history(:yesterday)
+# end
+
+end
View
38 test/pomodori/controllers/modal_button_controller_test.rb
@@ -0,0 +1,38 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/controllers/modal_button_controller'
+require 'pomodori/views/main_view'
+
+class ModalButtonControllerTest < Test::Unit::TestCase
+
+ def setup
+ @main_view = stub_everything
+ @modal_button_controller = ModalButtonController.new(:main_view => @main_view)
+ end
+
+ def test_should_switch_to_break_mode_on_void_click
+ when_goes_into_break_mode(:on_click_void)
+ end
+
+ def test_should_go_running_mode_on_restart
+ @main_view.expects(:running_mode)
+ @main_view.expects(:update_modal_button_action).with do |a_block|
+ assert_match(/on_click_void/, a_block.name)
+ end
+ @main_view.expects(:update_modal_button_label).with("Void")
+ @modal_button_controller.on_click_restart("sender")
+ end
+
+ def test_should_switch_to_break_on_submit
+ when_goes_into_break_mode(:on_click_submit)
+ end
+
+ def when_goes_into_break_mode(action)
+ @main_view.expects(:break_mode)
+ @main_view.expects(:update_modal_button_action).with do |a_block|
+ assert_match(/on_click_restart/, a_block.name)
+ end
+ @main_view.expects(:update_modal_button_label).with("Restart")
+ @modal_button_controller.send(action, "sender")
+ end
+
+end
View
54 test/pomodori/controllers/pomodori_controller_test.rb
@@ -0,0 +1,54 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/models/pomodoro'
+require 'pomodori/kirby_storage'
+require 'pomodori/controllers/pomodori_controller'
+
+class PomodoriControllerTest < Test::Unit::TestCase
+
+ def setup
+ @main_view = mock('main_view')
+ @pomodori_controller = PomodoriController.new(:main_view => @main_view)
+ @storage = stub_everything
+ @pomodori_controller.stubs(:storage => @storage)
+ end
+
+ it "should create a new pomodoro" do
+ storage = mock('storage')
+ ctrl = PomodoriController.new
+ ctrl.storage = storage
+ storage.expects(:save)
+ storage.expects(:invalidate_caches)
+ ctrl.create({:text => "hola"})
+ end
+
+ it "should not fail when no pomodoros exist" do
+ PomodoroCountByDay.stubs(:find_all).returns([])
+ assert_nothing_raised { @pomodori_controller.average_pomodoros }
+ end
+
+ it "should return the average daily pomo" do
+ PomodoroCountByDay.stubs(:find_all).returns(pomodoro_count_by_day_sample)
+ @pomodori_controller.average_pomodoros.should == 7
+ end
+
+ it "retrieves yesterday's pomodoros" do
+ @storage.expects(:find_all_day_before).returns(["a"])
+ @pomodori_controller.yesterday_pomodoros.size.should == 1
+ end
+
+ it "retrieves today's pomodoros" do
+ @storage.expects(:find_all_by_date).returns(["a","W"])
+ @pomodori_controller.today_pomodoros.size.should == 2
+ end
+
+ it 'should retrieve the last saved pomodoro' do
+ @storage.expects(:last).returns(Pomodoro.new(:text => "hey @you"))
+ @pomodori_controller.last_tags[0].should == "@you"
+ end
+
+ it 'retrieves all pomodoros' do
+ @storage.expects(:find_all).returns(pomodoros)
+ @pomodori_controller.total_count.should == 10
+ end
+
+end
View
86 test/pomodori/controllers/timer_controller_test.rb
@@ -0,0 +1,86 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/controllers/timer_controller'
+require 'pomodori/countdown'
+require 'pomodori/views/main_view'
+
+class TimerControllerTest < Test::Unit::TestCase
+ attr_accessor :called
+
+ def setup
+ @main_view = stub_everything
+ @timer_controller = TimerController.new(:main_view => @main_view)
+ end
+
+ def test_should_update_timer_label_on_timer_done
+ @main_view.expects(:update_timer).with("Done!")
+ @timer_controller.on_timer_tick
+ end
+
+ def test_state_is_stopped_after_timers_done
+ @timer_controller.on_timer_tick
+ assert_equal(:done, @timer_controller.state)
+ end
+
+ def test_moves_state_to_running_on_startup
+ @timer_controller.on_pomodoro_start
+ assert_equal(:running, @timer_controller.state)
+ end
+
+ def test_start_time_was_saved
+ @timer_controller.on_pomodoro_start
+ assert_not_nil(@timer_controller.start_time)
+ end
+
+ def test_should_update_timer_on_tick
+ @main_view.expects(:update_timer).with('Done!')
+ @timer_controller.on_timer_tick
+ end
+
+ def test_should_ring
+ sound = mock('sound')
+ @timer_controller.expects(:sound).returns(sound)
+ sound.expects(:play)
+ @timer_controller.ring
+ end
+
+ def test_should_go_submit_on_pomodoro_done
+ @main_view.expects(:submit_mode)
+ @timer_controller.on_pomodoro_done
+ end
+
+ def test_should_go_running_on_break_done
+ @main_view.expects(:running_mode)
+ @timer_controller.on_break_done
+ end
+
+ def test_should_go_running_on_start_timer
+ @timer_controller.send(:start_timer, 1)
+ assert_equal(TimerController::RUNNING, @timer_controller.state)
+ end
+
+ def test_should_initialize_counter
+ @timer_controller.send(:start_timer, 120)
+ assert_equal(2, @timer_controller.countdown.mins)
+ end
+
+ def test_should_set_start_time_on_start_timer
+ @timer_controller.send(:start_timer, 1)
+ assert_in_delta(Time.now.to_f, @timer_controller.start_time.to_f, 0.01)
+ end
+
+ def test_main_view_should_receive_message_on_timer_done
+ @main_view.expects(:message)
+ @timer_controller.send(:on_timer_done, "message")
+ end
+
+ def test_should_format_time
+ @timer_controller.send(:start_timer, 1)
+ assert_equal("00:01", @timer_controller.time)
+ @timer_controller.send(:start_timer, 60)
+ assert_equal("01:00", @timer_controller.time)
+ @timer_controller.send(:start_timer, 60*25-1)
+ assert_equal("24:59", @timer_controller.time)
+ end
+
+end
+
View
47 test/pomodori/countdown_test.rb
@@ -0,0 +1,47 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'pomodori/countdown'
+
+class CountdownTest < Test::Unit::TestCase
+
+ def setup
+ @ctdown = Countdown.new(60*25)
+ end
+
+ def test_should_tell_minutes
+ assert_equal(25, @ctdown.mins)
+ end
+
+ def test_should_tell_seconds
+ assert_equal(0, @ctdown.secs)
+ end
+
+ def test_should_decrease_a_second
+ @ctdown.tick
+ assert_equal(24, @ctdown.mins)
+ assert_equal(59, @ctdown.secs)
+ end
+
+ def test_just_stop_to_zero
+ @ctdown = Countdown.new(1)
+ @ctdown.tick
+ assert_equal(0, @ctdown.mins)
+ assert_equal(0, @ctdown.secs)
+ end
+
+end
+
+class CountdownCallbackTest < Test::Unit::TestCase
+
+ def test_receives_the_callback_at_new
+ ctdown = Countdown.new(2, lambda {})
+ assert_not_nil(ctdown.callback)
+ end
+
+ def test_callback_on_zero
+ called = false
+ ctdown = Countdown.new(1, lambda {called = true})
+ ctdown.tick
+ assert(called)
+ end
+
+end
View
31 test/pomodori/exensions/timestamp_test.rb
@@ -0,0 +1,31 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/extensions/timestamp'
+
+class TimestampTest < Test::Unit::TestCase
+
+ def setup
+ @ts = "2009-05-11 18:06:43 -0500"
+ end
+
+ it "tells if string match time format" do
+ @ts.timestamp?.should be(true)
+ "2009-05-11 AA:06:43 -0500".timestamp?.should be(false)
+ "2009_05_11 11:06:43 -0500".timestamp?.should be(false)
+ end
+
+ it "extracts the date" do
+ @ts.flatten_date.should == "20090511"
+ "2009-02-16 05:25:00 +0100".flatten_date.should == "20090216"
+ end
+
+ it "returns empty string if not ts" do
+ "cips".flatten_date.should == ""
+ end
+
+ it "format the time using same Time convention" do
+ time_regexp = /^\d{2}:\d{2}[ap]m$/
+ @ts.strftime('%I:%M%p').downcase.should =~ time_regexp
+ "2011-01-23 01:24:02 -0700".strftime('%I:%M%p').downcase.should =~ time_regexp
+ end
+
+end
View
72 test/pomodori/kirby_storage_test.rb
@@ -0,0 +1,72 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'thirdparties/kirbybase'
+require 'pomodori/kirby_storage'
+require 'pomodori/migration'
+require 'pomodori/models/pomodoro'
+
+class KirbyStorageTest < Test::Unit::TestCase
+
+ def setup
+ @path = File.dirname(__FILE__) + "/../work"
+ Migration.init_db(@path)
+ @kirby_storage = KirbyStorage.new(@path)
+ end
+
+ it "retrieves table for instances or classes" do
+ @kirby_storage.send(:table_for, Pomodoro.new).should_not be_nil
+ @kirby_storage.send(:table_for, Pomodoro).should_not be_nil
+ end
+
+ it "should convert lowercase" do
+ db = mock('db')
+ @kirby_storage.db = db
+ db.expects(:get_table).at_least(2).with(:pomodoro)
+ @kirby_storage.send(:table_for, Pomodoro)
+ @kirby_storage.send(:table_for, Pomodoro.new)
+ end
+
+ it "returns all pomodori" do
+ bulk_import_test_data
+ @kirby_storage.find_all(Pomodoro).size.should == 32
+ end
+
+ it "returns all pomodoros by date" do
+ bulk_import_test_data
+ date = Time.local(2009, "feb", 16, "5", "25")
+ @kirby_storage.find_all_by_date(Pomodoro, date).size.should == 5
+ end
+
+ it 'should retrieve the last pomodoro' do
+ bulk_import_test_data
+ pomodoro = @kirby_storage.last(Pomodoro)
+ pomodoro.text.should == "@planning just arranged tasks and wrote new tasks."
+ pomodoro.should be_instance_of(Pomodoro)
+ end
+
+ # FIXME: stopped working after 0.6 macruby install,
+ # can't find why
+ #it 'caches pomodoros' do
+ # table = mock()
+ # table.expects(:select).returns(["a", "b"])
+ # @kirby_storage.expects(:table_for).once.returns(table)
+ # @kirby_storage.find_all(Pomodoro)
+ # @kirby_storage.find_all(Pomodoro)
+ #end
+
+ it 'accepts unicode characters' do
+ @kirby_storage.save(Pomodoro.new(:text => "äåö"))
+ @kirby_storage.last(Pomodoro).text.should == "äåö"
+ end
+
+ def teardown
+ @kirby_storage.invalidate_caches
+ #wipe_dir(@path)
+ end
+
+ private
+
+ def bulk_import_test_data
+ `cp #{File.dirname(__FILE__) + '/pomodoro_test_data.txt'} #{@path}/pomodoro.tbl`
+ end
+
+end
View
49 test/pomodori/migration_test.rb
@@ -0,0 +1,49 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require "pomodori/migration"
+
+class MigrationTest < Test::Unit::TestCase
+
+ def setup
+ @migration = Migration
+ @migrate_me = File.dirname(__FILE__) + '/../work/migrate_me.tbl'
+ @do_not_migrate_me = File.dirname(__FILE__) + '/../work/do_not_migrate_me.tbl'
+ create_table(@migrate_me, "Time")
+ create_table(@do_not_migrate_me, "String")
+ end
+
+ it "replaces old timestamp declaration with new string" do
+ Migration.migrate(@migrate_me)
+ open(@migrate_me) do |input|
+ input.read.should_not =~ Migration::TIMESTAMP_REGEXP
+ end
+ end
+
+ it "should not change an already existent migrated tbl" do
+ expected = File.read(@do_not_migrate_me)
+ Migration.migrate(@do_not_migrate_me)
+ File.read(@do_not_migrate_me).should == expected
+ end
+
+ def teardown
+ wipe_dir(File.dirname(__FILE__) + "/../work")
+ end
+
+ def create_table(file, type)
+ File.open(file, 'w+') do |f|
+ f << example_raw_dump.gsub(/thetypehere/, type)
+ end
+ end
+
+ def example_raw_dump
+ <<-MESSAGE
+ 000035|000000|Struct|recno:Integer|text:String|thetypehere:String
+ 1|it works!|2009-02-12 09:57:17 -0600
+ 5|@planning review of the 5 minutes timer technical details|2009-02-12 14:55:00 -0600
+ 6|@keyboardevents searching how to connect return keypressed to action|2009-02-13 10:18:03 -0600
+ 7|@keyboardevents tried to fix the disappeared menu|2009-02-13 10:46:46 -0600
+ 8|@keyboardevents studying what the possibilities are, but the situation is not very good.|2009-02-13 11:21:05 -0600
+ 9|@keyboardevents oriented to try out the NSResponder|2009-02-14 14:50:57 -0600
+ MESSAGE
+ end
+
+end
View
16 test/pomodori/models/another_template.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>Another Test Template</title>
+ <link rel="stylesheet" href="barchart.css" type="text/css">
+</head>
+<body>
+ <div class="section">
+ <ul class="chartlist">
+ <%= pomodoro_summary %>
+ </ul>
+ </div>
+</body>
+</html>
View
53 test/pomodori/models/chart_test.rb
@@ -0,0 +1,53 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/models/chart'
+require 'pomodori/models/pomodoro_count_by_day'
+
+class ChartTest < Test::Unit::TestCase
+
+ def setup
+ @chart = Chart.new(:template_path => File.dirname(__FILE__))
+ PomodoroCountByDay.stubs(:find_all).returns(pomodoro_count_by_day_sample)
+ end
+
+ def test_should_load_deafult
+ assert_match(/Test Summary Template/, open(@chart.process).read)
+ end
+
+ def test_should_load_deifferent_template_file
+ assert_match(/Another Test Template/, open(@chart.process(
+ File.dirname(__FILE__),
+ "another_template.html")).read)
+ end
+
+ def test_extract_tag_content
+ assert_equal("content", @chart.extract_tag("<%= content %>"))
+ end
+
+ def test_call_method_from_tag
+ @chart.expects(:pomodoro_summary).returns("A nice summary")
+ assert_match(/A nice summary/, @chart.process_line("<%= pomodoro_summary %>"))
+ end
+
+ def test_should_return_unaltered_line_if_no_tag
+ assert_match(/no change/, @chart.process_line("no change"))
+ end
+
+ def test_creates_temp_file
+ path = @chart.create_temp_file("content")
+ assert_match("pomodori_chart", path)
+ assert_equal("content", File.open(path).read)
+ end
+
+ def test_generates_summary_html
+ html = @chart.pomodoro_summary
+ assert_match(/<span class="date">2009-03-01<\/span>/, html)
+ assert_match(/<span class="date">2009-03-02<\/span>/, html)
+ end
+
+ def test_template_was_processed
+ @chart.expects(:pomodoro_summary).returns('was_processed')
+ processed_path = @chart.process(File.dirname(__FILE__),"another_template.html")
+ assert_match(/was_processed/, open(processed_path).read)
+ end
+
+end
View
35 test/pomodori/models/pomodoro_count_by_day_test.rb
@@ -0,0 +1,35 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/models/pomodoro_count_by_day'
+require 'pomodori/kirby_storage'
+
+class PomodoroCountByDayTest < Test::Unit::TestCase
+
+ def setup
+ @pcbd = PomodoroCountByDay.new(DateTime.now)
+ @kirby_storage = stub(:find_all => pomodoros)
+ KirbyStorage.stubs(:new).returns(@kirby_storage)
+ end
+
+ def test_should_group_by_day
+ assert_equal(3, PomodoroCountByDay.find_all[0].count)
+ end
+
+ def test_adding_pomodoros_to_the_count
+ @pcbd << "pomodoro"
+ @pcbd << "pomodoro"
+ assert_equal(2, @pcbd.count)
+ end
+
+ it "compares to another PomoCount" do
+ PomodoroCountByDay.include?(Comparable).should be(true)
+ (@pcbd > PomodoroCountByDay.new(DateTime.new(2008))).should be(true)
+ (@pcbd > PomodoroCountByDay.new(DateTime.new((DateTime.now.year + 1)))).should be(false)
+ end
+
+ it "orders pomo count most recent first" do
+ counts = PomodoroCountByDay.find_all
+ assert(counts[0].date > counts[1].date, "First pomo count date should be the most recent")
+ assert(counts[1].date > counts[2].date, "Second pomo count date should be before next one")
+ end
+
+end
View
27 test/pomodori/models/pomodoro_test.rb
@@ -0,0 +1,27 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/models/pomodoro'
+
+class PomodoroTest < Test::Unit::TestCase
+
+ def setup
+ @pomodoro = Pomodoro.new
+ end
+
+ def test_intializes_with_arg_hash
+ now = "2009-05-11 18:06:43 -0500"
+ pomodoro = Pomodoro.new({:text => "hola", :timestamp => now})
+ pomodoro.text.should =~ /hola/
+ assert_equal(now, pomodoro.timestamp)
+ end
+
+ def test_has_default_timestamp
+ assert_not_nil(@pomodoro.timestamp)
+ end
+
+ it 'should extract tags from the description' do
+ pomodoro = Pomodoro.new(:text => "@at @cips urban @nop")
+ pomodoro.tags.size.should == 3
+ pomodoro.tags.should include("@nop")
+ end
+
+end
View
28 test/pomodori/models/pomodoros_by_tag_test.rb
@@ -0,0 +1,28 @@
+require File.dirname(__FILE__) + '/../../test_helper'
+require 'pomodori/models/pomodoros_by_tag'
+require 'pomodori/models/pomodoro'
+
+class PomodorosByTagTest < Test::Unit::TestCase
+
+ def setup
+ @pbt = PomodorosByTag.new
+ end
+
+ def test_should_be_zero
+ actual = @pbt.count('tag')
+ assert_equal(0, actual)
+ end
+
+ def test_should_increment_count
+ actual = @pbt.count('tag')
+ @pbt.add(Pomodoro.new(:text => '@tag something'))
+ @pbt.add(Pomodoro.new(:text => '@tag something'))
+ assert_equal(2, @pbt.count('tag') - actual)
+ end
+
+ def test_should_increment_for_multiple_tags
+ @pbt.add(Pomodoro.new(:text => '@tag @something hello'))
+ assert_equal(@pbt.count('tag'), @pbt.count('something'))
+ end
+
+end
View
1  test/pomodori/models/summary_template.html
@@ -0,0 +1 @@
+Test Summary Template
View
33 test/pomodori/pomodoro_test_data.txt
@@ -0,0 +1,33 @@
+000035|000000|Struct|recno:Integer|text:String|timestamp:String
+1|it works!|2009-02-12 09:57:17 -0600
+2|@macrubyembed First pomodoro with pomodori. I think a good solution would be to execute install_name_tool from Ruby everytime the application start, to change the macruby dylib|2009-02-12 11:05:08 -0600
+3|@macrubyembed I tried without success to change the dylib to a relative path. Abandoning because it looks a time sink.|2009-02-12 11:42:48 -0600
+4|@icon @textmate fixed textmate behavior and resolved the icon problem. The name of the icon must exactly be as the app name.|2009-02-12 14:15:41 -0600
+5|@planning review of the 5 minutes timer technical details|2009-02-12 14:55:00 -0600
+6|@keyboardevents searching how to connect return keypressed to action|2009-02-13 10:18:03 -0600
+7|@keyboardevents tried to fix the disappeared menu|2009-02-13 10:46:46 -0600
+8|@keyboardevents studying what the possibilities are, but the situation is not very good.|2009-02-13 11:21:05 -0600
+9|@keyboardevents oriented to try out the NSResponder|2009-02-14 14:50:57 -0600
+10|@keyboardevents had some learning about delegates in hotcocoa and it looks like that I need to implemennt a new one|2009-02-14 15:21:33 -0600
+11|@keyboardevents had a look at macruby examples to see if I could intercept a key pressed. No success.|2009-02-16 10:53:37 -0600
+12|@5mins moved state changes to the start method in the countdown_field|2009-02-16 11:23:08 -0600
+13|@5mins moved the creation of the timer to the start method as a parameter so the countdown can be re-instantiated again|2009-02-16 15:47:26 -0600
+14|@5mins now starting a new timer of 5 mins. But I need to clean the descr message first.|2009-02-16 16:42:33 -0600
+18|@5mins started another 25 mins countdown after the break|2009-02-16 18:54:50 -0600
+19|@soundalert almost a working spike. Found NSSound to use is mapped in hotcocoa and with OSX::NSBundle I can play a sound from the resource folder|2009-02-17 09:29:17 -0600
+20|@soundalert finished spike successful. Started designing solution for including ring bell.|2009-02-17 10:00:21 -0600
+21|@soundalert installed the ring bell at the end of both 25 and 5 mins|2009-02-17 11:19:19 -0600
+22|@ui created a small spike to test how to change the input box to not editable while the tomato is running|2009-02-17 12:22:11 -0600
+23|@ui disabled the input box when 25 mins are running. Need to re-enable at the end.|2009-02-17 12:53:52 -0600
+24|@ui had thoughts about the need of another callback from the countdown field to the main app controller. This is necessary if I want to enable the txt field after the tomato is done.|2009-02-17 13:19:45 -0600
+25|@ui created another callback mechanism to propagate the end timer event up to the app. I can move the ring up now.|2009-02-17 15:00:32 -0600
+26|@ui done with input enabling disabling. The disbled state is basically a label. I had the idea to display statisticis on the label.|2009-02-17 15:31:41 -0600
+27|@bug just moved the menu file to root to see the menu back. Also fixed icon issue (lowering case)|2009-02-17 16:57:15 -0600
+28|@ui now pomodori initializing at the low left corner so the timer is easily visible.|2009-02-17 17:28:40 -0600
+29|@ui working to disable the input box also when the break is running with a specific message (or stats)|2009-02-18 13:31:04 -0600
+30|@ui now displaying a label when counting the break. Had strange issue when removing left-center alignment|2009-02-18 14:01:48 -0600
+31|@buttonvoid added feature to change the button label and changed button lable to Void during timer|2009-02-18 14:33:10 -0600
+32|@buttonvoid now changing labels based on state. The action is always submit though.|2009-02-18 15:07:32 -0600
+33|@buttonvoid I had this problem with the controller test that suddenly stopped working. Debugged and fixed.|2009-02-18 15:39:27 -0600
+34|@buttonvoid implemented the abort pomodoro action which just shortcut to break. Some appliction test refactoring.|2009-02-18 16:11:21 -0600
+35|@planning just arranged tasks and wrote new tasks.|2009-02-19 09:31:05 -0600
View
55 test/pomodori/storage_test.rb
@@ -0,0 +1,55 @@
+require File.dirname(__FILE__) + '/../test_helper'