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
@@ -0,0 +1,43 @@
1
+ require 'open3'
2
+
3
+ module Paradeiser
4
+ class Scheduler
5
+ class << self
6
+ include Executor
7
+
8
+ def list
9
+ out, _ = exec("#{at} -l -q #{queue}")
10
+
11
+ out.lines.map do |line|
12
+ id = parse_list(line)
13
+ Job.new(id)
14
+ end.select do |job|
15
+ job.ours?
16
+ end
17
+ end
18
+
19
+ def add(command, minutes)
20
+ _, err = exec("echo pom #{command} | #{at} -q #{queue} now + #{minutes} minutes")
21
+ id = parse_add(err.chomp)
22
+ Job.new(id)
23
+ end
24
+
25
+ def clear
26
+ if list.any? # On Linux, at must not be called with an empty job list.
27
+ job_ids = list.map{|j| j.id}.join(' ')
28
+ exec("#{at} -q #{queue} -r #{job_ids}")
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def parse_list(line)
35
+ line[/^(\d+)/]
36
+ end
37
+
38
+ def parse_add(line)
39
+ line.match(/^job (?<job>\d+)/)[:job]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ class Numeric
2
+ def minutes
3
+ self / 60
4
+ end
5
+ end
@@ -0,0 +1,29 @@
1
+ module Paradeiser
2
+ class Router
3
+ attr_reader :status
4
+
5
+ def initialize
6
+ @status = 0
7
+ end
8
+
9
+ def dispatch(command)
10
+ Proc.new do |args, options|
11
+ method = command.name
12
+
13
+ # TODO Dynamically find the controller that handles the method. :pomodoro is the default.
14
+ if (:init == method.to_sym)
15
+ controller_class = ParadeiserController
16
+ else
17
+ controller_class = PomodoriController
18
+ end
19
+
20
+ controller = controller_class.new(method)
21
+ controller.call(args, options)
22
+
23
+ View.new(controller.model, method).render(controller.get_binding) if options.verbose || controller.has_output
24
+
25
+ @status = controller.exitstatus
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,3 @@
1
1
  module Paradeiser
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,21 @@
1
+ module Paradeiser
2
+ class View
3
+ def initialize(model, method)
4
+ @model, @method = model, method
5
+ end
6
+
7
+ def render(controller_binding)
8
+ puts(template.result(controller_binding))
9
+ end
10
+
11
+ private
12
+
13
+ def template
14
+ ERB.new(File.read(template_file), 0, '%<>')
15
+ end
16
+
17
+ def template_file
18
+ File.join(File.dirname(__FILE__), 'views', @model.downcase, "#{@method}.erb")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1 @@
1
+ Suffessfully initialized <%= Paradeiser.pom_dir %>.
@@ -0,0 +1 @@
1
+ Finished pomodoro #<%= @pom.id %> after <%= @pom.duration.minutes %> minutes.
@@ -0,0 +1,5 @@
1
+ ID | Status | Started | Ended
2
+ <% @pom.each do |pom| %>
3
+ <%= pom.id %> | <%= pom.status %> | <%= pom.started_at.strftime('%R') %> | <%= pom.finished_at.strftime('%R') if pom.finished? %>
4
+
5
+ <% end %>
@@ -0,0 +1 @@
1
+ Starting pomodoro #<%= @pom.id %>.
@@ -0,0 +1,9 @@
1
+ <% case @pom.status_name
2
+ when :active
3
+ %>Pomodoro #<%= @pom.id %> is active for another <%= @pom.remaining.minutes %> minutes (started at <%= @pom.started_at.strftime('%R') %>).<%
4
+ when :finished
5
+ %>No active pomodoro. Last one was finished at <%= @pom.finished_at.strftime('%R') %>.<%
6
+ else
7
+ %>Current state is <%= @pom.status %>.<%
8
+ end
9
+ %>
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Paradeiser::VERSION
9
9
  spec.authors = ["Nicholas E. Rabenau"]
10
10
  spec.email = ["nerab@gmx.net"]
11
- spec.description = %q{Paradeiser is a tool for the [Pomodoro Technique](http://www.pomodorotechnique.com/). It keeps track of the current pomodoro and assists the user in managing active and past pomodori.}
12
- spec.summary = %q{Paradeiser is a tool for the Pomodoro Technique}
11
+ spec.description = %q{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
+ spec.summary = %q{Command-line tool for the Pomodoro Technique}
13
13
  spec.homepage = "https://github.com/nerab/paradeiser"
14
14
  spec.license = "MIT"
15
15
 
@@ -18,6 +18,23 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
21
+ spec.add_runtime_dependency 'commander'
22
+ spec.add_runtime_dependency 'require_all'
23
+ spec.add_runtime_dependency 'state_machine'
24
+
25
+ # Moved to the Gemfile so that Travis CI can load the test group
26
+ # spec.add_development_dependency 'rake'
27
+ # spec.add_development_dependency 'minitest'
28
+
29
+ spec.add_development_dependency 'guard-minitest'
30
+ spec.add_development_dependency 'guard-bundler'
31
+ spec.add_development_dependency 'terminal-notifier-guard'
32
+ spec.add_development_dependency 'libnotify'
33
+ spec.add_development_dependency 'rb-inotify'
34
+ spec.add_development_dependency 'rb-fsevent'
35
+ spec.add_development_dependency 'pry'
36
+ spec.add_development_dependency 'pry-nav'
37
+ spec.add_development_dependency 'pry-stack_explorer'
38
+ spec.add_development_dependency 'bundler'
39
+ spec.add_development_dependency 'ruby-graphviz'
23
40
  end
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample post-finish hook for Paradeiser (called when a pomodoro ended). It
5
+ # displays a simple notification.
6
+ #
7
+ # $POM_ID - the ID of the pomodoro that just ended
8
+ # $POM_STARTED_AT - the time when the pomodoro was started
9
+ #
10
+ notify-send "Pomodoro" "The Pomodoro $POM_ID (started at $POM_STARTED_AT) is over." -u critical > /dev/null
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample post-finish 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 post-finish hook for Paradeiser (called when a pomodoro ended). It
5
+ # displays a simple notification.
6
+ #
7
+ # $POM_ID - the ID of the pomodoro that just ended
8
+ # $POM_STARTED_AT - the time when the pomodoro was started
9
+ #
10
+ terminal-notifier-success -message "The Pomodoro $POM_ID (started at $POM_STARTED_AT) is over." > /dev/null
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Sample post-finish 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
@@ -0,0 +1,24 @@
1
+ require 'minitest/autorun'
2
+ require 'paradeiser'
3
+ require_rel 'lib'
4
+
5
+ require 'lib/at_mock'
6
+
7
+ include Paradeiser
8
+
9
+ class MiniTest::Test
10
+
11
+ protected
12
+
13
+ def start!(pom = @pom)
14
+ Scheduler.stub(:add, nil) do
15
+ pom.start!
16
+ end
17
+ end
18
+
19
+ def finish!(pom = @pom)
20
+ Scheduler.stub(:clear, nil) do
21
+ pom.finish!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ require 'helper'
2
+
3
+ class TestPom < MiniTest::Test
4
+ POM_BIN = 'pom'
5
+
6
+ def test_no_args_defaults_to_help
7
+ out, err, status = Open3.capture3(POM_BIN)
8
+ assert_equal(0, status.exitstatus, "Expected exit status to be 0, but it was #{status.exitstatus}. STDERR contains: #{err}")
9
+ assert_empty(err)
10
+ end
11
+
12
+ def test_unknown
13
+ out, err, status = Open3.capture3("#{POM_BIN} unknown")
14
+ refute_equal(0, status.exitstatus)
15
+ refute_empty(err)
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ # from Gem::TestCase rubygems/test_case.rb
2
+ def assert_path_exists(path, msg = nil)
3
+ msg = message(msg){"Expected path '#{path}' to exist"}
4
+ assert(File.exist?(path), msg)
5
+ end
6
+
7
+ def refute_path_exists(path, msg = nil)
8
+ msg = message(msg){"Expected path '#{path}' to NOT exist"}
9
+ refute(File.exist?(path), msg)
10
+ end
@@ -0,0 +1,6 @@
1
+ # Include this module in order to use another queue for at commands
2
+ module Executor
3
+ def queue
4
+ 't'
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ require 'ostruct'
2
+
3
+ class OptionsMock < OpenStruct
4
+ def initialize(attributes)
5
+ super(attributes)
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ require 'ostruct'
2
+
3
+ class PomodoroMock < OpenStruct
4
+ def initialize(attributes)
5
+ super(attributes)
6
+ end
7
+
8
+ def active?
9
+ !!active
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ class ProcessStatusMock < Struct.new(:exitstatus)
2
+ end
@@ -0,0 +1,16 @@
1
+ class PStoreMock
2
+ extend Forwardable
3
+ def_delegators :@hash, :[]=, :[], :size
4
+
5
+ def initialize
6
+ @hash = {}
7
+ end
8
+
9
+ def transaction(read_only = true)
10
+ yield if block_given?
11
+ end
12
+
13
+ def roots
14
+ @hash.keys
15
+ end
16
+ end
@@ -0,0 +1 @@
1
+ echo pre-finish-hook
@@ -0,0 +1,88 @@
1
+ require 'helper'
2
+ require 'fakefs/safe'
3
+
4
+ class TestParadeiserController < MiniTest::Test
5
+ HOOKS = ['after-finish', 'before-finish']
6
+
7
+ def setup
8
+ @orig_pom_dir = ENV['POM_DIR']
9
+ FakeFS.activate!
10
+ create_hook_templates
11
+ end
12
+
13
+ def teardown
14
+ FileUtils.rm_r(Paradeiser.pom_dir, :force => true) if Dir.exists?(Paradeiser.pom_dir)
15
+ FakeFS.deactivate!
16
+ ENV['POM_DIR'] = @orig_pom_dir
17
+ end
18
+
19
+ def test_init_virgin
20
+ ENV.delete('POM_DIR')
21
+ refute(Dir.exists?(Paradeiser.pom_dir), "Expect #{Paradeiser.pom_dir} to not exist yet")
22
+
23
+ ParadeiserController.new(:init).call(nil, nil)
24
+ assert(Dir.exists?(Paradeiser.pom_dir))
25
+ assert_hooks_exist
26
+ end
27
+
28
+ def test_init_existing
29
+ FileUtils.mkdir_p(Paradeiser.pom_dir, 0700)
30
+ assert(Dir.exists?(Paradeiser.pom_dir))
31
+
32
+ ParadeiserController.new(:init).call(nil, nil)
33
+ assert(Dir.exists?(Paradeiser.pom_dir))
34
+ assert_hooks_exist
35
+ end
36
+
37
+ def test_init_virgin_with_env_override
38
+ dir = tempdir_name
39
+ refute_equal(dir, Paradeiser.pom_dir)
40
+ ENV['POM_DIR'] = dir
41
+ assert_equal(dir, Paradeiser.pom_dir)
42
+ refute(Dir.exists?(Paradeiser.pom_dir), "POM_DIR override #{Paradeiser.pom_dir} must not exist")
43
+
44
+ ParadeiserController.new(:init).call(nil, nil)
45
+
46
+ assert(Dir.exists?(Paradeiser.pom_dir))
47
+ assert_hooks_exist
48
+ end
49
+
50
+ def test_init_existing_with_env_override
51
+ ENV.delete('POM_DIR')
52
+
53
+ Dir.mktmpdir do |dir|
54
+ refute_equal(dir, Paradeiser.pom_dir)
55
+ ENV['POM_DIR'] = dir
56
+ assert_equal(dir, Paradeiser.pom_dir)
57
+ assert(Dir.exists?(Paradeiser.pom_dir), "POM_DIR override #{Paradeiser.pom_dir} must exist")
58
+
59
+ ParadeiserController.new(:init).call(nil, nil)
60
+ assert(Dir.exists?(Paradeiser.pom_dir), "POM_DIR override #{Paradeiser.pom_dir} must exist")
61
+ assert_hooks_exist
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ # Makes a temporary directory name, but does not create the directory
68
+ def tempdir_name
69
+ dir = File.join(Dir.tmpdir, SecureRandom.uuid)
70
+ refute(Dir.exists?(dir))
71
+ dir
72
+ end
73
+
74
+ def create_hook_templates
75
+ hook_templates_dir = File.join(Paradeiser.templates_dir, Paradeiser.os.to_s, 'hooks')
76
+ FileUtils.mkdir_p(hook_templates_dir)
77
+
78
+ HOOKS.each do |hook|
79
+ FileUtils.touch(File.join(hook_templates_dir, hook))
80
+ end
81
+ end
82
+
83
+ def assert_hooks_exist
84
+ HOOKS.each do |hook|
85
+ assert(File.exist?(File.join(Paradeiser.hooks_dir, hook)))
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,103 @@
1
+ require 'helper'
2
+
3
+ class TestPomodoriController < MiniTest::Test
4
+ def setup
5
+ @backend = PStoreMock.new
6
+ end
7
+
8
+ def test_start
9
+ pom, has_output = invoke(:start, '@pom', 'has_output')
10
+ assert_equal(:active, pom.status_name)
11
+ assert_equal(false, has_output)
12
+ assert_equal(1, @backend.size)
13
+ end
14
+
15
+ def test_start_active
16
+ invoke(:start)
17
+ assert_equal(1, @backend.size)
18
+
19
+ assert_raises SingletonError do
20
+ invoke(:start)
21
+ end
22
+ assert_equal(1, @backend.size)
23
+ end
24
+
25
+ def test_finish
26
+ invoke(:start)
27
+ pom, has_output = invoke(:finish, '@pom', 'has_output')
28
+ assert_equal(:finished, pom.status_name)
29
+ assert_equal(false, has_output)
30
+ assert_equal(1, @backend.size)
31
+ end
32
+
33
+ def test_finish_idle
34
+ assert_raises NoActivePomodoroError do
35
+ invoke(:finish)
36
+ end
37
+ assert_equal(0, @backend.size)
38
+ end
39
+
40
+ def test_report_idle
41
+ pomodori, has_output = invoke(:report, '@pom', 'has_output')
42
+ assert_empty(pomodori)
43
+ assert_equal(true, has_output)
44
+ end
45
+
46
+ def test_report_active
47
+ invoke(:start)
48
+ pomodori, has_output = invoke(:report, '@pom', 'has_output')
49
+ assert_equal(1, pomodori.size)
50
+ assert_equal(true, has_output)
51
+ end
52
+
53
+ def test_report_finished
54
+ invoke(:start)
55
+ invoke(:finish)
56
+ invoke(:start)
57
+ pomodori, has_output = invoke(:report, '@pom', 'has_output')
58
+ assert_equal(2, pomodori.size)
59
+ assert_equal(true, has_output)
60
+ end
61
+
62
+ def test_status_idle
63
+ pom, has_output = invoke(:status, '@pom', 'has_output')
64
+ assert_equal(:idle, pom.status_name)
65
+ assert_equal(true, has_output)
66
+ assert_equal(0, @backend.size)
67
+ end
68
+
69
+ def test_status_active
70
+ invoke(:start)
71
+ pom, has_output = invoke(:status, '@pom', 'has_output')
72
+ assert_equal(:active, pom.status_name)
73
+ assert_equal(true, has_output)
74
+ assert_equal(1, @backend.size)
75
+ end
76
+
77
+ def test_status_finished
78
+ invoke(:start)
79
+ invoke(:finish)
80
+ pom, has_output = invoke(:status, '@pom', 'has_output')
81
+ assert_equal(:finished, pom.status_name)
82
+ assert_equal(true, has_output)
83
+ assert_equal(1, @backend.size)
84
+ end
85
+
86
+ private
87
+
88
+ def invoke(method, *attributes)
89
+ controller = PomodoriController.new(method)
90
+
91
+ Repository.stub :backend, @backend do
92
+ Scheduler.stub(:add, nil) do
93
+ Scheduler.stub(:clear, nil) do
94
+ controller.call(nil, nil)
95
+ end
96
+ end
97
+ end
98
+
99
+ attributes.map do |attribute|
100
+ controller.get_binding.eval(attribute)
101
+ end
102
+ end
103
+ end