gwtf 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,68 @@
1
+ #!/usr/bin/env ruby
2
+ # 1.9 adds realpath to resolve symlinks; 1.8 doesn't
3
+ # have this method, so we add it so we get resolved symlinks
4
+ # and compatibility
5
+ unless File.respond_to? :realpath
6
+ class File #:nodoc:
7
+ def self.realpath path
8
+ return realpath(File.readlink(path)) if symlink?(path)
9
+ path
10
+ end
11
+ end
12
+ end
13
+ $: << File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib')
14
+
15
+ require 'rubygems'
16
+ require 'gli'
17
+ require 'gwtf'
18
+
19
+ include GLI
20
+
21
+ program_desc 'Go With The Flow'
22
+
23
+ version Gwtf::VERSION
24
+
25
+ config_file '.gwtf'
26
+
27
+ desc 'Path to storage directory'
28
+ long_desc "Where to store the database of items"
29
+ default_value File.join(Etc.getpwuid.dir, ".gwtf.d")
30
+ arg_name "data_dir"
31
+ flag [:data, :d]
32
+
33
+ desc 'Active project'
34
+ long_desc "Change the active project"
35
+ default_value "default"
36
+ arg_name "project"
37
+ flag [:project, :p]
38
+
39
+ Gwtf.each_command do |command|
40
+ load command
41
+ end
42
+
43
+ pre do |global,command,options,args|
44
+ project_dir = File.join(global[:data], global[:project])
45
+
46
+ unless File.directory?(project_dir)
47
+ puts "Created a new project %s in %s" % [global[:project], project_dir]
48
+ Gwtf::Items.setup(project_dir)
49
+ end
50
+
51
+ @items = Gwtf::Items.new(project_dir)
52
+
53
+ true
54
+ end
55
+
56
+ post do |global,command,options,args|
57
+ # Post logic here
58
+ # Use skips_post before a command to skip this
59
+ # block on that command only
60
+ end
61
+
62
+ on_error do |exception|
63
+ # Error logic here
64
+ # return false to skip default error handling
65
+ true
66
+ end
67
+
68
+ exit GLI.run(ARGV)
@@ -0,0 +1,40 @@
1
+ module Gwtf
2
+ require 'gwtf/items'
3
+ require 'gwtf/item'
4
+ require 'gwtf/version'
5
+ require 'json'
6
+ require 'yaml'
7
+ require 'fileutils'
8
+ require 'tempfile'
9
+
10
+ def self.each_command
11
+ commands_dir = File.join(File.dirname(__FILE__), "gwtf", "commands")
12
+ Dir.entries(commands_dir).grep(/_command.rb$/).sort.each do |command|
13
+ yield File.join(commands_dir, command)
14
+ end
15
+ end
16
+
17
+ # borrowed from ohai, thanks Adam.
18
+ def self.seconds_to_human(seconds)
19
+ days = seconds.to_i / 86400
20
+ seconds -= 86400 * days
21
+
22
+ hours = seconds.to_i / 3600
23
+ seconds -= 3600 * hours
24
+
25
+ minutes = seconds.to_i / 60
26
+ seconds -= 60 * minutes
27
+
28
+ if days > 1
29
+ return sprintf("%d days %d hours %d minutes %d seconds", days, hours, minutes, seconds)
30
+ elsif days == 1
31
+ return sprintf("%d day %d hours %d minutes %d seconds", days, hours, minutes, seconds)
32
+ elsif hours > 0
33
+ return sprintf("%d hours %d minutes %d seconds", hours, minutes, seconds)
34
+ elsif minutes > 0
35
+ return sprintf("%d minutes %d seconds", minutes, seconds)
36
+ else
37
+ return sprintf("%d seconds", seconds)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+ desc 'Mark an item as done'
2
+ arg_name 'Item id'
3
+ command [:done, :d] do |c|
4
+ c.action do |global_options,options,args|
5
+ raise "Please specify an item ID to mark as done" if args.empty?
6
+
7
+ item = @items.load_item(args.first)
8
+ item.close
9
+ item.save
10
+
11
+ puts "Marked item #{args.first} as done"
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ desc 'Edit an item using EDITOR'
2
+ arg_name 'Item id'
3
+ command [:edit, :vi, :e] do |c|
4
+ c.action do |global_options,options,args|
5
+ raise "Please specify an item ID to edit" if args.empty?
6
+ raise "EDITOR environment variable should be set" unless ENV.include?("EDITOR")
7
+
8
+ item = @items.load_item(args.first)
9
+
10
+ descr_sep = "== EDIT BETWEEN THESE LINES =="
11
+
12
+ temp_item = {"description" => "#{descr_sep}\n#{item.description}\n#{descr_sep}", "subject" => item.subject}
13
+
14
+ begin
15
+ tmp = Tempfile.new("gwtf")
16
+ tmp.write(temp_item.to_yaml)
17
+ tmp.rewind
18
+ system("%s %s" % [ENV["EDITOR"], tmp.path])
19
+ edited_item = YAML.load_file(tmp.path)
20
+ ensure
21
+ tmp.close
22
+ tmp.unlink
23
+ end
24
+
25
+ item.subject = edited_item["subject"] if edited_item["subject"]
26
+
27
+ if edited_item["description"] =~ /#{descr_sep}\n(.+)\n#{descr_sep}/m
28
+ item.description = $1
29
+ end
30
+
31
+ item.save
32
+
33
+ puts "Item #{item.item_id} has been saved"
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ desc 'List active items'
2
+ command [:list, :ls, :l] do |c|
3
+ c.desc 'Also show closed items'
4
+ c.default_value false
5
+ c.switch [:all, :a]
6
+
7
+ c.action do |global_options,options,args|
8
+ count = {"open" => 0, "closed" => 0}
9
+
10
+ @items.each_item do |item|
11
+ count[ item[:status] ] += 1
12
+
13
+ item.has_description ? id = "*#{item.item_id.to_s}" : id = item.item_id.to_s
14
+
15
+ puts "%5s %-3s%8s" % [ id, "", item.subject] if item.open?
16
+ puts "%5s %-3s%8s" % [ id, "C", item.subject] if (item.closed? && options[:all])
17
+ end
18
+
19
+ puts
20
+ puts "Items: %d / %d" % [count["open"], count["open"] + count["closed"]]
21
+ end
22
+ end
23
+
24
+
@@ -0,0 +1,36 @@
1
+ desc 'Log work performed against an item'
2
+ arg_name 'id log text'
3
+ command :log do |c|
4
+ c.desc 'Days spent'
5
+ c.default_value 0
6
+ c.flag [:days, :d]
7
+
8
+ c.desc 'Hours spent'
9
+ c.default_value 0
10
+ c.flag [:hour, :h]
11
+
12
+ c.desc 'Minutes spent'
13
+ c.default_value 0
14
+ c.flag [:min, :m]
15
+
16
+ c.action do |global_options,options,args|
17
+ raise "Please specify an item ID to work on" if args.empty?
18
+ raise "Please supply log text" if args.size == 1
19
+
20
+ elapsed_time = Float(options[:days]) * 60 * 60 * 24
21
+ elapsed_time += Float(options[:hour]) * 60 * 60
22
+ elapsed_time += Float(options[:min]) * 60
23
+
24
+ item = @items.load_item(args.first)
25
+
26
+ description = args[1..-1].join(" ")
27
+
28
+ item.record_work(description, elapsed_time)
29
+
30
+ item.save
31
+
32
+ puts "Logged '#{description}' against item #{item.item_id} for #{Gwtf.seconds_to_human(elapsed_time)}"
33
+ end
34
+ end
35
+
36
+
@@ -0,0 +1,31 @@
1
+ desc 'Create an item'
2
+ arg_name 'Short item description'
3
+ command [:new, :n] do |c|
4
+ c.desc 'Invoke EDITOR to provide a long form description'
5
+ c.default_value false
6
+ c.switch [:edit, :e]
7
+
8
+ c.action do |global_options,options,args|
9
+ if options[:edit]
10
+ raise "EDITOR is not set" unless ENV.include?("EDITOR")
11
+
12
+ begin
13
+ tmp = Tempfile.new("gwtf")
14
+ system("%s %s" % [ENV["EDITOR"], tmp.path])
15
+ description = tmp.read.chomp
16
+ ensure
17
+ tmp.close
18
+ tmp.unlink
19
+ end
20
+ else
21
+ description = nil
22
+ end
23
+
24
+ item = @items.new_item
25
+ item.subject = args.join(" ")
26
+ item.description = description if description
27
+ item.save
28
+
29
+ puts "Item #{item.item_id} saved"
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ desc 'Re-open a previously closed item'
2
+ arg_name 'Item id'
3
+ command [:open, :o] do |c|
4
+ c.action do |global_options,options,args|
5
+ raise "Please specify an item ID to mark re-open" if args.empty?
6
+
7
+ item = @items.load_item(args.first)
8
+
9
+ raise "Item #{args.first} was already open" if item.open?
10
+
11
+ item.open
12
+ item.save
13
+
14
+ puts "Item #{args.first} as been re-opened"
15
+ end
16
+ end
17
+
18
+
@@ -0,0 +1,43 @@
1
+ desc 'Start work on an item in a subshell'
2
+ arg_name 'Item id'
3
+ command :shell do |c|
4
+ c.action do |global_options,options,args|
5
+ raise "Please specify an item ID to work on" if args.empty?
6
+ raise "SHELL is not set, cannot create sub shell" unless ENV.include?("SHELL")
7
+
8
+ start_time = Time.now
9
+ item = @items.load_item(args.first)
10
+
11
+ puts "Starting work on item #{item.item_id}, exit to record the action and time"
12
+
13
+ ENV["GWTF_ITEM"] = item.item_id.to_s
14
+ ENV["GWTF_PROJECT"] = global_options[:project]
15
+ ENV["GWTF_SUBJECT"] = item.subject
16
+
17
+ system(ENV["SHELL"])
18
+
19
+ elapsed_time = Time.now - start_time
20
+
21
+ STDOUT.sync = true
22
+
23
+ print "Optional description for work log: "
24
+
25
+ begin
26
+ description = STDIN.gets.chomp
27
+ rescue Exception
28
+ puts
29
+ description = ""
30
+ end
31
+
32
+ description = "Worked in a subshell" if description == ""
33
+ description = description + " (#{Gwtf.seconds_to_human(elapsed_time.round)})"
34
+
35
+ item.record_work(description, elapsed_time.round)
36
+
37
+ item.save
38
+
39
+ puts "Recorded #{elapsed_time} seconds of work against item #{item.item_id}"
40
+ end
41
+ end
42
+
43
+
@@ -0,0 +1,44 @@
1
+ desc 'Show an item'
2
+ arg_name 'Item ID'
3
+ command [:show, :s] do |c|
4
+ c.action do |global_options,options,args|
5
+ raise "Please supply an item ID to show" if args.empty?
6
+
7
+ item = @items.load_item(args.first)
8
+
9
+ time_worked = item.work_log.inject(0) do |result, log|
10
+ begin
11
+ result + log["elapsed"]
12
+ rescue
13
+ result
14
+ end
15
+ end
16
+
17
+ puts " ID: %s" % [ item.item_id ]
18
+ puts " Subject: %s" % [ item.subject ]
19
+ puts " Status: %s" % [ item.status ]
20
+ puts "Time Worked: %s" % [ Gwtf.seconds_to_human(time_worked) ]
21
+ puts " Created: %s" % [ Time.parse(item.created_at).strftime("%D %R") ]
22
+ puts " Closed: %s" % [ Time.parse(item.closed_at).strftime("%D %R") ] if item.closed?
23
+
24
+ if item.has_description?
25
+ puts
26
+ puts "Description:"
27
+
28
+ item.description.split("\n").each do |line|
29
+ puts "%13s%s" % [ "", line]
30
+ end
31
+
32
+ puts
33
+ end
34
+
35
+ time_spent = 0
36
+
37
+ item.work_log.each_with_index do |log, idx|
38
+ puts
39
+ puts "Work Log: " if idx == 0
40
+
41
+ puts "%27s %s" % [Time.parse(log["time"]).strftime("%D %R"), log["text"]]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,157 @@
1
+ module Gwtf
2
+ class Item
3
+ attr_accessor :file
4
+
5
+ def initialize(file=nil)
6
+ @item = default_item
7
+ @file = file
8
+
9
+ load_item if file
10
+ end
11
+
12
+ def open?
13
+ @item["status"] == "open"
14
+ end
15
+
16
+ def closed?
17
+ !open?
18
+ end
19
+
20
+ def load_item
21
+ raise "A file to read from has not been specified" unless @file
22
+
23
+ read_item = JSON.parse(File.read(@file))
24
+
25
+ @item.merge!(read_item)
26
+ end
27
+
28
+ def backup_dir
29
+ File.join(File.dirname(file), "backups")
30
+ end
31
+
32
+ def save(backup=true)
33
+ raise "No item_id set, cannot save item" unless @item["item_id"]
34
+
35
+ if backup && File.exist?(@file)
36
+ backup_name = File.basename(@file) + "-" + Time.now.to_f.to_s
37
+
38
+ FileUtils.mv(@file, File.join(backup_dir, backup_name))
39
+ end
40
+
41
+ File.open(@file, "w") do |f|
42
+ f.print to_json
43
+ end
44
+
45
+ @file
46
+ end
47
+
48
+ def default_item
49
+ {"description" => nil,
50
+ "subject" => nil,
51
+ "created_at" => Time.now,
52
+ "edited_at" => nil,
53
+ "closed_at" => nil,
54
+ "status" => "open",
55
+ "item_id" => nil,
56
+ "work_log" => []}
57
+ end
58
+
59
+ def to_hash
60
+ @item
61
+ end
62
+
63
+ def record_work(text, elapsed=0)
64
+ update_property(:edited_at, Time.now)
65
+
66
+ @item["work_log"] << {"text" => text, "time" => Time.now, "elapsed" => elapsed}
67
+ end
68
+
69
+ def open
70
+ update_property(:closed_at, nil)
71
+ update_property(:status, "open")
72
+ end
73
+
74
+ def close
75
+ update_property(:closed_at, Time.now)
76
+ update_property(:status, "closed")
77
+ end
78
+
79
+ def to_json
80
+ JSON.pretty_generate(@item)
81
+ end
82
+
83
+ def to_yaml
84
+ @item.to_yaml
85
+ end
86
+
87
+ def update_property(property, value)
88
+ property = property.to_s
89
+
90
+ raise "No such property: #{property}" unless @item.include?(property)
91
+
92
+ @item["edited_at"] = Time.now
93
+ @item[property] = value
94
+ end
95
+
96
+ def [](property)
97
+ property = property.to_s
98
+
99
+ raise "No such property: #{property}" unless @item.include?(property)
100
+
101
+ @item[property]
102
+ end
103
+
104
+ def []=(property, value)
105
+ update_property(property, value)
106
+ end
107
+
108
+ # simple read from the class:
109
+ #
110
+ # >> i.description
111
+ # => "Sample Item"
112
+ #
113
+ # method like writes:
114
+ #
115
+ # >> i.description "This is a test"
116
+ # => "This is a test"
117
+ #
118
+ # assignment
119
+ #
120
+ # >> i.description = "This is a test"
121
+ # => "This is a test"
122
+ #
123
+ # boolean
124
+ #
125
+ # >> i.description?
126
+ # => false
127
+ # >> i.description "foo"
128
+ # => foo
129
+ # >> i.has_description?
130
+ # => true
131
+ # >> i.has_description
132
+ # => true
133
+ def method_missing(method, *args)
134
+ method = method.to_s
135
+
136
+ if @item.include?(method)
137
+ if args.empty?
138
+ return @item[method]
139
+ else
140
+ return update_property(method, args.first)
141
+ end
142
+
143
+ elsif method =~ /^has_(.+?)\?*$/
144
+ return !@item[$1].nil?
145
+
146
+ elsif method =~ /^(.+)\?$/
147
+ return !@item[$1].nil?
148
+
149
+ elsif method =~ /^(.+)=$/
150
+ property = $1
151
+ return update_property(property, args.first) if @item.include?(property)
152
+ end
153
+
154
+ raise NameError, "undefined local variable or method `#{method}'"
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,71 @@
1
+ module Gwtf
2
+ class Items
3
+ def self.config_file(data_dir)
4
+ File.expand_path(File.join(data_dir, "..", "gwtf.json"))
5
+ end
6
+
7
+ def self.setup(data_dir)
8
+ require 'fileutils'
9
+
10
+ raise "#{data_dir} already exist" if File.exist?(data_dir)
11
+
12
+ FileUtils.mkdir_p(File.join(data_dir, "backups"))
13
+ FileUtils.mkdir_p(File.join(data_dir, "archive"))
14
+ FileUtils.mkdir_p(File.join(data_dir, "garbage"))
15
+
16
+ unless File.exist?(config_file(data_dir))
17
+ File.open(config_file(data_dir), "w") do |f|
18
+ f.print({"next_item" => 0}.to_json)
19
+ end
20
+ end
21
+ end
22
+
23
+ def initialize(data_dir)
24
+ raise "Data directory #{data_dir} does not exist" unless File.directory?(data_dir)
25
+
26
+ @data_dir = data_dir
27
+ @config = read_config
28
+ end
29
+
30
+ def new_item
31
+ item = Item.new
32
+ item.item_id = @config["next_item"]
33
+ item.file = File.join(@data_dir, "#{item.item_id}.gwtf")
34
+
35
+ @config["next_item"] += 1
36
+ save_config
37
+
38
+ item
39
+ end
40
+
41
+ def load_item(item)
42
+ raise "Item #{item} does not exist" unless File.exist?(file_for_item(item))
43
+
44
+ Item.new(file_for_item(item))
45
+ end
46
+
47
+ def read_config
48
+ JSON.parse(File.read(Items.config_file(@data_dir)))
49
+ end
50
+
51
+ def save_config
52
+ raise "Config has not been loaded" unless @config
53
+
54
+ File.open(Items.config_file(@data_dir), "w") do |f|
55
+ f.print(@config.to_json)
56
+ end
57
+ end
58
+
59
+ def items
60
+ Dir.entries(@data_dir).grep(/\.gwtf$/).sort.map{|i| File.basename(i, ".gwtf")}
61
+ end
62
+
63
+ def file_for_item(item)
64
+ File.join(@data_dir, "#{item}.gwtf")
65
+ end
66
+
67
+ def each_item
68
+ items.each {|item| yield load_item(item) }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,3 @@
1
+ module Gwtf
2
+ VERSION = '0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gwtf
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - R.I.Pienaar
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-03-10 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rake
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rdoc
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ description: A Unix cli centric todo manager
50
+ email: rip@devco.net
51
+ executables:
52
+ - gwtf
53
+ extensions: []
54
+
55
+ extra_rdoc_files: []
56
+
57
+ files:
58
+ - bin/gwtf
59
+ - lib/gwtf/commands/show_command.rb
60
+ - lib/gwtf/commands/open_command.rb
61
+ - lib/gwtf/commands/shell_command.rb
62
+ - lib/gwtf/commands/done_command.rb
63
+ - lib/gwtf/commands/log_command.rb
64
+ - lib/gwtf/commands/edit_command.rb
65
+ - lib/gwtf/commands/new_command.rb
66
+ - lib/gwtf/commands/list_command.rb
67
+ - lib/gwtf/version.rb
68
+ - lib/gwtf/item.rb
69
+ - lib/gwtf/items.rb
70
+ - lib/gwtf.rb
71
+ has_rdoc: true
72
+ homepage: http://devco.net/
73
+ licenses: []
74
+
75
+ post_install_message:
76
+ rdoc_options: []
77
+
78
+ require_paths:
79
+ - lib
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 3
96
+ segments:
97
+ - 0
98
+ version: "0"
99
+ requirements: []
100
+
101
+ rubyforge_project:
102
+ rubygems_version: 1.3.7
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: Go With The Flow
106
+ test_files: []
107
+