gwtf 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+