paradeiser 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +8 -0
  4. data/Gemfile +6 -0
  5. data/Guardfile +17 -0
  6. data/README.md +76 -187
  7. data/Rakefile +12 -1
  8. data/TODO.md +62 -0
  9. data/VISION.md +474 -0
  10. data/bin/pom +60 -0
  11. data/doc/Paradeiser::Pomodoro_status.svg +50 -0
  12. data/lib/paradeiser.rb +25 -2
  13. data/lib/paradeiser/controllers/controller.rb +30 -0
  14. data/lib/paradeiser/controllers/paradeiser_controller.rb +10 -0
  15. data/lib/paradeiser/controllers/pomodori_controller.rb +38 -0
  16. data/lib/paradeiser/errors.rb +31 -0
  17. data/lib/paradeiser/executor.rb +15 -0
  18. data/lib/paradeiser/models/hook.rb +26 -0
  19. data/lib/paradeiser/models/job.rb +25 -0
  20. data/lib/paradeiser/models/pomodoro.rb +58 -0
  21. data/lib/paradeiser/models/repository.rb +57 -0
  22. data/lib/paradeiser/models/scheduler.rb +43 -0
  23. data/lib/paradeiser/refinements.rb +5 -0
  24. data/lib/paradeiser/router.rb +29 -0
  25. data/lib/paradeiser/version.rb +1 -1
  26. data/lib/paradeiser/view.rb +21 -0
  27. data/lib/paradeiser/views/paradeiser/init.erb +1 -0
  28. data/lib/paradeiser/views/pomodori/finish.erb +1 -0
  29. data/lib/paradeiser/views/pomodori/report.erb +5 -0
  30. data/lib/paradeiser/views/pomodori/start.erb +1 -0
  31. data/lib/paradeiser/views/pomodori/status.erb +9 -0
  32. data/paradeiser.gemspec +21 -4
  33. data/templates/linux/hooks/after-finish +10 -0
  34. data/templates/linux/hooks/after-start +7 -0
  35. data/templates/mac/hooks/after-finish +10 -0
  36. data/templates/mac/hooks/after-start +7 -0
  37. data/test/helper.rb +24 -0
  38. data/test/integration/test_pom.rb +17 -0
  39. data/test/lib/assertions.rb +10 -0
  40. data/test/lib/at_mock.rb +6 -0
  41. data/test/lib/options_mock.rb +7 -0
  42. data/test/lib/pomodoro_mock.rb +11 -0
  43. data/test/lib/process_status_mock.rb +2 -0
  44. data/test/lib/pstore_mock.rb +16 -0
  45. data/test/templates/hooks/pre-finish +1 -0
  46. data/test/unit/test_paradeiser_controller.rb +88 -0
  47. data/test/unit/test_pomodori_controller.rb +103 -0
  48. data/test/unit/test_pomodori_view.rb +78 -0
  49. data/test/unit/test_pomodoro.rb +100 -0
  50. data/test/unit/test_pomodoro_hooks.rb +83 -0
  51. data/test/unit/test_repository.rb +127 -0
  52. data/test/unit/test_router.rb +36 -0
  53. data/test/unit/test_scheduler.rb +44 -0
  54. metadata +244 -13
data/bin/pom ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler'
4
+ Bundler.require
5
+
6
+ require 'commander/import'
7
+ require 'paradeiser'
8
+ include Paradeiser
9
+
10
+ program :version, Paradeiser::VERSION
11
+ program :description, "Paradeiser is a command-line tool for the Pomodoro Technique. It keeps track of the current pomodoro and assists the user in managing active and past pomodori as well as breaks. Status commands and reports are provided to get insights."
12
+ program :help, 'Author', 'Nicholas E. Rabenau <nerab@gmx.at>'
13
+
14
+ global_option '-V', '--verbose', 'Enable verbose output'
15
+ default_command :help
16
+
17
+ router = Router.new
18
+
19
+ begin
20
+ command :init do |c|
21
+ c.syntax = "#{program(:name)} #{c.name}"
22
+ c.summary = 'Init paradeiser'
23
+ c.action router.dispatch(c)
24
+ end
25
+
26
+ command :start do |c|
27
+ c.syntax = "#{program(:name)} #{c.name}"
28
+ c.summary = 'Start a new pomodoro'
29
+ c.action router.dispatch(c)
30
+ end
31
+
32
+ command :finish do |c|
33
+ c.syntax = "#{program(:name)} #{c.name}"
34
+ c.summary = 'Finish the active pomodoro'
35
+ c.action router.dispatch(c)
36
+ end
37
+
38
+ command :report do |c|
39
+ c.syntax = "#{program(:name)} #{c.name}"
40
+ c.summary = 'Show a report on all pomodori'
41
+ c.action router.dispatch(c)
42
+ end
43
+
44
+ command :status do |c|
45
+ c.syntax = "#{program(:name)} #{c.name}"
46
+ c.summary = 'Show status of active pomodoro or break'
47
+ c.option '--quiet', 'Be quiet - no output is printed. The exit code reflects the status.'
48
+ c.action router.dispatch(c)
49
+ end
50
+ rescue
51
+ $stderr.puts("Error: #{$!.message}")
52
+ $stderr.puts $!.backtrace if options.trace
53
+ exit(1)
54
+ end
55
+
56
+ # We replace Commander's exit hook with our own in order to set a custom exit status.
57
+ at_exit do
58
+ run!
59
+ exit(router.status)
60
+ end
@@ -0,0 +1,50 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
3
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+ <!-- Generated by graphviz version 2.30.1 (20130609.1303)
5
+ -->
6
+ <!-- Title: G Pages: 1 -->
7
+ <svg width="408pt" height="88pt"
8
+ viewBox="0.00 0.00 408.00 88.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
9
+ <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 84)">
10
+ <title>G</title>
11
+ <polygon fill="white" stroke="white" points="-4,5 -4,-84 405,-84 405,5 -4,5"/>
12
+ <!-- idle -->
13
+ <g id="node1" class="node"><title>idle</title>
14
+ <ellipse fill="none" stroke="black" cx="78" cy="-40" rx="36" ry="36"/>
15
+ <text text-anchor="middle" x="78" y="-34.0493" font-family="Arial" font-size="14.00">idle</text>
16
+ </g>
17
+ <!-- active -->
18
+ <g id="node3" class="node"><title>active</title>
19
+ <ellipse fill="none" stroke="black" cx="214" cy="-40" rx="36" ry="36"/>
20
+ <text text-anchor="middle" x="214" y="-34.0493" font-family="Arial" font-size="14.00">active</text>
21
+ </g>
22
+ <!-- idle&#45;&gt;active -->
23
+ <g id="edge2" class="edge"><title>idle&#45;&gt;active</title>
24
+ <path fill="none" stroke="black" d="M114.207,-40C130.389,-40 149.793,-40 167.103,-40"/>
25
+ <polygon fill="black" stroke="black" points="167.563,-43.5001 177.563,-40 167.563,-36.5001 167.563,-43.5001"/>
26
+ <text text-anchor="middle" x="146" y="-42.0986" font-family="Arial" font-size="14.00">start</text>
27
+ </g>
28
+ <!-- starting_state -->
29
+ <g id="node2" class="node"><title>starting_state</title>
30
+ <ellipse fill="black" stroke="black" cx="2" cy="-40" rx="1.8" ry="1.8"/>
31
+ </g>
32
+ <!-- starting_state&#45;&gt;idle -->
33
+ <g id="edge1" class="edge"><title>starting_state&#45;&gt;idle</title>
34
+ <path fill="none" stroke="black" d="M3.83105,-40C6.96908,-40 18.5857,-40 31.6682,-40"/>
35
+ <polygon fill="black" stroke="black" points="31.8421,-43.5001 41.8421,-40 31.8421,-36.5001 31.8421,-43.5001"/>
36
+ </g>
37
+ <!-- finished -->
38
+ <g id="node4" class="node"><title>finished</title>
39
+ <ellipse fill="none" stroke="black" cx="360" cy="-40" rx="36" ry="36"/>
40
+ <ellipse fill="none" stroke="black" cx="360" cy="-40" rx="40" ry="40"/>
41
+ <text text-anchor="middle" x="360" y="-34.0493" font-family="Arial" font-size="14.00">finished</text>
42
+ </g>
43
+ <!-- active&#45;&gt;finished -->
44
+ <g id="edge3" class="edge"><title>active&#45;&gt;finished</title>
45
+ <path fill="none" stroke="black" d="M250.226,-40C268.083,-40 290.064,-40 309.582,-40"/>
46
+ <polygon fill="black" stroke="black" points="309.829,-43.5001 319.829,-40 309.829,-36.5001 309.829,-43.5001"/>
47
+ <text text-anchor="middle" x="285" y="-42.0986" font-family="Arial" font-size="14.00">finish</text>
48
+ </g>
49
+ </g>
50
+ </svg>
@@ -1,5 +1,28 @@
1
- require "paradeiser/version"
1
+ require 'pstore'
2
+ require 'erb'
3
+ require 'state_machine'
4
+
5
+ require 'require_all'
6
+ require_rel "paradeiser"
2
7
 
3
8
  module Paradeiser
4
- # Your code goes here...
9
+ def self.pom_dir
10
+ ENV['POM_DIR'] || File.expand_path('~/.paradeiser/')
11
+ end
12
+
13
+ def self.hooks_dir
14
+ File.join(Paradeiser.pom_dir, 'hooks')
15
+ end
16
+
17
+ def self.templates_dir
18
+ File.join(File.dirname(__FILE__), '..', 'templates')
19
+ end
20
+
21
+ def self.os
22
+ case RUBY_PLATFORM
23
+ when /darwin/ then :mac
24
+ when /linux/ then :linux
25
+ else :other
26
+ end
27
+ end
5
28
  end
@@ -0,0 +1,30 @@
1
+ module Paradeiser
2
+ class Controller
3
+ attr_reader :exitstatus, :has_output
4
+
5
+ def initialize(method)
6
+ @method = method
7
+ @exitstatus = 0
8
+ @has_output = false
9
+ end
10
+
11
+ def call(args, options)
12
+ @args = args
13
+ @options = options
14
+ send(@method)
15
+ end
16
+
17
+ def model
18
+ self.class.name.split("::").last.sub('Controller', '')
19
+ end
20
+
21
+ def get_binding
22
+ return binding
23
+ end
24
+
25
+ protected
26
+
27
+ attr_writer :exitstatus, :has_output
28
+ attr_reader :options, :args
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ require 'fileutils'
2
+
3
+ module Paradeiser
4
+ class ParadeiserController < Controller
5
+ def init
6
+ FileUtils.mkdir_p(Paradeiser.pom_dir)
7
+ FileUtils.cp_r(File.join(Paradeiser.templates_dir, Paradeiser.os.to_s, 'hooks'), Paradeiser.pom_dir)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,38 @@
1
+ module Paradeiser
2
+ class PomodoriController < Controller
3
+ def start
4
+ # The repository will protect itself, but we don't want to create
5
+ # a new pomodoro if saving it will fail anyway.
6
+ raise SingletonError.new(Repository.active) if Repository.active?
7
+
8
+ @pom = Pomodoro.new
9
+ @pom.start!
10
+ Repository.save(@pom)
11
+ end
12
+
13
+ def finish
14
+ @pom = Repository.active
15
+ raise NoActivePomodoroError unless @pom
16
+ @pom.finish!
17
+ Repository.save(@pom)
18
+ end
19
+
20
+ def report
21
+ @pom = Repository.all
22
+ self.has_output = true
23
+ end
24
+
25
+ def status
26
+ if @pom = Repository.active
27
+ self.exitstatus = 0
28
+ elsif @pom = Repository.find{|pom| pom.finished?}.last
29
+ self.exitstatus = 1
30
+ # elsif Repository.find(:status => 'cancelled').last
31
+ # self.exitstatus = 2
32
+ else
33
+ @pom = Pomodoro.new # nothing found, we are idle
34
+ end
35
+ self.has_output = true
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ module Paradeiser
2
+ class SingletonError < StandardError
3
+ def initialize(pom)
4
+ super("Pomodoro #{pom.id} is already active.")
5
+ end
6
+ end
7
+
8
+ class NoActivePomodoroError < StandardError
9
+ def initialize
10
+ super('There is no active pomodoro.')
11
+ end
12
+ end
13
+
14
+ class NotInitializedError < StandardError
15
+ def initialize(msg)
16
+ super("Paradeiser was not properly initialized; #{msg}.")
17
+ end
18
+ end
19
+
20
+ class IllegalStatusError < StandardError
21
+ def initialize
22
+ super('Idle pomodori cannot be saved.')
23
+ end
24
+ end
25
+
26
+ class HookFailedError < StandardError
27
+ def initialize(hook, out, err, status)
28
+ super("The hook #{hook} failed with status #{status.exitstatus}. STDERR contained: #{err}")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ module Executor
2
+ def exec(cmd)
3
+ out, err, status = Open3.capture3(cmd)
4
+ raise err if 0 < status.exitstatus
5
+ [out, err]
6
+ end
7
+
8
+ def at
9
+ 'at'
10
+ end
11
+
12
+ def queue
13
+ 'p'
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ module Paradeiser
2
+ class Hook
3
+ def initialize(phase)
4
+ @phase = phase
5
+ end
6
+
7
+ def execute(pom, transition)
8
+ name = "#{@phase}-#{transition.event}"
9
+ hook = hook(name)
10
+
11
+ if File.exist?(hook) && File.executable?(hook)
12
+ ENV['POM_ID'] = pom.id.to_s
13
+ ENV['POM_STARTED_AT'] = pom.started_at.strftime('%H:%M') if pom.started_at
14
+
15
+ out, err, status = Open3.capture3(hook)
16
+ raise HookFailedError.new(hook, out, err, status) if 0 != status.exitstatus
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def hook(name)
23
+ File.join(Paradeiser.hooks_dir, name)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Paradeiser
2
+ class Job
3
+ include Executor
4
+
5
+ attr_reader :id
6
+
7
+ JOB_PATTERN = %r{^pom .+$}
8
+
9
+ def initialize(id)
10
+ @id = id
11
+ end
12
+
13
+ def ==(other)
14
+ id == other.id
15
+ end
16
+
17
+ def ours?
18
+ exec("#{at} -c #{@id}")[-2, 2].each do |line|
19
+ if line
20
+ return true if line.chomp.match(JOB_PATTERN)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,58 @@
1
+ module Paradeiser
2
+ class Pomodoro
3
+ LENGTH_SECONDS = 25 * 60
4
+
5
+ attr_accessor :id, :started_at, :finished_at
6
+
7
+ state_machine :status, :initial => :idle do
8
+ event :start do
9
+ transition :idle => :active
10
+ end
11
+
12
+ event :finish do
13
+ transition :active => :finished
14
+ end
15
+
16
+ state :finished
17
+ state :active
18
+ state :idle
19
+
20
+ after_transition :on => :start do |pom, transition|
21
+ pom.started_at = Time.now
22
+ Scheduler.clear # There must be no other jobs scheduled because of Rule #1
23
+ Scheduler.add(:finish, LENGTH_SECONDS.minutes)
24
+ end
25
+
26
+ around_transition do |pom, transition, block|
27
+ Hook.new(:before).execute(pom, transition)
28
+ block.call
29
+ Hook.new(:after).execute(pom, transition)
30
+ end
31
+
32
+ after_transition :on => :finish do |pom, transition|
33
+ pom.finished_at = Time.now
34
+ Scheduler.clear # There must be no other jobs scheduled because of Rule #1
35
+ end
36
+ end
37
+
38
+ def initialize
39
+ super # required for state_machine
40
+ end
41
+
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
51
+ end
52
+
53
+ def remaining
54
+ raise NoActivePomodoroError if !active?
55
+ LENGTH_SECONDS - Time.now.to_i + started_at.to_i
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,57 @@
1
+ module Paradeiser
2
+ class Repository
3
+ class << self
4
+ def all
5
+ backend.transaction(true) do
6
+ backend.roots.map{|id| backend[id]}
7
+ end
8
+ end
9
+
10
+ def any?(&blk)
11
+ all.any?(&blk)
12
+ end
13
+
14
+ def find(&blk)
15
+ all.select(&blk)
16
+ end
17
+
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
22
+ end
23
+
24
+ def active?
25
+ !!active
26
+ end
27
+
28
+ def save(pom)
29
+ raise IllegalStatusError if pom.idle?
30
+ raise SingletonError.new(self.active) if self.active? && active.id != pom.id
31
+
32
+ pom.id = next_id if pom.new?
33
+ backend.transaction do
34
+ backend[pom.id] = pom
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def backend
41
+ begin
42
+ @backend ||= PStore.new(File.join(Paradeiser.pom_dir, 'repository.pstore'), true)
43
+ rescue PStore::Error => e
44
+ raise NotInitializedError.new(e.message)
45
+ end
46
+ 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
+ end
56
+ end
57
+ end