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.
- 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
|