medo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +12 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.txt +20 -0
- data/README.md +14 -0
- data/Rakefile +29 -0
- data/VERSION +1 -0
- data/bin/medo +59 -0
- data/features/add_notes.feature +18 -0
- data/features/add_task.feature +12 -0
- data/features/list_tasks.feature +25 -0
- data/features/step_definitions/add_task_steps.rb +3 -0
- data/features/step_definitions/list_tasks_steps.rb +6 -0
- data/features/support/env.rb +14 -0
- data/lib/medo.rb +6 -0
- data/lib/medo/commands/clear.rb +9 -0
- data/lib/medo/commands/delete.rb +13 -0
- data/lib/medo/commands/done.rb +13 -0
- data/lib/medo/commands/list.rb +16 -0
- data/lib/medo/commands/new.rb +15 -0
- data/lib/medo/commands/note.rb +18 -0
- data/lib/medo/commands/show.rb +19 -0
- data/lib/medo/file_task_storage.rb +53 -0
- data/lib/medo/json_task_reader.rb +31 -0
- data/lib/medo/json_task_writer.rb +33 -0
- data/lib/medo/support.rb +6 -0
- data/lib/medo/support/decorator.rb +40 -0
- data/lib/medo/task.rb +63 -0
- data/lib/medo/task_reader.rb +8 -0
- data/lib/medo/task_writer.rb +21 -0
- data/lib/medo/terminal.rb +6 -0
- data/lib/medo/text_task_writer.rb +137 -0
- data/lib/medo/text_task_writer/decorators/colors_decorator.rb +28 -0
- data/lib/medo/text_task_writer/decorators/numbers_decorator.rb +37 -0
- data/spec/lib/file_task_storage_spec.rb +59 -0
- data/spec/lib/json_task_reader_spec.rb +26 -0
- data/spec/lib/json_task_writer_spec.rb +32 -0
- data/spec/lib/task_reader_spec.rb +9 -0
- data/spec/lib/task_spec.rb +101 -0
- data/spec/lib/task_writer_spec.rb +18 -0
- data/spec/lib/text_task_writer_spec.rb +106 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/task_stubs_spec_helper.rb +41 -0
- 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
|
+
|
data/lib/medo/support.rb
ADDED
@@ -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
|
data/lib/medo/task.rb
ADDED
@@ -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,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,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
|