plan 0.0.1
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/bin/plan +4 -0
- data/lib/plan.rb +7 -0
- data/lib/plan/advice.rb +17 -0
- data/lib/plan/cli.rb +168 -0
- data/lib/plan/item.rb +114 -0
- data/lib/plan/version.rb +5 -0
- data/spec/spec_helper.rb +1 -0
- metadata +64 -0
data/bin/plan
ADDED
data/lib/plan.rb
ADDED
data/lib/plan/advice.rb
ADDED
data/lib/plan/cli.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Plan
|
4
|
+
|
5
|
+
class CLI
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def run(args)
|
10
|
+
begin
|
11
|
+
command args.first, args[1..-1]
|
12
|
+
rescue Plan::Advice => e
|
13
|
+
e.lines.each do |line|
|
14
|
+
puts "\e[31m[uh-oh]\e[0m #{line}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# decide what to do
|
20
|
+
def command(command, paths)
|
21
|
+
# default is list
|
22
|
+
return list([]) if command.nil?
|
23
|
+
# choose other command
|
24
|
+
case command
|
25
|
+
when 'create' then create paths
|
26
|
+
when 'list' then list paths
|
27
|
+
when 'finish' then finish paths
|
28
|
+
when 'unfinish' then unfinish paths
|
29
|
+
when 'cleanup' then cleanup paths
|
30
|
+
when 'help' then help
|
31
|
+
else unknown_command(command)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
COMMAND_GLOSSARY = {
|
36
|
+
'create' => 'create a new item',
|
37
|
+
'list' => 'list items',
|
38
|
+
'finish' => 'mark an item finished',
|
39
|
+
'unfinish' => 'mark an item unfinished',
|
40
|
+
'cleanup' => 'remove finished items from view',
|
41
|
+
'help' => 'display a list of commands'
|
42
|
+
}
|
43
|
+
|
44
|
+
# display a list of help
|
45
|
+
def help
|
46
|
+
puts "plan #{Plan::VERSION} - john crepezzi - http://github.com/seejohnrun/plan"
|
47
|
+
COMMAND_GLOSSARY.each do |cmd, description|
|
48
|
+
puts "\e[0;33m#{cmd}\e[0m - #{description}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Remove all finished items that are descendents
|
53
|
+
def cleanup(paths)
|
54
|
+
item = path_tree.descend(paths)
|
55
|
+
item.cleanup
|
56
|
+
save_path_tree
|
57
|
+
# print what happened here
|
58
|
+
print_depth item
|
59
|
+
end
|
60
|
+
|
61
|
+
# Mark a task or group of tasks as "unfinished"
|
62
|
+
def unfinish(paths)
|
63
|
+
if paths.empty?
|
64
|
+
raise Plan::Advice.new 'please drill down to a level to unfinish'
|
65
|
+
end
|
66
|
+
# go to the right depth and unfinish
|
67
|
+
item = path_tree.descend(paths)
|
68
|
+
item.unfinish!
|
69
|
+
save_path_tree
|
70
|
+
# print what happened here
|
71
|
+
print_depth item
|
72
|
+
end
|
73
|
+
|
74
|
+
# Mark a task or group of tasks as "finished"
|
75
|
+
def finish(paths)
|
76
|
+
if paths.empty?
|
77
|
+
raise Plan::Advice.new 'please drill down to a level to finish'
|
78
|
+
end
|
79
|
+
# descend and finish
|
80
|
+
item = path_tree.descend(paths)
|
81
|
+
item.finish!
|
82
|
+
save_path_tree
|
83
|
+
# print what happened here
|
84
|
+
print_depth item
|
85
|
+
end
|
86
|
+
|
87
|
+
# list things at a certain depth
|
88
|
+
def list(paths)
|
89
|
+
item = path_tree.descend(paths)
|
90
|
+
if item.visible_child_count == 0
|
91
|
+
raise Plan::Advice.new 'no events here - create some with `plan create`'
|
92
|
+
end
|
93
|
+
print_depth item
|
94
|
+
end
|
95
|
+
|
96
|
+
# create a new todo
|
97
|
+
def create(paths)
|
98
|
+
if paths.empty?
|
99
|
+
raise Plan::Advice.new 'please provide something to create'
|
100
|
+
end
|
101
|
+
# descend to the right depth
|
102
|
+
item = path_tree.descend(paths[0..-2])
|
103
|
+
# and then create
|
104
|
+
if item.children.any? { |c| !c.hidden? && c.has_label?(paths[-1]) }
|
105
|
+
raise Plan::Advice.new "duplicate entry at level: #{paths[-1]}"
|
106
|
+
else
|
107
|
+
item.children << Item.new(paths[-1])
|
108
|
+
save_path_tree
|
109
|
+
# and say what happened
|
110
|
+
print_depth item
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
DATA_STORE = ENV['PLAN_DATA_PATH'] || "#{ENV['HOME']}/plan"
|
117
|
+
DATE_FORMAT = '%b. %e, %Y %l:%M %P'
|
118
|
+
|
119
|
+
def unknown_command(cmd)
|
120
|
+
raise Plan::Advice.new "unknown command: #{cmd}. try `plan help` for options."
|
121
|
+
end
|
122
|
+
|
123
|
+
# print the item and its descendents
|
124
|
+
def print_depth(item)
|
125
|
+
return if item.hidden?
|
126
|
+
print_item item, 0
|
127
|
+
list_recur_print item, 2
|
128
|
+
end
|
129
|
+
|
130
|
+
# Used by #print_depth to print its tree
|
131
|
+
def list_recur_print(item, desc = 0)
|
132
|
+
item.children.each do |child|
|
133
|
+
next if child.hidden?
|
134
|
+
print_item child, desc
|
135
|
+
list_recur_print(child, desc + 2)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# output an individual item
|
140
|
+
def print_item(item, desc = 0)
|
141
|
+
if item.finished?
|
142
|
+
puts "\e[1;30m#{'-' * desc}#{desc > 0 ? " #{item.label}" : item.label} \e[1;31m(finished @ #{item.finished.strftime(DATE_FORMAT)})\e[0m"
|
143
|
+
else
|
144
|
+
puts "\e[1;30m#{'-' * desc}\e[0m#{desc > 0 ? " #{item.label}" : item.label}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Save any changes to the tree
|
149
|
+
def save_path_tree
|
150
|
+
file = File.open(DATA_STORE, 'w')
|
151
|
+
file.write path_tree.dump.to_json
|
152
|
+
file.close
|
153
|
+
end
|
154
|
+
|
155
|
+
# Get the path tree from the data file
|
156
|
+
def path_tree
|
157
|
+
@path_tree ||= if File.exists?(DATA_STORE)
|
158
|
+
Item.load JSON.parse(File.read(DATA_STORE))
|
159
|
+
else
|
160
|
+
Item.new 'plan'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
data/lib/plan/item.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
module Plan
|
2
|
+
|
3
|
+
class Item
|
4
|
+
|
5
|
+
attr_reader :label, :finished, :children
|
6
|
+
|
7
|
+
# Create a new item
|
8
|
+
def initialize(label, finished = nil, hidden = nil)
|
9
|
+
@label = label.strip
|
10
|
+
@finished = finished.is_a?(Fixnum) ? Time.at(finished) : nil
|
11
|
+
@hidden = hidden
|
12
|
+
@children = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# determine whether a label is a duplicate of this item's label
|
16
|
+
# downcases to get rid of basic mistakes
|
17
|
+
def has_label?(other_label)
|
18
|
+
label.downcase == other_label.downcase
|
19
|
+
end
|
20
|
+
|
21
|
+
# for fuzzy matching on descending - so you can do things like
|
22
|
+
# todo create so hi - instead of
|
23
|
+
# todo create something hi
|
24
|
+
def has_label_like?(other_label)
|
25
|
+
label.downcase.include? other_label.downcase
|
26
|
+
end
|
27
|
+
|
28
|
+
# count of the visible children
|
29
|
+
def visible_child_count
|
30
|
+
children.count { |c| !c.hidden? }
|
31
|
+
end
|
32
|
+
|
33
|
+
# remove all finished items from this tree, by hiding them
|
34
|
+
def cleanup
|
35
|
+
@hidden = true if finished?
|
36
|
+
children.each { |c| c.cleanup }
|
37
|
+
end
|
38
|
+
|
39
|
+
# mark a finish date for this item
|
40
|
+
def finish!(at = Time.now)
|
41
|
+
@finished = at unless finished? # don't overwrite
|
42
|
+
children.each { |c| c.finish!(at) unless c.hidden? } # and finish each child
|
43
|
+
end
|
44
|
+
|
45
|
+
# mark a finished item as unfinished
|
46
|
+
# and all descendents
|
47
|
+
def unfinish!
|
48
|
+
@finished = nil
|
49
|
+
children.each { |c| c.unfinish! unless c.hidden? }
|
50
|
+
end
|
51
|
+
|
52
|
+
# return a boolean indicating whether or not this item is hidden
|
53
|
+
def hidden?
|
54
|
+
!!@hidden
|
55
|
+
end
|
56
|
+
|
57
|
+
# return whether this item is finished
|
58
|
+
def finished?
|
59
|
+
!!@finished && children.all? { |c| c.finished? || c.hidden? }
|
60
|
+
end
|
61
|
+
|
62
|
+
# recursively descent through children until you find the given item
|
63
|
+
def descend(paths)
|
64
|
+
return self if paths.empty?
|
65
|
+
# prefer exact matches
|
66
|
+
next_items = children.select { |c| !c.hidden? && c.has_label?(paths.first) }
|
67
|
+
# fall back on approximates
|
68
|
+
if next_items.empty?
|
69
|
+
next_items = children.select { |c| !c.hidden? && c.has_label_like?(paths.first) }
|
70
|
+
end
|
71
|
+
# give an error if we have no matches
|
72
|
+
if next_items.empty?
|
73
|
+
lines = []
|
74
|
+
lines << "no match for #{paths.first}"
|
75
|
+
unless children.empty?
|
76
|
+
lines << "available options are: #{children.reject(&:hidden?).map(&:label).join(', ')}"
|
77
|
+
end
|
78
|
+
raise Plan::Advice.new *lines
|
79
|
+
end
|
80
|
+
# give an error if we have too many matches
|
81
|
+
if next_items.count > 1
|
82
|
+
lines = []
|
83
|
+
lines << "ambiguous match for '#{paths.first}' - please choose one of:"
|
84
|
+
next_items.each do |np|
|
85
|
+
lines << "* #{np.label}"
|
86
|
+
end
|
87
|
+
raise Plan::Advice.new *lines
|
88
|
+
end
|
89
|
+
# and off we go, continuing to descent
|
90
|
+
next_items.first.descend(paths[1..-1])
|
91
|
+
end
|
92
|
+
|
93
|
+
# Load a Item from a data hash
|
94
|
+
def self.load(data)
|
95
|
+
item = Item.new data['label'], data['finished'], data['hidden']
|
96
|
+
data['children'] && data['children'].each do |child|
|
97
|
+
item.children << Item.load(child)
|
98
|
+
end
|
99
|
+
item
|
100
|
+
end
|
101
|
+
|
102
|
+
# dump a nested representation of this item
|
103
|
+
def dump
|
104
|
+
data = {}
|
105
|
+
data['label'] = label
|
106
|
+
data['finished'] = finished.nil? ? nil : finished.to_i
|
107
|
+
data['children'] = children.map(&:dump)
|
108
|
+
data['hidden'] = true if hidden?
|
109
|
+
data
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
data/lib/plan/version.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/plan'
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: plan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Crepezzi
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-27 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70151867057320 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70151867057320
|
25
|
+
description: plan is a simple command-line todo-list manager
|
26
|
+
email: john.crepezzi@gmail.com
|
27
|
+
executables:
|
28
|
+
- plan
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- lib/plan/advice.rb
|
33
|
+
- lib/plan/cli.rb
|
34
|
+
- lib/plan/item.rb
|
35
|
+
- lib/plan/version.rb
|
36
|
+
- lib/plan.rb
|
37
|
+
- bin/plan
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
homepage: http://github.com/seejohnrun/plan
|
40
|
+
licenses: []
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project: plan
|
59
|
+
rubygems_version: 1.8.10
|
60
|
+
signing_key:
|
61
|
+
specification_version: 3
|
62
|
+
summary: simple command-line todo-list manager
|
63
|
+
test_files:
|
64
|
+
- spec/spec_helper.rb
|