ruby-pomodoro 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +24 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +78 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +8 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +46 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/exe/ruby-pomodoro +5 -0
  16. data/lib/ruby/pomodoro/cmd/base.rb +40 -0
  17. data/lib/ruby/pomodoro/cmd/choose_task.rb +33 -0
  18. data/lib/ruby/pomodoro/cmd/edit_list.rb +15 -0
  19. data/lib/ruby/pomodoro/cmd/error_handler.rb +17 -0
  20. data/lib/ruby/pomodoro/cmd/main.rb +15 -0
  21. data/lib/ruby/pomodoro/cmd/pause.rb +27 -0
  22. data/lib/ruby/pomodoro/cmd/quit.rb +19 -0
  23. data/lib/ruby/pomodoro/cmd/stop.rb +12 -0
  24. data/lib/ruby/pomodoro/command_controller.rb +42 -0
  25. data/lib/ruby/pomodoro/notification.rb +40 -0
  26. data/lib/ruby/pomodoro/notification_observer.rb +32 -0
  27. data/lib/ruby/pomodoro/printer.rb +56 -0
  28. data/lib/ruby/pomodoro/progressbar.rb +32 -0
  29. data/lib/ruby/pomodoro/task.rb +25 -0
  30. data/lib/ruby/pomodoro/tasks/editor.rb +65 -0
  31. data/lib/ruby/pomodoro/tasks/entity.rb +27 -0
  32. data/lib/ruby/pomodoro/tasks/resource.rb +42 -0
  33. data/lib/ruby/pomodoro/terminal_notifier_channel.rb +13 -0
  34. data/lib/ruby/pomodoro/time_converter.rb +27 -0
  35. data/lib/ruby/pomodoro/time_helpers.rb +11 -0
  36. data/lib/ruby/pomodoro/version.rb +5 -0
  37. data/lib/ruby/pomodoro/worker.rb +78 -0
  38. data/lib/ruby/pomodoro.rb +86 -0
  39. data/lib/view/main.txt.erb +13 -0
  40. data/ruby-pomodoro.gemspec +31 -0
  41. 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,13 @@
1
+ module Ruby
2
+ module Pomodoro
3
+ module TerminalNotifierChannel
4
+ module_function
5
+
6
+ def call(message)
7
+ TerminalNotifier.notify(
8
+ message, title: "RubyPomodoro", sound: "default"
9
+ )
10
+ end
11
+ end
12
+ end
13
+ 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,11 @@
1
+ module Ruby
2
+ module Pomodoro
3
+ module TimeHelpers
4
+ # @param [Integer] seconds
5
+ # @return [String] as N:d N:h N:m
6
+ def to_format_string(seconds)
7
+ Ruby::Pomodoro::TimeConverter.to_format_string(seconds)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Ruby
2
+ module Pomodoro
3
+ VERSION = "0.4.0"
4
+ end
5
+ 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