ruby-pomodoro 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +24 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +78 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/ruby-pomodoro +5 -0
- data/lib/ruby/pomodoro/cmd/base.rb +40 -0
- data/lib/ruby/pomodoro/cmd/choose_task.rb +33 -0
- data/lib/ruby/pomodoro/cmd/edit_list.rb +15 -0
- data/lib/ruby/pomodoro/cmd/error_handler.rb +17 -0
- data/lib/ruby/pomodoro/cmd/main.rb +15 -0
- data/lib/ruby/pomodoro/cmd/pause.rb +27 -0
- data/lib/ruby/pomodoro/cmd/quit.rb +19 -0
- data/lib/ruby/pomodoro/cmd/stop.rb +12 -0
- data/lib/ruby/pomodoro/command_controller.rb +42 -0
- data/lib/ruby/pomodoro/notification.rb +40 -0
- data/lib/ruby/pomodoro/notification_observer.rb +32 -0
- data/lib/ruby/pomodoro/printer.rb +56 -0
- data/lib/ruby/pomodoro/progressbar.rb +32 -0
- data/lib/ruby/pomodoro/task.rb +25 -0
- data/lib/ruby/pomodoro/tasks/editor.rb +65 -0
- data/lib/ruby/pomodoro/tasks/entity.rb +27 -0
- data/lib/ruby/pomodoro/tasks/resource.rb +42 -0
- data/lib/ruby/pomodoro/terminal_notifier_channel.rb +13 -0
- data/lib/ruby/pomodoro/time_converter.rb +27 -0
- data/lib/ruby/pomodoro/time_helpers.rb +11 -0
- data/lib/ruby/pomodoro/version.rb +5 -0
- data/lib/ruby/pomodoro/worker.rb +78 -0
- data/lib/ruby/pomodoro.rb +86 -0
- data/lib/view/main.txt.erb +13 -0
- data/ruby-pomodoro.gemspec +31 -0
- metadata +156 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
class NotificationObserver
|
4
|
+
# @param stop [Ruby::Pomodoro::Notification]
|
5
|
+
# @param pause [Ruby::Pomodoro::Notification]
|
6
|
+
# @param time [Numeric] repeat notifications interval in seconds
|
7
|
+
def initialize(stop:, pause:, time:, printer: Printer.new)
|
8
|
+
@stop_notification = stop
|
9
|
+
@pause_notification = pause
|
10
|
+
@time = time
|
11
|
+
@printer = printer
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [Symbol] event
|
15
|
+
def update(event)
|
16
|
+
case event
|
17
|
+
when :finish
|
18
|
+
@stop_notification.notify(@time)
|
19
|
+
@printer.print_line "_Task #{Worker.instance.current_task.name} was stopped, type [\u21e7 + R] for resume\r",
|
20
|
+
color: :red
|
21
|
+
when :pause
|
22
|
+
@pause_notification.notify(@time, skip_now: true)
|
23
|
+
when :stop, :start
|
24
|
+
@pause_notification.stop
|
25
|
+
@stop_notification.stop
|
26
|
+
else
|
27
|
+
@pause_notification.stop
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
class Printer
|
4
|
+
def initialize(stream: $stdout, cursor: TTY::Cursor, palette: Pastel.new)
|
5
|
+
@stream = stream
|
6
|
+
@cursor = cursor
|
7
|
+
@palette = palette
|
8
|
+
end
|
9
|
+
|
10
|
+
def print_template(template_name, cmd_binding)
|
11
|
+
@__cmd_binding = cmd_binding
|
12
|
+
stream.print(render(template_name))
|
13
|
+
end
|
14
|
+
|
15
|
+
def clear_terminal
|
16
|
+
stream.print(cursor.up(100))
|
17
|
+
stream.print(cursor.clear_screen_down)
|
18
|
+
end
|
19
|
+
|
20
|
+
def print(text, color: nil)
|
21
|
+
if color
|
22
|
+
stream.print(palette.send(color, text))
|
23
|
+
else
|
24
|
+
stream.print(text)
|
25
|
+
end
|
26
|
+
stream.flush
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_line(text, color: nil)
|
30
|
+
cursor.clear_line
|
31
|
+
print(text, color: color)
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def render(file_name)
|
36
|
+
ERB.new(read_file(file_name)).result(@__cmd_binding)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :stream, :cursor, :palette
|
42
|
+
|
43
|
+
|
44
|
+
def read_file(file_name_or_path)
|
45
|
+
if file_name_or_path.kind_of?(Symbol)
|
46
|
+
path = File.expand_path(
|
47
|
+
"../../../#{File.join("view", "#{file_name_or_path}.txt.erb")}", __FILE__
|
48
|
+
)
|
49
|
+
File.read(path)
|
50
|
+
else
|
51
|
+
File.read(file_name_or_path)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
class Progressbar
|
4
|
+
def initialize(seconds:, printer: Ruby::Pomodoro::Printer.new)
|
5
|
+
@all_seconds = seconds
|
6
|
+
@spent_seconds = 0
|
7
|
+
@printer = printer
|
8
|
+
end
|
9
|
+
|
10
|
+
def start(text)
|
11
|
+
@text = text
|
12
|
+
end
|
13
|
+
|
14
|
+
def increment
|
15
|
+
@spent_seconds += 1
|
16
|
+
print
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def print
|
22
|
+
@printer.print " In progress: #{@text} [#{strftime}]\r", color: :green
|
23
|
+
end
|
24
|
+
|
25
|
+
def strftime
|
26
|
+
seconds = @all_seconds - @spent_seconds
|
27
|
+
minutes = seconds / 60
|
28
|
+
"#{minutes} m #{seconds - minutes * 60} s"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
# Task for [Ruby::Pomodoro::Worker]
|
4
|
+
class Task
|
5
|
+
attr_reader :name, :pomodors, :spent_time
|
6
|
+
|
7
|
+
def initialize(name, spent_time: 0)
|
8
|
+
@name = name
|
9
|
+
@spent_time = spent_time
|
10
|
+
@pomodors = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Integer]
|
14
|
+
def add_pomodoro
|
15
|
+
@pomodors += 1
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [Integer] seconds
|
19
|
+
# @return [Integer]
|
20
|
+
def track(seconds)
|
21
|
+
@spent_time += seconds
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
module Tasks
|
4
|
+
class Editor
|
5
|
+
attr_reader :tasks_repo, :editor, :file_path
|
6
|
+
|
7
|
+
# @param [Array<Ruby::Pomodoro::Task>] tasks_repo
|
8
|
+
# @param [String] file_path
|
9
|
+
# @param editor [Class, Object] Editor with method open, for create and open file
|
10
|
+
def initialize(tasks_repo: Resource, file_path: Ruby::Pomodoro.tasks_file_path, editor: TTY::Editor)
|
11
|
+
@tasks_repo = tasks_repo
|
12
|
+
@file_path = file_path
|
13
|
+
@editor = editor
|
14
|
+
end
|
15
|
+
|
16
|
+
# Open editor and save tasks from tmp file to task_repo
|
17
|
+
# @return [TrueClass]
|
18
|
+
def edit
|
19
|
+
save
|
20
|
+
editor.open(file_path)
|
21
|
+
create_tasks
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
# save list to disc
|
26
|
+
# @return [TrueClass]
|
27
|
+
def save
|
28
|
+
File.open(file_path, "w") do |f|
|
29
|
+
if @tasks_repo.size > 0
|
30
|
+
f.puts(@tasks_repo.all.map {|task| print_task(task) }.join("\n"))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Load tasks form file
|
37
|
+
# @return [TrueClass]
|
38
|
+
def load
|
39
|
+
create_tasks
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def read_tmp_file
|
46
|
+
File.readlines(file_path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_tasks
|
50
|
+
tasks_repo.delete_all
|
51
|
+
read_tmp_file.each do |line|
|
52
|
+
next unless line
|
53
|
+
|
54
|
+
name, time = line.split("|").compact.map {|l| l.chomp.strip }
|
55
|
+
tasks_repo.create(name: name, spent_time: TimeConverter.to_seconds(time))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def print_task(task)
|
60
|
+
"#{task.name} | #{TimeConverter.to_format_string(task.spent_time)}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
module Tasks
|
4
|
+
class Entity
|
5
|
+
attr_reader :name, :pomodors, :spent_time, :id
|
6
|
+
|
7
|
+
def initialize(name:, spent_time: 0, id: nil)
|
8
|
+
@name = name
|
9
|
+
@spent_time = spent_time
|
10
|
+
@pomodors = 0
|
11
|
+
@id = id
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Integer]
|
15
|
+
def add_pomodoro
|
16
|
+
@pomodors += 1
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Integer] seconds
|
20
|
+
# @return [Integer]
|
21
|
+
def track(seconds)
|
22
|
+
@spent_time += seconds
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
module Tasks
|
4
|
+
class Resource
|
5
|
+
class << self
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
delegate size: :store
|
9
|
+
|
10
|
+
def create(attributes)
|
11
|
+
id = store.size.next
|
12
|
+
Entity.new(id: id, **attributes.slice(:name, :spent_time)).tap do |task|
|
13
|
+
store[id] = task
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(id)
|
18
|
+
store[id]
|
19
|
+
end
|
20
|
+
|
21
|
+
def all
|
22
|
+
store.values
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete_all
|
26
|
+
store.clear
|
27
|
+
end
|
28
|
+
|
29
|
+
def sum(attribute)
|
30
|
+
all.inject(0) { |sum, task| sum + task.public_send(attribute).to_i }
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def store
|
36
|
+
@store ||= {}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
module TimeConverter
|
4
|
+
class << self
|
5
|
+
def to_format_string(seconds)
|
6
|
+
days = seconds / 60 / 60 / 24
|
7
|
+
hours = (seconds - days * 24 * 60 * 60) / 60 / 60
|
8
|
+
minutes = (seconds - (hours * 60 * 60) - (days * 24 * 60 * 60)) / 60
|
9
|
+
{d: days, h: hours, m: minutes}.select { |_k, v| v.positive? }.each.with_object(String.new) do |item, acc|
|
10
|
+
acc << item.reverse.join(":") + " "
|
11
|
+
end.strip
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_seconds(format_string)
|
15
|
+
return 0 if format_string.nil?
|
16
|
+
|
17
|
+
match = format_string.split(" ").compact.map do |item|
|
18
|
+
if item
|
19
|
+
item.split(":").reverse.then { |res| res.size == 2 ? res : nil }
|
20
|
+
end
|
21
|
+
end.compact.to_h.select { |k| %w[m h d].include?(k) }
|
22
|
+
(match["d"].to_i * 24 * 60 * 60) + (match["h"].to_i * 60 * 60) + (match["m"].to_i * 60)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Ruby
|
2
|
+
module Pomodoro
|
3
|
+
# Singleton for work with tasks in application
|
4
|
+
class Worker
|
5
|
+
include Observable
|
6
|
+
include AASM
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
attr_reader :current_task
|
10
|
+
attr_accessor :time_interval, :pomodoro_size, :progressbar
|
11
|
+
|
12
|
+
aasm do
|
13
|
+
state :sleeping, initial: true
|
14
|
+
state :working, :paused
|
15
|
+
|
16
|
+
after_all_transitions :notify
|
17
|
+
|
18
|
+
event :start, before: :changed do
|
19
|
+
transitions from: :sleeping, to: :working, after: ->(task) { run_task(task) }
|
20
|
+
end
|
21
|
+
|
22
|
+
event :resume, before: :changed do
|
23
|
+
transitions from: :paused, to: :working
|
24
|
+
end
|
25
|
+
|
26
|
+
event :pause, before: :changed do
|
27
|
+
transitions from: :working, to: :paused
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
event :finish, before: :changed do
|
32
|
+
after { @do&.kill }
|
33
|
+
|
34
|
+
transitions from: :working, to: :sleeping
|
35
|
+
end
|
36
|
+
|
37
|
+
event :stop, before: :changed do
|
38
|
+
after do
|
39
|
+
@do&.kill
|
40
|
+
@current_task = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
transitions to: :sleeping
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def notify
|
50
|
+
notify_observers(aasm.current_event)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [Ruby::Pomodoro::Task] task
|
54
|
+
# @return [TrueClass]
|
55
|
+
def run_task(task)
|
56
|
+
raise Error, "Setup pomodoro_size" if pomodoro_size.nil?
|
57
|
+
|
58
|
+
@current_task = task
|
59
|
+
@do = Thread.new do
|
60
|
+
progress = progressbar || Ruby::Pomodoro::Progressbar.new(seconds: pomodoro_size)
|
61
|
+
progress.start(task.name)
|
62
|
+
count = 0
|
63
|
+
loop do
|
64
|
+
seconds = time_interval || 1
|
65
|
+
sleep seconds
|
66
|
+
unless paused?
|
67
|
+
task.track(seconds)
|
68
|
+
count += seconds
|
69
|
+
progress.increment
|
70
|
+
finish if count >= pomodoro_size.to_i && working?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'terminal-notifier'
|
2
|
+
require 'observer'
|
3
|
+
require 'tty-cursor'
|
4
|
+
require 'tty-editor'
|
5
|
+
require 'tty-reader'
|
6
|
+
require 'logger'
|
7
|
+
require 'aasm'
|
8
|
+
require "erb"
|
9
|
+
require "ruby/pomodoro/command_controller"
|
10
|
+
require "ruby/pomodoro/time_helpers"
|
11
|
+
require "ruby/pomodoro/notification_observer"
|
12
|
+
require "ruby/pomodoro/printer"
|
13
|
+
require "ruby/pomodoro/cmd/base"
|
14
|
+
require "ruby/pomodoro/cmd/main"
|
15
|
+
require "ruby/pomodoro/cmd/quit"
|
16
|
+
require "ruby/pomodoro/cmd/choose_task"
|
17
|
+
require "ruby/pomodoro/cmd/edit_list"
|
18
|
+
require "ruby/pomodoro/cmd/pause"
|
19
|
+
require "ruby/pomodoro/cmd/stop"
|
20
|
+
require "ruby/pomodoro/cmd/error_handler"
|
21
|
+
require "ruby/pomodoro/time_converter"
|
22
|
+
require "ruby/pomodoro/version"
|
23
|
+
require "ruby/pomodoro/tasks/resource"
|
24
|
+
require "ruby/pomodoro/tasks/editor"
|
25
|
+
require "ruby/pomodoro/task"
|
26
|
+
require "ruby/pomodoro/tasks/entity"
|
27
|
+
require "ruby/pomodoro/worker"
|
28
|
+
require "ruby/pomodoro/progressbar"
|
29
|
+
require "ruby/pomodoro/notification"
|
30
|
+
require "ruby/pomodoro/terminal_notifier_channel"
|
31
|
+
|
32
|
+
module Ruby
|
33
|
+
module Pomodoro
|
34
|
+
class Error < StandardError; end
|
35
|
+
|
36
|
+
REPEAT_ALERT_TIME = 60 * 5
|
37
|
+
POMODORO_SIZE = 60 * 30
|
38
|
+
|
39
|
+
class << self
|
40
|
+
attr_reader :editor, :tasks_file_path
|
41
|
+
|
42
|
+
def start
|
43
|
+
Worker.instance.then do |worker|
|
44
|
+
add_observer(worker)
|
45
|
+
worker.pomodoro_size = POMODORO_SIZE
|
46
|
+
end
|
47
|
+
|
48
|
+
@tasks_file_path = File.join(init_app_folder, "tasks")
|
49
|
+
|
50
|
+
@editor = Tasks::Editor.new(file_path: tasks_file_path)
|
51
|
+
editor.load
|
52
|
+
Cmd::Main.new.call
|
53
|
+
reader = TTY::Reader.new
|
54
|
+
reader.subscribe(CommandController)
|
55
|
+
loop do
|
56
|
+
reader.read_char
|
57
|
+
rescue => e
|
58
|
+
Cmd::ErrorHandler.new.call(e)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def add_observer(worker)
|
65
|
+
notify_ch = TerminalNotifierChannel
|
66
|
+
pause_notification = Notification.new("Task is paused, resume?", notify_ch)
|
67
|
+
stop_notification =
|
68
|
+
Notification.new("Work is stopped, choose task for resume", notify_ch)
|
69
|
+
|
70
|
+
worker.add_observer(
|
71
|
+
NotificationObserver.new(
|
72
|
+
stop: stop_notification, pause: pause_notification, time: REPEAT_ALERT_TIME
|
73
|
+
)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def init_app_folder
|
78
|
+
path = File.join(Dir.home, ".ruby-pomodoro")
|
79
|
+
unless Dir.exists?(path)
|
80
|
+
Dir.mkdir(path)
|
81
|
+
end
|
82
|
+
path
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Total time: <%= to_format_string(Ruby::Pomodoro::Tasks::Resource.sum(:spent_time)) %>
|
2
|
+
List of tasks:
|
3
|
+
<% if @tasks %>
|
4
|
+
<% @tasks.each do |task| %>
|
5
|
+
<%= task.id %>. <%= task.name %> <%= task.spent_time.zero? ? nil : to_format_string(task.spent_time) %>
|
6
|
+
<% end %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
[c] - Choose task to work
|
10
|
+
[e] - Edit list of tasks
|
11
|
+
[p] - Pause work
|
12
|
+
[s] - Stop work
|
13
|
+
[q] - Quit
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'lib/ruby/pomodoro/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "ruby-pomodoro"
|
5
|
+
spec.version = Ruby::Pomodoro::VERSION
|
6
|
+
spec.authors = ["Alexander Shvaykin"]
|
7
|
+
spec.email = ["skiline.alex@gmail.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{CLI pomodoro-tracker}
|
10
|
+
spec.homepage = "https://github.com/AlexanderShvaykin"
|
11
|
+
spec.license = "MIT"
|
12
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
13
|
+
|
14
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
15
|
+
spec.metadata["source_code_uri"] = "https://github.com/AlexanderShvaykin"
|
16
|
+
spec.metadata["changelog_uri"] = "https://github.com/AlexanderShvaykin"
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
spec.add_runtime_dependency 'terminal-notifier'
|
27
|
+
spec.add_runtime_dependency 'tty-cursor', '~> 0.6'
|
28
|
+
spec.add_runtime_dependency 'tty-editor', '~> 0.7'
|
29
|
+
spec.add_runtime_dependency 'pastel', '~> 0.8'
|
30
|
+
spec.add_runtime_dependency 'aasm', '~> 5'
|
31
|
+
end
|