paradeiser 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/README.md +96 -35
  4. data/TODO.md +14 -15
  5. data/VISION.md +92 -84
  6. data/bin/{pom → par} +32 -2
  7. data/doc/Paradeiser::Break_status.svg +50 -0
  8. data/doc/Paradeiser::Pomodoro_status.svg +40 -22
  9. data/lib/paradeiser.rb +3 -3
  10. data/lib/paradeiser/controllers/breaks_controller.rb +19 -0
  11. data/lib/paradeiser/controllers/controller.rb +3 -3
  12. data/lib/paradeiser/controllers/paradeiser_controller.rb +13 -2
  13. data/lib/paradeiser/controllers/pomodori_controller.rb +36 -17
  14. data/lib/paradeiser/errors.rb +12 -6
  15. data/lib/paradeiser/executor.rb +2 -0
  16. data/lib/paradeiser/initializers/inflections.rb +4 -0
  17. data/lib/paradeiser/models/break.rb +40 -0
  18. data/lib/paradeiser/models/hook.rb +4 -4
  19. data/lib/paradeiser/models/interrupt.rb +18 -0
  20. data/lib/paradeiser/models/job.rb +1 -1
  21. data/lib/paradeiser/models/pomodoro.rb +30 -22
  22. data/lib/paradeiser/models/repository.rb +26 -13
  23. data/lib/paradeiser/models/scheduled.rb +25 -0
  24. data/lib/paradeiser/models/scheduler.rb +1 -1
  25. data/lib/paradeiser/models/status.rb +21 -0
  26. data/lib/paradeiser/{refinements.rb → refinements/numeric.rb} +4 -0
  27. data/lib/paradeiser/router.rb +12 -7
  28. data/lib/paradeiser/version.rb +1 -1
  29. data/lib/paradeiser/views/paradeiser/init.erb +1 -1
  30. data/lib/paradeiser/views/paradeiser/report.erb +5 -0
  31. data/lib/paradeiser/views/paradeiser/status.erb +13 -0
  32. data/paradeiser.gemspec +2 -0
  33. data/templates/linux/hooks/after-finish-break +10 -0
  34. data/templates/linux/hooks/after-finish-pomodoro +10 -0
  35. data/templates/linux/hooks/after-start-break +7 -0
  36. data/templates/linux/hooks/after-start-pomodoro +7 -0
  37. data/templates/mac/hooks/after-finish-break +10 -0
  38. data/templates/mac/hooks/after-finish-pomodoro +10 -0
  39. data/templates/mac/hooks/after-start-break +7 -0
  40. data/templates/mac/hooks/after-start-pomodoro +7 -0
  41. data/test/helper.rb +37 -4
  42. data/test/integration/{test_pom.rb → test_par.rb} +4 -4
  43. data/test/lib/{pomodoro_mock.rb → schedulable_mock.rb} +9 -1
  44. data/test/unit/test_break.rb +99 -0
  45. data/test/unit/test_break_controller.rb +56 -0
  46. data/test/unit/test_interrupt.rb +36 -0
  47. data/test/unit/test_paradeiser_controller_init.rb +92 -0
  48. data/test/unit/test_paradeiser_controller_report.rb +44 -0
  49. data/test/unit/test_paradeiser_controller_status.rb +70 -0
  50. data/test/unit/test_paradeiser_view.rb +66 -0
  51. data/test/unit/test_pomodori_controller.rb +87 -31
  52. data/test/unit/test_pomodori_view.rb +0 -50
  53. data/test/unit/test_pomodoro.rb +131 -9
  54. data/test/unit/test_pomodoro_hooks.rb +165 -17
  55. data/test/unit/test_repository.rb +38 -15
  56. data/test/unit/test_router.rb +4 -4
  57. data/test/unit/test_status.rb +26 -0
  58. metadata +70 -17
  59. data/lib/paradeiser/views/pomodori/report.erb +0 -5
  60. data/lib/paradeiser/views/pomodori/status.erb +0 -9
  61. data/templates/linux/hooks/after-finish +0 -10
  62. data/templates/linux/hooks/after-start +0 -7
  63. data/templates/mac/hooks/after-finish +0 -10
  64. data/templates/mac/hooks/after-start +0 -7
  65. data/test/unit/test_paradeiser_controller.rb +0 -88
@@ -0,0 +1,18 @@
1
+ module Paradeiser
2
+ class Interrupt
3
+ KNOWN_TYPES = [:internal, :external]
4
+ attr_reader :created_at, :type
5
+
6
+ def initialize(type = nil)
7
+ @type = type || :internal
8
+ raise InvalidTypeError.new(@type, KNOWN_TYPES) unless KNOWN_TYPES.include?(@type)
9
+ @created_at = Time.now
10
+ end
11
+ end
12
+
13
+ class ExternalInterrupt < Interrupt
14
+ def initialize
15
+ super(:external)
16
+ end
17
+ end
18
+ end
@@ -4,7 +4,7 @@ module Paradeiser
4
4
 
5
5
  attr_reader :id
6
6
 
7
- JOB_PATTERN = %r{^pom .+$}
7
+ JOB_PATTERN = %r|^#{BIN_PAR} .+$|
8
8
 
9
9
  def initialize(id)
10
10
  @id = id
@@ -1,32 +1,46 @@
1
1
  module Paradeiser
2
- class Pomodoro
3
- LENGTH_SECONDS = 25 * 60
2
+ class Pomodoro < Scheduled
3
+ attr_reader :interrupts, :interrupt_type
4
+ attr_accessor :canceled_at
4
5
 
5
- attr_accessor :id, :started_at, :finished_at
6
+ MINUTES_25 = 25
6
7
 
7
8
  state_machine :status, :initial => :idle do
8
9
  event :start do
9
10
  transition :idle => :active
10
11
  end
11
12
 
13
+ event :interrupt do
14
+ transition :active => :active
15
+ end
16
+
17
+ event :cancel do
18
+ transition :active => :canceled
19
+ end
20
+
12
21
  event :finish do
13
22
  transition :active => :finished
14
23
  end
15
24
 
16
- state :finished
17
- state :active
18
- state :idle
19
-
20
25
  after_transition :on => :start do |pom, transition|
21
26
  pom.started_at = Time.now
22
27
  Scheduler.clear # There must be no other jobs scheduled because of Rule #1
23
- Scheduler.add(:finish, LENGTH_SECONDS.minutes)
28
+ Scheduler.add(:"#{pom.name} finish", pom.length.minutes)
29
+ end
30
+
31
+ after_transition :on => :interrupt do |pom, transition|
32
+ pom.interrupts << Interrupt.new(pom.interrupt_type)
24
33
  end
25
34
 
26
35
  around_transition do |pom, transition, block|
27
- Hook.new(:before).execute(pom, transition)
36
+ Hook.new(:before).execute(pom, transition.event)
28
37
  block.call
29
- Hook.new(:after).execute(pom, transition)
38
+ Hook.new(:after).execute(pom, transition.event)
39
+ end
40
+
41
+ after_transition :on => :cancel do |pom, transition|
42
+ pom.canceled_at = Time.now
43
+ Scheduler.clear # There must be no other jobs scheduled because of Rule #1
30
44
  end
31
45
 
32
46
  after_transition :on => :finish do |pom, transition|
@@ -37,22 +51,16 @@ module Paradeiser
37
51
 
38
52
  def initialize
39
53
  super # required for state_machine
54
+ @interrupts = []
40
55
  end
41
56
 
42
- def new?
43
- @id.nil?
44
- end
45
-
46
- # from https://github.com/travis-ci/travis/blob/master/lib/travis/client/job.rb
47
- def duration
48
- start = started_at || Time.now
49
- finish = finished_at || Time.now
50
- (finish - start).to_i
57
+ def length
58
+ MINUTES_25 * 60
51
59
  end
52
60
 
53
- def remaining
54
- raise NoActivePomodoroError if !active?
55
- LENGTH_SECONDS - Time.now.to_i + started_at.to_i
61
+ def interrupt!(type = nil)
62
+ @interrupt_type = type
63
+ super
56
64
  end
57
65
  end
58
66
  end
@@ -16,18 +16,31 @@ module Paradeiser
16
16
  end
17
17
 
18
18
  def active
19
- all_active = find{|pom| pom.active?}
20
- raise SingletonError.new(all_active.first) if all_active.size > 1
21
- all_active.first
19
+ all_active = find{|pom| pom.active?}.sort{|a,b| a.started_at <=> b.started_at}
20
+
21
+ # Cannot recover from an internal inconsistency.
22
+ if all_active.size > 1
23
+ raise "The repository was corrupted. There are #{all_active.size} active objects, but only one is allowed to be active."
24
+ end
25
+
26
+ all_active.last
22
27
  end
23
28
 
24
29
  def active?
25
30
  !!active
26
31
  end
27
32
 
33
+ def last_finished
34
+ find{|p| p.finished?}.sort{|a,b| a.started_at <=> b.started_at}.last
35
+ end
36
+
37
+ def last_canceled
38
+ find{|p| p.canceled?}.sort{|a,b| a.started_at <=> b.started_at}.last
39
+ end
40
+
28
41
  def save(pom)
29
42
  raise IllegalStatusError if pom.idle?
30
- raise SingletonError.new(self.active) if self.active? && active.id != pom.id
43
+ raise SingletonError.new(pom.class, self.active, :save) if self.active? && active.id != pom.id
31
44
 
32
45
  pom.id = next_id if pom.new?
33
46
  backend.transaction do
@@ -35,23 +48,23 @@ module Paradeiser
35
48
  end
36
49
  end
37
50
 
51
+ def next_id
52
+ if all.empty?
53
+ 1
54
+ else
55
+ all.max{|a, b| a.id <=> b.id}.id + 1
56
+ end
57
+ end
58
+
38
59
  private
39
60
 
40
61
  def backend
41
62
  begin
42
- @backend ||= PStore.new(File.join(Paradeiser.pom_dir, 'repository.pstore'), true)
63
+ @backend ||= PStore.new(File.join(Paradeiser.par_dir, 'repository.pstore'), true)
43
64
  rescue PStore::Error => e
44
65
  raise NotInitializedError.new(e.message)
45
66
  end
46
67
  end
47
-
48
- def next_id
49
- if all.empty?
50
- 1
51
- else
52
- all.max{|a, b| a.id <=> b.id}.id + 1
53
- end
54
- end
55
68
  end
56
69
  end
57
70
  end
@@ -0,0 +1,25 @@
1
+ module Paradeiser
2
+ class Scheduled
3
+ attr_accessor :id, :started_at, :finished_at
4
+
5
+ def new?
6
+ @id.nil?
7
+ end
8
+
9
+ # from https://github.com/travis-ci/travis/blob/master/lib/travis/client/job.rb
10
+ def duration
11
+ start = started_at || Time.now
12
+ finish = finished_at || (respond_to?(:canceled_at) ? canceled_at : nil) || Time.now
13
+ (finish - start).to_i
14
+ end
15
+
16
+ def remaining
17
+ raise NotActiveError unless active?
18
+ length - Time.now.to_i + started_at.to_i
19
+ end
20
+
21
+ def name
22
+ self.class.name.split("::").last.downcase
23
+ end
24
+ end
25
+ end
@@ -17,7 +17,7 @@ module Paradeiser
17
17
  end
18
18
 
19
19
  def add(command, minutes)
20
- _, err = exec("echo pom #{command} | #{at} -q #{queue} now + #{minutes} minutes")
20
+ _, err = exec("echo #{BIN_PAR} #{command} | #{at} -q #{queue} now + #{minutes} minutes")
21
21
  id = parse_add(err.chomp)
22
22
  Job.new(id)
23
23
  end
@@ -0,0 +1,21 @@
1
+ module Paradeiser
2
+ module Status
3
+
4
+ MAP = {
5
+ 'pomodoro:active' => 0,
6
+ 'pomodoro:finished' => 1,
7
+ 'break:active' => 2,
8
+ 'break:finished' => 3,
9
+ }
10
+
11
+ def self.of(thing)
12
+ thing.nil? ? -1 : MAP[key(thing)]
13
+ end
14
+
15
+ private
16
+
17
+ def self.key(thing)
18
+ "#{thing.name}:#{thing.status}"
19
+ end
20
+ end
21
+ end
@@ -2,4 +2,8 @@ class Numeric
2
2
  def minutes
3
3
  self / 60
4
4
  end
5
+
6
+ def seconds
7
+ self
8
+ end
5
9
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
1
3
  module Paradeiser
2
4
  class Router
3
5
  attr_reader :status
@@ -8,19 +10,22 @@ module Paradeiser
8
10
 
9
11
  def dispatch(command)
10
12
  Proc.new do |args, options|
11
- method = command.name
13
+ parts = command.name.split
14
+ resource = parts.shift
15
+ controller_name = "#{resource.pluralize.capitalize}Controller".to_sym
12
16
 
13
- # TODO Dynamically find the controller that handles the method. :pomodoro is the default.
14
- if (:init == method.to_sym)
15
- controller_class = ParadeiserController
17
+ if Paradeiser.const_defined?(controller_name)
18
+ verb = parts.join
19
+ controller_class = Paradeiser.const_get(controller_name)
16
20
  else
17
- controller_class = PomodoriController
21
+ verb = resource
22
+ controller_class = ParadeiserController
18
23
  end
19
24
 
20
- controller = controller_class.new(method)
25
+ controller = controller_class.new(verb)
21
26
  controller.call(args, options)
22
27
 
23
- View.new(controller.model, method).render(controller.get_binding) if options.verbose || controller.has_output
28
+ View.new(controller.model, verb).render(controller.get_binding) if options.verbose || controller.has_output
24
29
 
25
30
  @status = controller.exitstatus
26
31
  end
@@ -1,3 +1,3 @@
1
1
  module Paradeiser
2
- VERSION = "0.1.0"
2
+ VERSION = '0.2.0'
3
3
  end
@@ -1 +1 @@
1
- Suffessfully initialized <%= Paradeiser.pom_dir %>.
1
+ Suffessfully initialized <%= Paradeiser.par_dir %>.
@@ -0,0 +1,5 @@
1
+ ID | Name | Status | Started | Ended | Interrupts
2
+ <% @pom.each do |pom| %>
3
+ <% interrupts_by_type = pom.interrupts.group_by{|i| i.type } %>
4
+ <%= pom.id %> | <%= pom.name %> | <%= pom.status %> | <%= pom.started_at.strftime('%R') %> | <%= pom.finished_at.strftime('%R') if pom.finished? %><%= pom.canceled_at.strftime('%R') if pom.canceled? %> | <%= interrupts[:internal].size rescue 0 %> I, <%= interrupts_by_type[:external].size rescue 0 %> E
5
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <% case @pom.status_name
2
+ when :active
3
+ %><%= @pom.name.capitalize %> #<%= @pom.id %> is active for another <%= @pom.remaining.minutes %> minutes (started at <%= @pom.started_at.strftime('%R') %>).<% unless !@pom.respond_to?(:interrupts) || @pom.interrupts.empty? %> <%= @pom.interrupts.size %> interrupts on record.<% end %><%
4
+ when :break
5
+ %><%= @pom.name.capitalize %> is active for another <%= @pom.remaining.minutes %> minutes.<%
6
+ when :finished
7
+ %>Nothing active. Last <%= @pom.name %> was finished at <%= @pom.finished_at.strftime('%R') %>.<% unless !@pom.respond_to?(:interrupts) || @pom.interrupts.empty? %> It had <%= @pom.interrupts.size %> interrupts.<% end %><%
8
+ when :canceled
9
+ %>Nothing active. Last <%= @pom.name %> was canceled at <%= @pom.canceled_at.strftime('%R') %>.<% unless !@pom.respond_to?(:interrupts) || @pom.interrupts.empty? %> It had <%= @pom.interrupts.size %> interrupts.<% end %><%
10
+ else
11
+ %>Current state is <%= @pom.status %>.<%
12
+ end
13
+ %>
@@ -21,6 +21,8 @@ Gem::Specification.new do |spec|
21
21
  spec.add_runtime_dependency 'commander'
22
22
  spec.add_runtime_dependency 'require_all'
23
23
  spec.add_runtime_dependency 'state_machine'
24
+ spec.add_runtime_dependency 'activesupport'
25
+ spec.add_runtime_dependency 'actionpack'
24
26
 
25
27
  # Moved to the Gemfile so that Travis CI can load the test group
26
28
  # spec.add_development_dependency 'rake'
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample after-finish-break hook for Paradeiser (called after a break ended). It
5
+ # displays a simple notification.
6
+ #
7
+ # $PAR_BREAK_ID - the ID of the break that just ended
8
+ # $PAR_BREAK_STARTED_AT - the time when the break was started
9
+ #
10
+ notify-send "Pomodoro" "The break $PAR_BREAK_ID (started at $PAR_BREAK_STARTED_AT) is over." -u critical > /dev/null
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample after-finish-pomodoro hook for Paradeiser (called after a pomodoro ended). It
5
+ # displays a simple notification.
6
+ #
7
+ # $PAR_POMODORO_ID - the ID of the pomodoro that just ended
8
+ # $PAR_POMODORO_STARTED_AT - the time when the pomodoro was started
9
+ #
10
+ notify-send "Pomodoro" "The pomodoro $PAR_POMODORO_ID (started at $PAR_POMODORO_STARTED_AT) is over." -u critical > /dev/null
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample after-start-break hook for Paradeiser (called after a break started). It
5
+ # displays a simple notification.
6
+ #
7
+ notify-send "Pomodoro" "A new break started." -u critical > /dev/null
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample after-start-pomodoro hook for Paradeiser (called after a pomodoro started). It
5
+ # displays a simple notification.
6
+ #
7
+ notify-send "Pomodoro" "A new pomodoro started." -u critical > /dev/null
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample after-finish-break hook for Paradeiser (called after a break ended). It
5
+ # displays a simple notification.
6
+ #
7
+ # $PAR_BREAK_ID - the ID of the break that just ended
8
+ # $PAR_BREAK_STARTED_AT - the time when the break was started
9
+ #
10
+ terminal-notifier-success -message "The break $PAR_BREAK_ID (started at $PAR_BREAK_STARTED_AT) is over." > /dev/null
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample after-finish-pomodoro hook for Paradeiser (called after a pomodoro ended). It
5
+ # displays a simple notification.
6
+ #
7
+ # $PAR_POMODORO_ID - the ID of the pomodoro that just ended
8
+ # $PAR_POMODORO_STARTED_AT - the time when the pomodoro was started
9
+ #
10
+ terminal-notifier-success -message "The pomodoro $PAR_POMODORO_ID (started at $PAR_POMODORO_STARTED_AT) is over." > /dev/null
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample after-start-break hook for Paradeiser (called after a break started). It
5
+ # displays a simple notification.
6
+ #
7
+ terminal-notifier-success -message "A new break started." > /dev/null
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample after-start-pomodoro hook for Paradeiser (called after a pomodoro started). It
5
+ # displays a simple notification.
6
+ #
7
+ terminal-notifier-success -message "A new pomodoro started." > /dev/null
@@ -10,15 +10,48 @@ class MiniTest::Test
10
10
 
11
11
  protected
12
12
 
13
- def start!(pom = @pom)
13
+ def start!(thing = @pom || @break)
14
14
  Scheduler.stub(:add, nil) do
15
- pom.start!
15
+ thing.start!
16
16
  end
17
17
  end
18
18
 
19
- def finish!(pom = @pom)
19
+ def interrupt!(type = :internal, pom = @pom)
20
20
  Scheduler.stub(:clear, nil) do
21
- pom.finish!
21
+ pom.interrupt!(type)
22
+ end
23
+ end
24
+
25
+ def finish!(thing = @pom || @break)
26
+ Scheduler.stub(:clear, nil) do
27
+ thing.finish!
28
+ end
29
+ end
30
+
31
+ def cancel!(pom = @pom)
32
+ Scheduler.stub(:clear, nil) do
33
+ pom.cancel!
34
+ end
35
+ end
36
+ end
37
+
38
+ class ControllerTest < MiniTest::Test
39
+
40
+ protected
41
+
42
+ def invoke(method, *attributes)
43
+ controller = ParadeiserController.new(method)
44
+
45
+ Repository.stub :backend, @backend do
46
+ Scheduler.stub(:add, nil) do
47
+ Scheduler.stub(:clear, nil) do
48
+ controller.call(nil, nil)
49
+ end
50
+ end
51
+ end
52
+
53
+ attributes.map do |attribute|
54
+ controller.get_binding.eval(attribute)
22
55
  end
23
56
  end
24
57
  end