paradeiser 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.travis.yml +8 -0
- data/Gemfile +6 -0
- data/Guardfile +17 -0
- data/README.md +76 -187
- data/Rakefile +12 -1
- data/TODO.md +62 -0
- data/VISION.md +474 -0
- data/bin/pom +60 -0
- data/doc/Paradeiser::Pomodoro_status.svg +50 -0
- data/lib/paradeiser.rb +25 -2
- data/lib/paradeiser/controllers/controller.rb +30 -0
- data/lib/paradeiser/controllers/paradeiser_controller.rb +10 -0
- data/lib/paradeiser/controllers/pomodori_controller.rb +38 -0
- data/lib/paradeiser/errors.rb +31 -0
- data/lib/paradeiser/executor.rb +15 -0
- data/lib/paradeiser/models/hook.rb +26 -0
- data/lib/paradeiser/models/job.rb +25 -0
- data/lib/paradeiser/models/pomodoro.rb +58 -0
- data/lib/paradeiser/models/repository.rb +57 -0
- data/lib/paradeiser/models/scheduler.rb +43 -0
- data/lib/paradeiser/refinements.rb +5 -0
- data/lib/paradeiser/router.rb +29 -0
- data/lib/paradeiser/version.rb +1 -1
- data/lib/paradeiser/view.rb +21 -0
- data/lib/paradeiser/views/paradeiser/init.erb +1 -0
- data/lib/paradeiser/views/pomodori/finish.erb +1 -0
- data/lib/paradeiser/views/pomodori/report.erb +5 -0
- data/lib/paradeiser/views/pomodori/start.erb +1 -0
- data/lib/paradeiser/views/pomodori/status.erb +9 -0
- data/paradeiser.gemspec +21 -4
- data/templates/linux/hooks/after-finish +10 -0
- data/templates/linux/hooks/after-start +7 -0
- data/templates/mac/hooks/after-finish +10 -0
- data/templates/mac/hooks/after-start +7 -0
- data/test/helper.rb +24 -0
- data/test/integration/test_pom.rb +17 -0
- data/test/lib/assertions.rb +10 -0
- data/test/lib/at_mock.rb +6 -0
- data/test/lib/options_mock.rb +7 -0
- data/test/lib/pomodoro_mock.rb +11 -0
- data/test/lib/process_status_mock.rb +2 -0
- data/test/lib/pstore_mock.rb +16 -0
- data/test/templates/hooks/pre-finish +1 -0
- data/test/unit/test_paradeiser_controller.rb +88 -0
- data/test/unit/test_pomodori_controller.rb +103 -0
- data/test/unit/test_pomodori_view.rb +78 -0
- data/test/unit/test_pomodoro.rb +100 -0
- data/test/unit/test_pomodoro_hooks.rb +83 -0
- data/test/unit/test_repository.rb +127 -0
- data/test/unit/test_router.rb +36 -0
- data/test/unit/test_scheduler.rb +44 -0
- 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->active -->
|
23
|
+
<g id="edge2" class="edge"><title>idle->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->idle -->
|
33
|
+
<g id="edge1" class="edge"><title>starting_state->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->finished -->
|
44
|
+
<g id="edge3" class="edge"><title>active->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>
|
data/lib/paradeiser.rb
CHANGED
@@ -1,5 +1,28 @@
|
|
1
|
-
require
|
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
|
-
|
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,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,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
|