minder 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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