medo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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