minder 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ require 'minder/timer'
2
+
3
+ module Minder
4
+ class Period
5
+ attr_accessor :minutes,
6
+ :timer
7
+
8
+ def initialize(minutes: nil)
9
+ self.minutes = minutes
10
+ self.timer = Minder::Timer.new(seconds: minutes.to_i * 60)
11
+ end
12
+
13
+ def start!
14
+ Minder.play_sound('start.wav')
15
+ timer.start!
16
+ end
17
+
18
+ def complete!
19
+ Minder.play_sound('done.wav')
20
+ @status = :completed
21
+ end
22
+
23
+ def completed?
24
+ @status == :completed
25
+ end
26
+
27
+ def elapsed?
28
+ timer.completed?
29
+ end
30
+
31
+ def message
32
+ timer.to_s
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,54 @@
1
+ require 'minder/frame'
2
+
3
+ module Minder
4
+ class PomodoroFrame < Frame
5
+ def template
6
+ text = <<-TEXT
7
+ <%= period.title %>
8
+ TEXT
9
+
10
+ if period.message
11
+ text += <<-TEXT
12
+
13
+ <%= period.message %>
14
+ TEXT
15
+ end
16
+
17
+ if task_manager.started_task
18
+ text += <<-TEXT
19
+
20
+ Working on: #{task_manager.started_task}
21
+ TEXT
22
+ end
23
+
24
+ text
25
+ end
26
+
27
+ def period
28
+ pomodoro_runner.current_action
29
+ end
30
+
31
+ def handle_char_keypress(key)
32
+ event = case key
33
+ when ' ' then :continue
34
+ when 'e' then :editor
35
+ end
36
+
37
+ changed
38
+ notify_observers(event)
39
+ end
40
+
41
+ def handle_non_char_keypress(key)
42
+ event = case key
43
+ when 3 then :exit
44
+ end
45
+
46
+ changed
47
+ notify_observers(event)
48
+ end
49
+
50
+ def set_cursor_position
51
+ window.setpos(1, lines[0].strip.length + 2)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ require 'minder/period'
2
+
3
+ module Minder
4
+ class PomodoroPeriod < Period
5
+ def initialize(minutes: DEFAULT_WORK_PERIOD)
6
+ super
7
+ end
8
+
9
+ def title
10
+ "Work period"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,69 @@
1
+ require 'observer'
2
+
3
+ require 'minder/pomodoro_period'
4
+ require 'minder/break_period'
5
+ require 'minder/idle_period'
6
+
7
+ module Minder
8
+ class PomodoroRunner
9
+ include Observable
10
+
11
+ attr_accessor :work_duration,
12
+ :short_break_duration,
13
+ :long_break_duration
14
+
15
+ attr_reader :action_count,
16
+ :current_action
17
+
18
+ def initialize(**options)
19
+ self.work_duration = options.fetch(:work_duration)
20
+ self.short_break_duration = options.fetch(:short_break_duration)
21
+ self.long_break_duration = options.fetch(:long_break_duration)
22
+ @action_count = 0
23
+ @current_action = IdlePeriod.new
24
+ end
25
+
26
+ def tick
27
+ return if !current_action.elapsed? || current_action.completed?
28
+
29
+ old_action = current_action
30
+ current_action.complete!
31
+ @current_action = IdlePeriod.new
32
+
33
+ changed
34
+ if old_action.is_a?(PomodoroPeriod)
35
+ notify_observers(:completed_work)
36
+ elsif old_action.is_a?(BreakPeriod)
37
+ notify_observers(:completed_break)
38
+ end
39
+ end
40
+
41
+ def continue
42
+ return unless current_action.elapsed?
43
+
44
+ advance_action
45
+ current_action.start!
46
+ end
47
+
48
+ def advance_action
49
+ @action_count += 1
50
+ changed
51
+
52
+ if action_count.odd?
53
+ notify_observers(:started_work)
54
+ @current_action = PomodoroPeriod.new(minutes: work_duration)
55
+ else
56
+ notify_observers(:started_break)
57
+ @current_action = BreakPeriod.new(minutes: break_duration)
58
+ end
59
+ end
60
+
61
+ def break_duration
62
+ if action_count % 8 == 0
63
+ long_break_duration
64
+ else
65
+ short_break_duration
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,47 @@
1
+ require 'minder/frame'
2
+
3
+ module Minder
4
+ class QuickAddFrame < Frame
5
+ attr_accessor :input
6
+
7
+ def initialize(*)
8
+ super
9
+ self.input = ''
10
+ end
11
+
12
+ def template
13
+ <<-TEXT
14
+ Quick add task:
15
+ TEXT
16
+ end
17
+
18
+ def set_text
19
+ self.lines[0] += ' ' + input
20
+ super
21
+ end
22
+
23
+ def set_cursor_position
24
+ window.setpos(1, template.strip.length + 2 + input.length)
25
+ end
26
+
27
+ def handle_char_keypress(key)
28
+ self.input += key
29
+ refresh
30
+ end
31
+
32
+ def handle_non_char_keypress(key)
33
+ case key
34
+ when 127
35
+ self.input.chop!
36
+ refresh
37
+ when 10
38
+ changed
39
+ notify_observers(:add_task, { task: input })
40
+ self.input = ''
41
+ refresh
42
+ else
43
+ #Minder.pry_open(binding)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,85 @@
1
+ require 'ostruct'
2
+ require 'minder/pomodoro_frame'
3
+ require 'minder/message_frame'
4
+ require 'minder/quick_add_frame'
5
+
6
+ module Minder
7
+ class Scene
8
+ attr_accessor :frames
9
+
10
+ def initialize
11
+ self.frames = []
12
+ end
13
+
14
+ def setup
15
+ Curses.noecho
16
+ Curses.init_screen
17
+ Curses.timeout = 0
18
+ clear
19
+ Curses.refresh
20
+ end
21
+
22
+ def focused_frame
23
+ frames.find(&:focused?)
24
+ end
25
+
26
+ def switch_focus
27
+ current_index = frames.find_index(focused_frame)
28
+ focused_frame.unfocus
29
+ next_frame = frames[current_index + 1..-1].find { |frame| !frame.hidden? }
30
+ if next_frame
31
+ next_frame.focus
32
+ else
33
+ frames[0].focus
34
+ end
35
+ end
36
+
37
+ def refresh
38
+ frames.map(&:refresh)
39
+ end
40
+
41
+ def resize_frames
42
+ frames.map(&:resize)
43
+
44
+ available_height = Curses.lines - frames.last.height
45
+
46
+ frames.first.move(0, 0)
47
+ next_height = frames.first.height
48
+ second_frame = frames[1]
49
+
50
+ unless second_frame.hidden?
51
+ proposed_height = available_height - frames.first.height
52
+ if proposed_height < second_frame.desired_height
53
+ second_frame.height = proposed_height
54
+ else
55
+ second_frame.height = second_frame.desired_height
56
+ end
57
+ second_frame.move(next_height, 0)
58
+ next_height += second_frame.height
59
+ end
60
+
61
+ if next_height <= available_height
62
+ frames.last.move(next_height, 0)
63
+ else
64
+ frames.last.move(available_height, 0)
65
+ end
66
+ end
67
+
68
+ def redraw
69
+ refresh
70
+ resize_frames
71
+ refresh
72
+ Curses.curs_set(1)
73
+ focused_frame.set_cursor_position
74
+ focused_frame.window_refresh
75
+ end
76
+
77
+ def clear
78
+ Curses.clear
79
+ end
80
+
81
+ def close
82
+ Curses.close_screen
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,28 @@
1
+ require 'virtus'
2
+
3
+ module Minder
4
+ class Task
5
+ include Virtus.model
6
+
7
+ attribute :description, String
8
+ attribute :selected, Boolean, default: false
9
+ attribute :started, Boolean, default: false
10
+
11
+ def start
12
+ self.started = true
13
+ end
14
+
15
+ def unstart
16
+ self.description.gsub!(/\* /, '')
17
+ self.started = false
18
+ end
19
+
20
+ def started?
21
+ super || description =~ /\* /
22
+ end
23
+
24
+ def to_s
25
+ description.gsub(/\A\* /, '')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,120 @@
1
+ require 'minder/task'
2
+ require 'fileutils'
3
+
4
+ module Minder
5
+ class TaskRecorder
6
+ attr_accessor :tasks,
7
+ :lines
8
+
9
+ def initialize
10
+ @selected_task_index = 0
11
+ reload
12
+ end
13
+
14
+ def tasks
15
+ @tasks ||= lines.map { |task| Task.new(description: task) }
16
+ end
17
+
18
+ def tasks?
19
+ !tasks.empty?
20
+ end
21
+
22
+ def add_task(task)
23
+ File.open(DOING_FILE, 'a') do |file|
24
+ file.write("#{task}\n")
25
+ end
26
+ reload
27
+ end
28
+
29
+ def select_next_task
30
+ if @selected_task_index + 1 <= tasks.length - 1
31
+ @selected_task_index += 1
32
+ else
33
+ @selected_task_index = 0
34
+ end
35
+ end
36
+
37
+ def select_previous_task
38
+ if @selected_task_index == 0
39
+ @selected_task_index = tasks.length - 1
40
+ else
41
+ @selected_task_index -= 1
42
+ end
43
+ end
44
+
45
+ def selected_task_index
46
+ @selected_task_index
47
+ end
48
+
49
+ def selected_task
50
+ tasks[selected_task_index]
51
+ end
52
+
53
+ def delete_task
54
+ lines.delete_at(selected_task_index)
55
+ @tasks = nil
56
+ write_file_with_backup
57
+ reload
58
+
59
+ select_previous_task
60
+ end
61
+
62
+ def reload
63
+ self.lines = File.read(DOING_FILE).strip.split("\n")
64
+ @tasks = nil
65
+ end
66
+
67
+ def complete_task
68
+ task = selected_task
69
+ delete_task
70
+ add_to_done_file("Finished: #{task.description}")
71
+ end
72
+
73
+ def add_to_done_file(text)
74
+ File.open(DONE_FILE, 'a') do |file|
75
+ file.write("[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #{text}\n")
76
+ end
77
+ end
78
+
79
+ def write_file_with_backup
80
+ FileUtils.cp(DOING_FILE, DOING_FILE + '.old')
81
+ write_file(DOING_FILE)
82
+ end
83
+
84
+ def write_file(path)
85
+ File.open(path, 'w') do |file|
86
+ tasks.each do |task|
87
+ line = task.to_s
88
+ line = "* #{line}" if task.started?
89
+ file.write("#{line}\n")
90
+ end
91
+ end
92
+ end
93
+
94
+ def start_task
95
+ selected_task.start
96
+ write_file_with_backup
97
+ add_to_done_file("Started: #{selected_task.description}")
98
+ reload
99
+ end
100
+
101
+ def unstart_task
102
+ selected_task.unstart
103
+ write_file_with_backup
104
+ add_to_done_file("Un-started: #{selected_task.description}")
105
+ reload
106
+ end
107
+
108
+ def started_task
109
+ tasks.find(&:started?)
110
+ end
111
+
112
+ def select_last_task
113
+ @selected_task_index = tasks.length - 1
114
+ end
115
+
116
+ def select_first_task
117
+ @selected_task_index = 0
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,32 @@
1
+ require 'minder'
2
+
3
+ module Minder
4
+ class Timer
5
+ attr_accessor :seconds,
6
+ :start_time
7
+
8
+ def initialize(seconds: DEFAULT_WORK_PERIOD)
9
+ self.seconds = seconds
10
+ end
11
+
12
+ def start!
13
+ self.start_time = Time.now
14
+ end
15
+
16
+ def completed?
17
+ elapsed_time.to_i >= seconds
18
+ end
19
+
20
+ def elapsed_time
21
+ return 0 unless start_time
22
+
23
+ (Time.now - start_time)
24
+ end
25
+
26
+ def to_s
27
+ "#{Minder.formatted_time(elapsed_time)} " \
28
+ "(out of #{Minder.formatted_time(seconds)})"
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,3 @@
1
+ module Minder
2
+ VERSION = 0.1
3
+ end
data/lib/minder.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Minder
2
+ DEFAULT_WORK_PERIOD = 25
3
+ DEFAULT_BREAK_PERIOD = 5
4
+ CONFIG_LOCATION = ENV['HOME'] + '/.minder.json'
5
+ ASSETS_LOCATION = File.expand_path(File.dirname(__FILE__) + '/../assets')
6
+ DOING_FILE = File.join(ENV["HOME"], '.minder', 'doing.txt')
7
+ DONE_FILE = File.join(ENV["HOME"], '.minder', 'done.txt')
8
+ def self.formatted_time(seconds)
9
+ minutes = (seconds / 60).to_i
10
+ seconds = (seconds % 60).round
11
+ "#{'%02d' % minutes}:#{'%02d' % seconds}"
12
+ end
13
+
14
+ def self.play_sound(name)
15
+ command = is_linux? ? 'aplay' : 'afplay'
16
+ spawn("#{command} #{ASSETS_LOCATION}/#{name}",
17
+ out: '/dev/null',
18
+ err: '/dev/null')
19
+ end
20
+
21
+ def self.is_linux?
22
+ RUBY_PLATFORM =~ /linux/
23
+ end
24
+
25
+ def self.pry_open(b)
26
+ Curses.close_screen
27
+ require 'pry'
28
+ b.pry
29
+ end
30
+ end
data/minder.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'minder/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "minder"
7
+ spec.version = Minder::VERSION
8
+ spec.authors = ["Joseph Method"]
9
+ spec.email = ["tristil@gmail.com"]
10
+ spec.description = %q{Productivity tool borrowing a little from everything.}
11
+ spec.summary = %q{Combines a Pomodoro Technique runner with GTD-style task backlogs and Day One-style prompts."}
12
+ spec.homepage = "http://github.com/tristil/minder"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_runtime_dependency 'curses', '~> 1.0', '>= 1.0.1'
20
+ spec.add_runtime_dependency 'virtus', '~> 1.0', '>= 1.0.5'
21
+
22
+ spec.add_development_dependency "bundler", '~> 0'
23
+ spec.add_development_dependency "rspec", '~> 3.2', '>= 3.2'
24
+ spec.add_development_dependency "timecop", '~> 0.7', '>= 0.7.3'
25
+ spec.add_development_dependency "pry", '~> 0.10', '>= 0.10'
26
+ spec.add_development_dependency "pry-byebug", '~> 3.1', '>= 3.1.0'
27
+ spec.add_development_dependency 'pry-stack_explorer', '~> 0.4.9'
28
+
29
+ spec.licenses = ['MIT']
30
+ end
31
+
@@ -0,0 +1,55 @@
1
+ require 'minder/application'
2
+ require 'tempfile'
3
+
4
+ describe Minder::Application do
5
+ let(:config_file) do
6
+ Tempfile.new('minder config')
7
+ end
8
+
9
+ describe '#initialize' do
10
+ it 'sets the config' do
11
+ config = instance_spy('Minder::Config')
12
+ application = Minder::Application.new(config: config)
13
+ expect(application.config).to eq(config)
14
+ end
15
+ end
16
+
17
+ specify '#config_location is delegated to the config' do
18
+ config = instance_spy('Minder::Config', location: 'location')
19
+ application = Minder::Application.new(config: config)
20
+ expect(application.config_location).to eq('location')
21
+ end
22
+
23
+ describe '#run' do
24
+ let(:config) do
25
+ instance_spy(
26
+ 'Minder::Config',
27
+ work_duration: 'work_duration',
28
+ short_break_duration: 'short_break_duration',
29
+ long_break_duration: 'long_break_duration')
30
+ end
31
+ let(:application) { Minder::Application.new(config: config) }
32
+
33
+ it 'runs the application' do
34
+ pomodoro_runner = instance_double(Minder::PomodoroRunner)
35
+ allow(Minder::PomodoroRunner).to receive(:new)
36
+ .with(work_duration: 'work_duration',
37
+ short_break_duration: 'short_break_duration',
38
+ long_break_duration: 'long_break_duration')
39
+ .and_return(pomodoro_runner)
40
+ allow(application).to receive(:system)
41
+ allow(application).to receive(:puts)
42
+ allow(STDIN).to receive(:getc).and_return(' ')
43
+ interval = instance_double(Minder::Interval)
44
+ allow(pomodoro_runner).to receive(:next_action).and_return(interval)
45
+
46
+ application.run
47
+
48
+ expect(application).to have_received(:system).with('stty raw -echo')
49
+ expect(application).to have_received(:system).with('stty -raw echo')
50
+ expect(STDIN).to have_received(:getc).with('stty -raw echo')
51
+ expect(pomodoro_runner).to have_received(:next_action)
52
+ expect(interval).to receive(:)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,44 @@
1
+ require 'minder/config'
2
+ require 'tempfile'
3
+
4
+ describe Minder::Config do
5
+ specify 'it sets location' do
6
+ config = Minder::Config.new('location')
7
+ expect(config.location).to eq('location')
8
+ end
9
+
10
+ describe '#load' do
11
+ it 'loads values from a json file if it exists' do
12
+ data = {
13
+ work_duration: 15,
14
+ short_break_duration: 10
15
+ }
16
+ json_file = Tempfile.new('minder json file')
17
+ json_file.write(JSON.dump(data))
18
+ json_file.seek(0)
19
+ config = Minder::Config.new(json_file.path)
20
+ config.load
21
+ expect(config.data).to include(
22
+ work_duration: 15,
23
+ short_break_duration: 10,
24
+ long_break_duration: 15)
25
+ end
26
+
27
+ it 'loads default values if no location is passed' do
28
+ config = Minder::Config.new
29
+ config.load
30
+ expect(config.data).to include(
31
+ work_duration: 25,
32
+ short_break_duration: 5,
33
+ long_break_duration: 15)
34
+ end
35
+ end
36
+
37
+ describe 'auto-generated methods' do
38
+ it 'creates a reader method for each default value key' do
39
+ config = Minder::Config.new
40
+ config.load
41
+ expect(config.work_duration).to eq(25)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,25 @@
1
+ require 'minder/pomodoro_break'
2
+
3
+ describe Minder::PomodoroBreak do
4
+ describe '#run' do
5
+ let(:timer) { instance_spy(Minder::Timer) }
6
+
7
+ it 'runs the pomodoro' do
8
+ pomodoro = described_class.new(minutes: 5)
9
+
10
+ allow(Minder::Timer).to receive(:new)
11
+ .with(seconds: 300)
12
+ .and_return(timer)
13
+ allow(timer).to receive(:completed?).and_return(false, true)
14
+ allow($stdout).to receive(:flush)
15
+ allow(pomodoro).to receive(:puts)
16
+ allow(pomodoro).to receive(:print)
17
+
18
+ pomodoro.run
19
+
20
+ expect(pomodoro).to have_received(:puts).with('Break period')
21
+ expect(timer).to have_received(:start!)
22
+ expect(timer).to have_received(:tick).at_least(:once)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ describe Minder::PomodoroRunner do
2
+
3
+ end