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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/plan'
4
+ Plan::CLI.run(ARGV)
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/plan/version'
2
+ require File.dirname(__FILE__) + '/plan/advice'
3
+ require File.dirname(__FILE__) + '/plan/item'
4
+
5
+ module Plan
6
+ autoload :CLI, File.dirname(__FILE__) + '/plan/cli'
7
+ end
@@ -0,0 +1,17 @@
1
+ module Plan
2
+
3
+ class Advice < Exception
4
+
5
+ attr_reader :lines
6
+
7
+ def initialize(*lines)
8
+ @lines = lines
9
+ end
10
+
11
+ def message
12
+ @lines.join("\n")
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ module Plan
2
+
3
+ VERSION = '0.0.1'
4
+
5
+ end
@@ -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