minder 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d1bd7f4b433d3f09c0b5c5c932d45f65b7e00dd6
4
+ data.tar.gz: e8c92eac92153eea880e7762c040fef0f43ffc27
5
+ SHA512:
6
+ metadata.gz: 44bbcf6c44358e936ad5d0896992a7ddb9d1815ab18bf4b5c077b346ab7651e19baba8ae5d1fbdb3c9391c97e8bc4f9d6fef108477fd29715c237fcc0d9860f3
7
+ data.tar.gz: efd8144a3790c3afe5d2848d8d392ec1ceb59bfedc6584e2b0b734e3f2215a8f775e988a2636ef2663d537f60a8490e000fae70786ff4a1bd19e4223ae160939
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.2
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ ruby '2.2.2'
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,75 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ minder (0.1)
5
+ curses
6
+ virtus
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ axiom-types (0.1.1)
12
+ descendants_tracker (~> 0.0.4)
13
+ ice_nine (~> 0.11.0)
14
+ thread_safe (~> 0.3, >= 0.3.1)
15
+ binding_of_caller (0.7.2)
16
+ debug_inspector (>= 0.0.1)
17
+ byebug (4.0.5)
18
+ columnize (= 0.9.0)
19
+ coderay (1.1.0)
20
+ coercible (1.0.0)
21
+ descendants_tracker (~> 0.0.1)
22
+ columnize (0.9.0)
23
+ curses (1.0.1)
24
+ debug_inspector (0.0.2)
25
+ descendants_tracker (0.0.4)
26
+ thread_safe (~> 0.3, >= 0.3.1)
27
+ diff-lcs (1.2.5)
28
+ equalizer (0.0.11)
29
+ ice_nine (0.11.1)
30
+ method_source (0.8.2)
31
+ pry (0.10.1)
32
+ coderay (~> 1.1.0)
33
+ method_source (~> 0.8.1)
34
+ slop (~> 3.4)
35
+ pry-byebug (3.1.0)
36
+ byebug (~> 4.0)
37
+ pry (~> 0.10)
38
+ pry-stack_explorer (0.4.9.2)
39
+ binding_of_caller (>= 0.7)
40
+ pry (>= 0.9.11)
41
+ rake (10.4.2)
42
+ rspec (3.2.0)
43
+ rspec-core (~> 3.2.0)
44
+ rspec-expectations (~> 3.2.0)
45
+ rspec-mocks (~> 3.2.0)
46
+ rspec-core (3.2.2)
47
+ rspec-support (~> 3.2.0)
48
+ rspec-expectations (3.2.0)
49
+ diff-lcs (>= 1.2.0, < 2.0)
50
+ rspec-support (~> 3.2.0)
51
+ rspec-mocks (3.2.1)
52
+ diff-lcs (>= 1.2.0, < 2.0)
53
+ rspec-support (~> 3.2.0)
54
+ rspec-support (3.2.2)
55
+ slop (3.6.0)
56
+ thread_safe (0.3.5)
57
+ timecop (0.7.3)
58
+ virtus (1.0.5)
59
+ axiom-types (~> 0.1)
60
+ coercible (~> 1.0)
61
+ descendants_tracker (~> 0.0, >= 0.0.3)
62
+ equalizer (~> 0.0, >= 0.0.9)
63
+
64
+ PLATFORMS
65
+ ruby
66
+
67
+ DEPENDENCIES
68
+ bundler
69
+ minder!
70
+ pry
71
+ pry-byebug
72
+ pry-stack_explorer
73
+ rake
74
+ rspec
75
+ timecop
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Joseph Method
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ ## What is it?
2
+
3
+ This is a CLI-based tool that combines the best of several different
4
+ productivity methodologies.
5
+
6
+ At present it has these features:
7
+
8
+ - A timer to help with keeping to the [Pomodoro
9
+ Technique](http://pomodorotechnique.com/) rhythm of 4 25 minute work periods
10
+ with five minute breaks, followed by a long 15 minute break. You hear the
11
+ soothing sounds of a "pomodoro" kitchen timer at the beginning and end of a
12
+ break.
13
+ - A todo log that allows starting, unstarting and completing a task.
14
+ - Stores a simple text log of task and pomodoro activity.
15
+
16
+ Plans for the future include:
17
+
18
+ - Filtering by GTD-style labels (like @context, +project, and #tag)
19
+ - Integration with a website blacklist tool like
20
+ [SelfControl](https://github.com/SelfControlApp/selfcontrol/).
21
+ - Keeping track of the number of pomodoros performed during a day.
22
+ - Desktop notifications for Linux and MacOS
23
+ - Prompts to summarize plans for the day and day's end progress.
24
+
25
+ Audio files are from https://github.com/niftylettuce/pomodoro-timer
26
+
27
+ ## Usage
28
+
29
+ There are three sections. You can switch between sections by pressing Tab. The
30
+ commands for each section only work when the section is focused.
31
+
32
+ - The Pomodoro timer section. Press space to begin the next period. Press 'e'
33
+ to edit the whole tasks list in your `$EDITOR`.
34
+ - The Tasks section. This section disappears during a Pomodoro period to avoid
35
+ distraction.
36
+ - Press 'd' to mark a task as done.
37
+ - Press 'x' to delete a task.
38
+ - Press 's' to start a task.
39
+ - Press 'u' to un-start a task.
40
+ - The Quick Add Task section. Enter text here to add a task to the tasks list
41
+ at any time.
42
+
43
+ ## License
44
+
45
+ MIT License, see LICENSE.txt
data/assets/done.wav ADDED
Binary file
data/assets/start.wav ADDED
Binary file
data/bin/minder ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'minder'
4
+ require 'minder/application'
5
+
6
+ application = Minder::Application.new
7
+ application.run
@@ -0,0 +1,128 @@
1
+ require 'minder/config'
2
+ require 'minder/pomodoro_runner'
3
+ require 'minder/task_recorder'
4
+ require 'minder/scene'
5
+
6
+ require 'curses'
7
+ require 'fileutils'
8
+
9
+ module Minder
10
+ class Application
11
+ attr_accessor :config,
12
+ :scene,
13
+ :pomodoro_frame,
14
+ :message_frame,
15
+ :quick_add_frame
16
+
17
+ def initialize(config: Minder::Config.new(CONFIG_LOCATION))
18
+ self.config = config
19
+ config.load
20
+ FileUtils.mkdir_p(File.join(ENV['HOME'], '.minder'))
21
+ FileUtils.touch(DOING_FILE)
22
+ FileUtils.touch(DONE_FILE)
23
+ end
24
+
25
+ def config_location
26
+ config.location
27
+ end
28
+
29
+ def run
30
+ pomodoro_runner.add_observer(self, :handle_event)
31
+
32
+ self.scene = Scene.new
33
+ scene.setup
34
+
35
+ options = { pomodoro_runner: pomodoro_runner, task_manager: task_recorder }
36
+
37
+ self.pomodoro_frame = PomodoroFrame.new(options)
38
+ self.message_frame = MessageFrame.new(options)
39
+ self.quick_add_frame = QuickAddFrame.new(options)
40
+ quick_add_frame.focus
41
+
42
+ scene.frames << pomodoro_frame
43
+ scene.frames << message_frame
44
+ scene.frames << quick_add_frame
45
+
46
+ scene.frames.each do |frame|
47
+ frame.add_observer(self, :handle_event)
48
+ end
49
+
50
+ # TODO: Eww, gross
51
+ scene.redraw
52
+ scene.redraw
53
+
54
+ old_dimensions = [Curses.lines, Curses.cols]
55
+ loop do
56
+ scene.frames.each do |frame|
57
+ next unless frame.focused?
58
+ frame.listen
59
+ end
60
+ pomodoro_runner.tick
61
+ pomodoro_frame.refresh
62
+ scene.focused_frame.set_cursor_position
63
+ scene.focused_frame.window_refresh
64
+
65
+ new_dimensions = [Curses.lines, Curses.cols]
66
+ if new_dimensions != old_dimensions
67
+ scene.redraw
68
+ scene.redraw
69
+ old_dimensions = new_dimensions
70
+ end
71
+
72
+ sleep(0.01)
73
+ end
74
+
75
+ scene.close
76
+ end
77
+
78
+ def pomodoro_runner
79
+ @runner ||= PomodoroRunner.new(
80
+ work_duration: config.work_duration,
81
+ short_break_duration: config.short_break_duration,
82
+ long_break_duration: config.long_break_duration)
83
+ end
84
+
85
+ def task_recorder
86
+ @task_recorder ||= TaskRecorder.new
87
+ end
88
+
89
+ def handle_event(event, data = {})
90
+ return unless event
91
+
92
+ case event
93
+ when :started_work
94
+ message_frame.hide
95
+ when :completed_work
96
+ message_frame.unhide
97
+ when :continue
98
+ pomodoro_runner.continue
99
+ when :editor
100
+ `$EDITOR ~/.minder/doing.txt`
101
+ task_recorder.reload
102
+ when :add_task
103
+ task_recorder.add_task(data[:task])
104
+ when :switch_focus
105
+ scene.switch_focus
106
+ when :select_next_task
107
+ task_recorder.select_next_task
108
+ when :select_previous_task
109
+ task_recorder.select_previous_task
110
+ when :delete_task
111
+ task_recorder.delete_task
112
+ when :complete_task
113
+ task_recorder.complete_task
114
+ when :start_task
115
+ task_recorder.start_task
116
+ when :unstart_task
117
+ task_recorder.unstart_task
118
+ when :select_last_task
119
+ task_recorder.select_last_task
120
+ when :select_first_task
121
+ task_recorder.select_first_task
122
+ end
123
+
124
+ scene.redraw
125
+ scene.redraw
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,13 @@
1
+ require 'minder/period'
2
+
3
+ module Minder
4
+ class BreakPeriod < Period
5
+ def initialize(minutes: DEFAULT_WORK_PERIOD)
6
+ super
7
+ end
8
+
9
+ def title
10
+ "Break period"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ require 'json'
2
+
3
+ module Minder
4
+ class Config
5
+ DEFAULTS = {
6
+ work_duration: 25,
7
+ short_break_duration: 5,
8
+ long_break_duration: 15
9
+ }
10
+
11
+ attr_accessor :location
12
+ attr_reader :data
13
+
14
+ def initialize(location = nil)
15
+ self.location = location
16
+ @data = {}
17
+ end
18
+
19
+ def load
20
+ if File.exists?(location.to_s)
21
+ file = File.open(location, 'r')
22
+ @data = JSON.parse(file.read, symbolize_names: true)
23
+ end
24
+ DEFAULTS.each do |key, value|
25
+ data[key] ||= DEFAULTS[key]
26
+ end
27
+ end
28
+
29
+ DEFAULTS.keys.each do |key|
30
+ define_method(key) do
31
+ data[key]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,172 @@
1
+ require 'observer'
2
+ require 'erb'
3
+ module Minder
4
+ class Frame
5
+ include Observable
6
+
7
+ attr_accessor :window,
8
+ :min_height,
9
+ :height,
10
+ :width,
11
+ :left,
12
+ :top,
13
+ :pomodoro_runner,
14
+ :task_manager,
15
+ :lines
16
+
17
+ def initialize(**options)
18
+ height = options.fetch(:height, 3)
19
+ width = options.fetch(:width, 40)
20
+ top = options.fetch(:top, 0)
21
+ left = options.fetch(:left, 0)
22
+ task_manager = options.fetch(:task_manager)
23
+ pomodoro_runner = options.fetch(:pomodoro_runner)
24
+
25
+ @focused = false
26
+ @hidden = false
27
+ @has_cursor = false
28
+ self.pomodoro_runner = pomodoro_runner
29
+ self.task_manager = task_manager
30
+ self.min_height = height
31
+
32
+ self.height = height
33
+ self.width = width
34
+ self.top = top
35
+ self.left = left
36
+
37
+ self.window = build_window
38
+ self.lines = []
39
+ end
40
+
41
+ def build_window
42
+ Curses::Window.new(min_height, width, top, left)
43
+ end
44
+
45
+ def focus
46
+ @focused = true
47
+ @has_cursor = true
48
+ end
49
+
50
+ def unfocus
51
+ @focused = false
52
+ @has_cursor = false
53
+ end
54
+
55
+ def focused?
56
+ @focused
57
+ end
58
+
59
+ def hidden?
60
+ @hidden
61
+ end
62
+
63
+ def hide
64
+ erase
65
+ @hidden = true
66
+ end
67
+
68
+ def unhide
69
+ @hidden = false
70
+ end
71
+
72
+ def template
73
+ raise NotImplementedError
74
+ end
75
+
76
+ def move(top, left)
77
+ window.move(top, left)
78
+ end
79
+
80
+ def has_cursor?
81
+ @has_cursor
82
+ end
83
+
84
+ def listen
85
+ window.timeout = 0
86
+ handle_keypress(window.getch)
87
+ end
88
+
89
+ def refresh
90
+ return if @hidden
91
+ parse_template
92
+ set_text
93
+ window_refresh
94
+ end
95
+
96
+ def window_refresh
97
+ window.refresh
98
+ end
99
+
100
+ def parse_template
101
+ b = binding
102
+ self.lines = ERB.new(template).result(b).split("\n")
103
+ end
104
+
105
+ def resize
106
+ erase
107
+
108
+ self.width = Curses.cols
109
+ if lines.length >= min_height - 2
110
+ self.height = lines.length + 2
111
+ else
112
+ self.height = min_height
113
+ end
114
+
115
+ window.resize(height, width)
116
+ window_refresh
117
+ end
118
+
119
+ def set_text
120
+ window.box(?|, ?-)
121
+ height.times do |index|
122
+ next if index >= height - 2
123
+ window.setpos(index + 1, 1)
124
+ line = lines[index]
125
+ if line
126
+ print_line(line)
127
+ else
128
+ print_line('')
129
+ end
130
+ end
131
+ end
132
+
133
+ def erase
134
+ height.times do |index|
135
+ window.setpos(index, 0)
136
+ window.addstr(' ' * width )
137
+ end
138
+ window_refresh
139
+ end
140
+
141
+ def set_cursor_position
142
+ window.setpos(1, 0)
143
+ end
144
+
145
+ def print_line(text)
146
+ text = text[0,width - 2]
147
+ remainder = width - 2 - text.length
148
+ window.addstr(text + ' ' * remainder)
149
+ end
150
+
151
+ def handle_keypress(key)
152
+ return unless key
153
+
154
+ if key.is_a?(Fixnum)
155
+ if key == 9 # tab
156
+ changed
157
+ notify_observers(:switch_focus)
158
+ else
159
+ handle_non_char_keypress(key)
160
+ end
161
+ else
162
+ handle_char_keypress(key)
163
+ end
164
+ end
165
+
166
+ def handle_non_char_keypress(key)
167
+ end
168
+
169
+ def handle_char_keypress(key)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,25 @@
1
+ require 'minder/period'
2
+
3
+ module Minder
4
+ class IdlePeriod < Period
5
+ def title
6
+ "Press space to start next period"
7
+ end
8
+
9
+ def message
10
+ nil
11
+ end
12
+
13
+ def start!
14
+ # noop
15
+ end
16
+
17
+ def elapsed?
18
+ true
19
+ end
20
+
21
+ def completed?
22
+ true
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,124 @@
1
+ require 'minder/frame'
2
+
3
+ module Minder
4
+ class MessageFrame < Frame
5
+ attr_reader :current_line
6
+
7
+ def initialize(*)
8
+ super
9
+ self.height = desired_height
10
+ end
11
+
12
+ def template
13
+ if task_manager.tasks?
14
+ doing_message
15
+ else
16
+ prompt_message
17
+ end
18
+ end
19
+
20
+ #def build_window
21
+ #Curses::Pad.new(min_height, width)
22
+ #end
23
+
24
+ #def window_refresh
25
+ #window.refresh(scroll_offset, 0, 3, 0, height + 2, width)
26
+ #end
27
+
28
+ def prompt_message
29
+ <<-TEXT
30
+ What are you working on?
31
+
32
+ Press (e) to open editor.
33
+ TEXT
34
+ end
35
+
36
+ def header_text
37
+ <<-TEXT
38
+ Tasks (s) start (x) to delete (d) to mark as done
39
+
40
+ TEXT
41
+ end
42
+
43
+ def tasks_text
44
+ text = ''
45
+ task_manager.tasks.each do |task|
46
+ if task.started?
47
+ text += "-[*] #{task}\n"
48
+ else
49
+ text += "-[ ] #{task}\n"
50
+ end
51
+ end
52
+ text
53
+ end
54
+
55
+ def tasks_text_lines
56
+ tasks_text.split("\n")
57
+ end
58
+
59
+ def header_text_lines
60
+ header_text.split("\n")
61
+ end
62
+
63
+ def desired_height
64
+ header_text_lines.length + tasks_text_lines.length + 3
65
+ end
66
+
67
+ def allocated_tasks_height
68
+ height - header_text_lines.length
69
+ end
70
+
71
+ def offset_tasks_text
72
+ tasks_text_lines[scroll_offset..(allocated_tasks_height + scroll_offset - 4)].join("\n")
73
+ end
74
+
75
+ def blank_line
76
+ " " * (width - 2) + "\n"
77
+ end
78
+
79
+ def doing_message
80
+ header_text +
81
+ offset_tasks_text #+
82
+ #blank_lines
83
+ end
84
+
85
+ def blank_lines
86
+ blank_line * scroll_offset
87
+ end
88
+
89
+ def set_cursor_position
90
+ window.setpos(3 + task_manager.selected_task_index - scroll_offset, 3)
91
+ end
92
+
93
+ def scroll_offset
94
+ position = task_manager.selected_task_index + 5
95
+ if height - position < 0
96
+ (height - position).abs
97
+ else
98
+ 0
99
+ end
100
+ end
101
+
102
+ def handle_char_keypress(key)
103
+ event = case key
104
+ when 'j' then :select_next_task
105
+ when 'k' then :select_previous_task
106
+ when 'd' then :complete_task
107
+ when 'x' then :delete_task
108
+ when 's' then :start_task
109
+ when 'u' then :unstart_task
110
+ when 'G' then :select_last_task
111
+ when 'g'
112
+ @keypress_memory ||= []
113
+ @keypress_memory << 'g'
114
+ if @keypress_memory == ['g', 'g']
115
+ @keypress_memory = []
116
+ :select_first_task
117
+ end
118
+ end
119
+
120
+ changed
121
+ notify_observers(event)
122
+ end
123
+ end
124
+ end