medo 0.1.0

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.
Files changed (43) hide show
  1. data/Gemfile +12 -0
  2. data/Gemfile.lock +48 -0
  3. data/LICENSE.txt +20 -0
  4. data/README.md +14 -0
  5. data/Rakefile +29 -0
  6. data/VERSION +1 -0
  7. data/bin/medo +59 -0
  8. data/features/add_notes.feature +18 -0
  9. data/features/add_task.feature +12 -0
  10. data/features/list_tasks.feature +25 -0
  11. data/features/step_definitions/add_task_steps.rb +3 -0
  12. data/features/step_definitions/list_tasks_steps.rb +6 -0
  13. data/features/support/env.rb +14 -0
  14. data/lib/medo.rb +6 -0
  15. data/lib/medo/commands/clear.rb +9 -0
  16. data/lib/medo/commands/delete.rb +13 -0
  17. data/lib/medo/commands/done.rb +13 -0
  18. data/lib/medo/commands/list.rb +16 -0
  19. data/lib/medo/commands/new.rb +15 -0
  20. data/lib/medo/commands/note.rb +18 -0
  21. data/lib/medo/commands/show.rb +19 -0
  22. data/lib/medo/file_task_storage.rb +53 -0
  23. data/lib/medo/json_task_reader.rb +31 -0
  24. data/lib/medo/json_task_writer.rb +33 -0
  25. data/lib/medo/support.rb +6 -0
  26. data/lib/medo/support/decorator.rb +40 -0
  27. data/lib/medo/task.rb +63 -0
  28. data/lib/medo/task_reader.rb +8 -0
  29. data/lib/medo/task_writer.rb +21 -0
  30. data/lib/medo/terminal.rb +6 -0
  31. data/lib/medo/text_task_writer.rb +137 -0
  32. data/lib/medo/text_task_writer/decorators/colors_decorator.rb +28 -0
  33. data/lib/medo/text_task_writer/decorators/numbers_decorator.rb +37 -0
  34. data/spec/lib/file_task_storage_spec.rb +59 -0
  35. data/spec/lib/json_task_reader_spec.rb +26 -0
  36. data/spec/lib/json_task_writer_spec.rb +32 -0
  37. data/spec/lib/task_reader_spec.rb +9 -0
  38. data/spec/lib/task_spec.rb +101 -0
  39. data/spec/lib/task_writer_spec.rb +18 -0
  40. data/spec/lib/text_task_writer_spec.rb +106 -0
  41. data/spec/spec_helper.rb +24 -0
  42. data/spec/support/task_stubs_spec_helper.rb +41 -0
  43. metadata +189 -0
@@ -0,0 +1,31 @@
1
+ require 'medo/task_reader'
2
+ require 'json'
3
+ require 'time'
4
+
5
+ module Medo
6
+ class JsonTaskReader < TaskReader
7
+ def initialize(input_stream)
8
+ super()
9
+ @input_stream = input_stream
10
+ end
11
+
12
+ def read
13
+ tasks = []
14
+ JSON.parse(@input_stream.read).map do |task_attributes|
15
+ instantiate_task(task_attributes)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def instantiate_task(attributes)
22
+ attributes["created_at"] = Time.parse(attributes["created_at"])
23
+ if attributes["done"]
24
+ attributes["completed_at"] = Time.parse(attributes["completed_at"])
25
+ end
26
+ Task.from_attributes(attributes)
27
+ end
28
+ end
29
+ end
30
+
31
+
@@ -0,0 +1,33 @@
1
+ require 'medo/task_writer'
2
+ require 'json'
3
+
4
+ module Medo
5
+ class JsonTaskWriter < TaskWriter
6
+ def initialize(output_stream = STDOUT)
7
+ super()
8
+ @output_stream = output_stream
9
+ end
10
+
11
+ def write
12
+ tasks = @tasks.map { |t| TaskPresenter.new(t).as_json }.to_json
13
+ @output_stream.write(tasks)
14
+ end
15
+
16
+ class TaskPresenter
17
+ def initialize(task)
18
+ @task = task
19
+ end
20
+
21
+ def as_json
22
+ {
23
+ :done => @task.done?,
24
+ :description => @task.description,
25
+ :created_at => @task.created_at,
26
+ :completed_at => (@task.completed_at if @task.done?),
27
+ :notes => @task.notes
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,6 @@
1
+ module Medo
2
+ module Support
3
+ end
4
+ end
5
+
6
+ require 'medo/support/decorator'
@@ -0,0 +1,40 @@
1
+ module Medo
2
+ module Support
3
+
4
+ ##
5
+ # This module is intended to extend other (decorator) modules
6
+ #
7
+ # Usage:
8
+ #
9
+ # module MyDecorator
10
+ # extend Support::Decorator
11
+ #
12
+ # after_decorate do |arg1, arg2|
13
+ # @arg1, @arg2 = arg1, arg2
14
+ # ...
15
+ # end
16
+ #
17
+ # #methods go here
18
+ # end
19
+ #
20
+ # decorated = Object.new
21
+ # MyDecorator.decorate(decorate, :foo, :bar)
22
+ #
23
+ # :foo and :bar go to after_decorate block, which is evaluated
24
+ # on decorated object
25
+ #
26
+ # after_update block is optional
27
+ #
28
+ module Decorator
29
+ def decorate(base, *args)
30
+ base.extend(self)
31
+ base.instance_exec(*args, &@after_decorate_block) if defined? @after_decorate_block
32
+ base
33
+ end
34
+
35
+ def after_decorate(&block)
36
+ @after_decorate_block = block
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,63 @@
1
+ module Medo
2
+ class Task
3
+ include Comparable
4
+
5
+ attr_reader :description, :created_at, :completed_at, :notes
6
+
7
+ class << self
8
+ attr_accessor :clock
9
+
10
+ def from_attributes(attributes)
11
+ task = Task.new(attributes.delete("description"), attributes)
12
+ task.instance_eval do
13
+ @created_at = attributes["created_at"] or raise ArgumentError, "Missing created_at"
14
+ @done = attributes["done"]
15
+ @completed_at = attributes["completed_at"] or raise ArgumentError, "Missing completed_at" if @done
16
+ end
17
+ task
18
+ end
19
+ end
20
+ self.clock = Time
21
+
22
+ def initialize(description, options = {})
23
+ @description = description.to_s.strip
24
+ raise ArgumentError, "No description given!" if @description.empty?
25
+ @created_at = clock.now
26
+ @completed_at = nil
27
+ @notes = parse_notes(options["notes"] || options[:notes])
28
+ end
29
+
30
+ def <=>(other)
31
+ unless other.kind_of?(Task)
32
+ raise ArgumentError, "comparison of #{self.class.name} with #{other.class.name} failed"
33
+ end
34
+
35
+ case [self.completed_at.nil?, other.completed_at.nil?]
36
+ when [true, true] then (self.created_at <=> other.created_at) * -1
37
+ when [false, false] then (self.completed_at <=> other.completed_at) * -1
38
+ when [true, false] then -1
39
+ when [false, true] then 1
40
+ end
41
+ end
42
+
43
+ def done
44
+ @completed_at = clock.now
45
+ @done = true
46
+ end
47
+
48
+ def done?
49
+ !!@done
50
+ end
51
+
52
+ private
53
+
54
+ def clock
55
+ self.class.clock
56
+ end
57
+
58
+ def parse_notes(notes)
59
+ Array(notes).map { |n| n.to_s.strip }.reject(&:empty?)
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,8 @@
1
+ module Medo
2
+ class TaskReader
3
+ def read
4
+ raise NotImplementedError
5
+ end
6
+ end
7
+ end
8
+
@@ -0,0 +1,21 @@
1
+ module Medo
2
+ class TaskWriter
3
+ def initialize
4
+ @tasks = []
5
+ end
6
+
7
+ def add_task(*tasks)
8
+ @tasks += tasks.flatten
9
+ end
10
+ alias add_tasks add_task
11
+
12
+ def tasks_to_write
13
+ @tasks.dup
14
+ end
15
+
16
+ def write
17
+ raise NotImplementedError
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,6 @@
1
+ require 'gli/terminal'
2
+
3
+ module Medo
4
+ Terminal = GLI::Terminal
5
+ end
6
+
@@ -0,0 +1,137 @@
1
+ require 'medo/task_writer'
2
+ require 'medo/terminal'
3
+
4
+ module Medo
5
+ class TextTaskWriter < TaskWriter
6
+ def initialize(output_stream = STDOUT)
7
+ super()
8
+ @output_stream = output_stream
9
+ end
10
+
11
+ def write
12
+ return puts "There are no tasks left!" if @tasks.empty?
13
+
14
+ presented_active_tasks = present_tasks(active_tasks)
15
+ presented_completed_tasks = present_tasks(completed_tasks)
16
+
17
+ max_task_length = (presented_active_tasks + presented_completed_tasks).map do |t|
18
+ t.to_s.split("\n").map(&:size).max
19
+ end.max
20
+
21
+ max_output_width = [Terminal.instance.size.first, max_task_length].min
22
+
23
+ presented_active_tasks.each do |t|
24
+ @output_stream.puts t.to_s(max_output_width)
25
+ end
26
+
27
+ if presented_active_tasks.any? and presented_completed_tasks.any?
28
+ @output_stream.puts "-" * max_output_width
29
+ end
30
+
31
+ presented_completed_tasks.each do |t|
32
+ @output_stream.puts t.to_s(max_output_width)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def active_tasks
39
+ @tasks.reject(&:done?).sort
40
+ end
41
+
42
+ def completed_tasks
43
+ @tasks.select(&:done?).sort
44
+ end
45
+
46
+ def present_tasks(tasks)
47
+ tasks.map { |t| TaskPresenter.new(t) }
48
+ end
49
+
50
+ class TaskPresenter
51
+
52
+ class Components < Struct.new(:done, :description, :time, :notes); end
53
+
54
+ def initialize(task)
55
+ @task = task
56
+ end
57
+
58
+ def description(length = nil, options = {})
59
+ if length
60
+ break_line_to_fit(@task.description, length, options)
61
+ else
62
+ @task.description
63
+ end
64
+ end
65
+
66
+ def time
67
+ format = "%H:%M"
68
+ if @task.done?
69
+ "[#{@task.completed_at.strftime(format)}]"
70
+ else
71
+ "(#{@task.created_at.strftime(format)})"
72
+ end
73
+ end
74
+
75
+ def notes(length = nil, options = {})
76
+ return "" if @task.notes.empty?
77
+ "\n\n" + @task.notes.map do |n|
78
+ if length
79
+ break_line_to_fit(n, length, options)
80
+ else
81
+ n.rjust(n.size + done.size + 1)
82
+ end
83
+ end.join("\n") + "\n\n"
84
+ end
85
+
86
+ def done
87
+ "[#{@task.done? ? '+' : ' '}]"
88
+ end
89
+
90
+ def to_s(length = nil)
91
+ c = components(length)
92
+ "#{c.done} #{c.description} #{c.time}#{c.notes}"
93
+ end
94
+
95
+ private
96
+
97
+ def components(length = nil)
98
+ if length
99
+ description_length = length - done.length - time.length - 2
100
+ description_padding = done.length + 1
101
+ formatted_description = description(description_length, :left_padding => description_padding)
102
+
103
+ notes_length = length - done.length - 1
104
+ notes_first_line_padding = done.length + 1
105
+ notes_padding = notes_first_line_padding + 2
106
+ formatted_notes = notes(notes_length, :first_line_padding => notes_first_line_padding, :left_padding => notes_padding)
107
+
108
+ Components.new(done, formatted_description, time, formatted_notes)
109
+ else
110
+ Components.new(done, description, time, notes)
111
+ end
112
+ end
113
+
114
+ def break_line_to_fit(str, length, options = {})
115
+ first_line_padding = options[:first_line_padding]
116
+ left_padding = options[:left_padding]
117
+ padding_diff = (left_padding - first_line_padding) if first_line_padding && left_padding
118
+ available_length = length - padding_diff.to_i
119
+
120
+ lines = str.gsub(/(.{1,#{available_length}})(?:\s+|\Z)|(.{1,#{available_length}})/m, "\\1\\2\n").split("\n")
121
+
122
+ lines.map! do |line|
123
+ line.strip.ljust(available_length).rjust(available_length + left_padding.to_i)
124
+ end
125
+
126
+ lines[0] = lines.first.strip.ljust(length).rjust(length + first_line_padding.to_i)
127
+ lines.join("\n")
128
+ end
129
+ end
130
+
131
+ end
132
+ end
133
+
134
+ Dir.glob(File.dirname(__FILE__) + '/text_task_writer/decorators/*_decorator.rb') do |decorator|
135
+ require decorator
136
+ end
137
+
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ require 'rainbow'
3
+
4
+ module Medo
5
+ class TextTaskWriter
6
+ module Decorators
7
+ module ColorsDecorator
8
+ extend Support::Decorator
9
+
10
+ private
11
+
12
+ def present_tasks(tasks)
13
+ super.each { |t| TaskColors.decorate(t) }
14
+ end
15
+
16
+ module TaskColors
17
+ extend Support::Decorator
18
+
19
+ def to_s(length = nil)
20
+ c = components(length)
21
+ "#{c.done.color(:red)} #{c.description.color(:black)} #{c.time.color(:yellow)}#{c.notes}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,37 @@
1
+ require 'medo/support/decorator'
2
+
3
+ module Medo
4
+ class TextTaskWriter
5
+ module Decorators
6
+ module NumbersDecorator
7
+ extend Support::Decorator
8
+
9
+ def present_tasks(tasks)
10
+ max_tasks_count = [active_tasks.count, completed_tasks.count].max
11
+ super.each_with_index.map do |t, i|
12
+ TaskNumbers.decorate(t, i + 1, max_tasks_count)
13
+ end
14
+ end
15
+
16
+ module TaskNumbers
17
+ extend Support::Decorator
18
+
19
+ after_decorate do |number, total|
20
+ @number = number
21
+ @number_length = total.to_s.size
22
+ end
23
+
24
+ def number
25
+ format = "%-#{@number_length + 1}s"
26
+ format % (@task.done? ? "" : "#@number.")
27
+ end
28
+
29
+ def done
30
+ "#{number} #{super}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,59 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+ require 'medo/file_task_storage'
3
+
4
+ describe FileTaskStorage do
5
+ before(:each) do
6
+ verbosity, $VERBOSE = $VERBOSE, nil
7
+ load 'fileutils.rb'
8
+ $VERBOSE = verbosity
9
+ end
10
+
11
+ describe "#initialize" do
12
+ it "should touch a storage file, so it is created if not exists" do
13
+ FileUtils.should_receive(:touch)
14
+ FileTaskStorage.new("f")
15
+ end
16
+ end
17
+
18
+ describe "#read" do
19
+ it "return empty array on error" do
20
+ class Reader; def read; raise; end; end
21
+ FileTaskStorage.new("f", Reader, Class).read.should == []
22
+ end
23
+ end
24
+
25
+ describe "#write" do
26
+ class StubWriter
27
+ def initialize(t); @t = t; end
28
+ def write; @t.write "foo"; end
29
+ def add_tasks(t); end
30
+ end
31
+
32
+ it "should write to tempfile only" do
33
+ storage = FileTaskStorage.new("f", anything, StubWriter)
34
+ tempfile = mock
35
+ storage.stub(:tempfile => tempfile)
36
+ tempfile.should_receive(:write).with("foo")
37
+ tempfile.should_receive(:close)
38
+ storage.write(anything)
39
+ end
40
+
41
+ it "should close tempfile if error occured" do
42
+ class Writer
43
+ def write
44
+ raise
45
+ end
46
+ end
47
+
48
+ tempfile = mock
49
+ storage = FileTaskStorage.new("f", Class, Writer)
50
+ storage.stub(:tempfile => tempfile)
51
+ tempfile.should_receive(:close)
52
+ storage.write([]) rescue nil #nothing
53
+ end
54
+ end
55
+
56
+ after(:each) do
57
+ FileUtils.rm("f") if File.exists?("f")
58
+ end
59
+ end