ursm-ditz 0.4
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/Changelog +35 -0
- data/README.txt +127 -0
- data/Rakefile +33 -0
- data/ReleaseNotes +50 -0
- data/bin/ditz +213 -0
- data/contrib/completion/_ditz.zsh +29 -0
- data/contrib/completion/ditz.bash +22 -0
- data/lib/component.rhtml +22 -0
- data/lib/ditz.rb +56 -0
- data/lib/hook.rb +67 -0
- data/lib/html.rb +69 -0
- data/lib/index.rhtml +113 -0
- data/lib/issue.rhtml +111 -0
- data/lib/issue_table.rhtml +33 -0
- data/lib/lowline.rb +202 -0
- data/lib/model-objects.rb +314 -0
- data/lib/model.rb +208 -0
- data/lib/operator.rb +549 -0
- data/lib/plugins/git.rb +114 -0
- data/lib/plugins/issue-claiming.rb +92 -0
- data/lib/release.rhtml +69 -0
- data/lib/style.css +127 -0
- data/lib/trollop.rb +518 -0
- data/lib/unassigned.rhtml +31 -0
- data/lib/util.rb +57 -0
- data/lib/vendor/yaml_waml.rb +28 -0
- data/lib/view.rb +16 -0
- data/lib/views.rb +136 -0
- data/man/ditz.1 +38 -0
- metadata +90 -0
data/lib/html.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Ditz
|
4
|
+
|
5
|
+
## pass through any variables needed for template generation, and add a bunch
|
6
|
+
## of HTML formatting utility methods.
|
7
|
+
class ErbHtml
|
8
|
+
def initialize template_dir, links, binding={}
|
9
|
+
@template_dir = template_dir
|
10
|
+
@links = links
|
11
|
+
@binding = binding
|
12
|
+
end
|
13
|
+
|
14
|
+
## return an ErbHtml object that has the current binding plus extra_binding merged in
|
15
|
+
def clone_for_binding extra_binding={}
|
16
|
+
extra_binding.empty? ? self : ErbHtml.new(@template_dir, @links, @binding.merge(extra_binding))
|
17
|
+
end
|
18
|
+
|
19
|
+
def render_template template_name, extra_binding={}
|
20
|
+
if extra_binding.empty?
|
21
|
+
@@erbs ||= {}
|
22
|
+
@@erbs[template_name] ||= ERB.new IO.read(File.join(@template_dir, "#{template_name}.rhtml"))
|
23
|
+
@@erbs[template_name].result binding
|
24
|
+
else
|
25
|
+
clone_for_binding(extra_binding).render_template template_name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def render_string s, extra_binding={}
|
30
|
+
if extra_binding.empty?
|
31
|
+
ERB.new(s).result binding
|
32
|
+
else
|
33
|
+
clone_for_binding(extra_binding).render_string s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
###
|
38
|
+
### the following methods are meant to be called from the ERB itself
|
39
|
+
###
|
40
|
+
|
41
|
+
def h o; o.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">") end
|
42
|
+
def t o; o.strftime "%Y-%m-%d %H:%M %Z" end
|
43
|
+
def p o; "<p>" + h(o.to_s).gsub("\n\n", "</p><p>") + "</p>" end
|
44
|
+
def obscured_email e; h e.gsub(/@.*?(>|$)/, "@...\\1") end
|
45
|
+
def link_to o, name
|
46
|
+
dest = @links[o]
|
47
|
+
dest = o if dest.nil? && o.is_a?(String)
|
48
|
+
raise ArgumentError, "no link for #{o.inspect}" unless dest
|
49
|
+
"<a href=\"#{dest}\">#{name}</a>"
|
50
|
+
end
|
51
|
+
def fancy_issue_link_for i
|
52
|
+
"<span class=\"issuestatus_#{i.status}\">" + link_to(i, "[#{i.title}]") + "</span>"
|
53
|
+
end
|
54
|
+
|
55
|
+
def link_issue_names project, s
|
56
|
+
project.issues.inject(s) do |s, i|
|
57
|
+
s.gsub(/\b#{i.name}\b/, fancy_issue_link_for(i))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
## render a nested ERB
|
62
|
+
alias :render :render_template
|
63
|
+
|
64
|
+
def method_missing meth, *a
|
65
|
+
@binding.member?(meth) ? @binding[meth] : super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/lib/index.rhtml
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
+
|
4
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
5
|
+
<head>
|
6
|
+
<title><%= project.name %> Issue Tracker</title>
|
7
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
|
8
|
+
<link rel="stylesheet" href="style.css" type="text/css" />
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
|
12
|
+
<h1><%= project.name %> Issue Tracker</h1>
|
13
|
+
|
14
|
+
<h2>Upcoming Releases</h2>
|
15
|
+
<% if upcoming_releases.empty? %>
|
16
|
+
<p>No upcoming releases.</p>
|
17
|
+
<% else %>
|
18
|
+
<ul>
|
19
|
+
<% upcoming_releases.each do |r| %>
|
20
|
+
<%
|
21
|
+
issues = project.issues_for_release r
|
22
|
+
num_done = issues.count_of { |i| i.closed? }
|
23
|
+
pct_done = issues.size == 0 ? 100 : (100.0 * num_done / issues.size)
|
24
|
+
open_issues = project.group_issues(issues.select { |i| i.open? })
|
25
|
+
%>
|
26
|
+
<li>
|
27
|
+
<%= link_to r, "Release #{r.name}" %>:
|
28
|
+
<% if issues.empty? %>
|
29
|
+
no issues
|
30
|
+
<% else %>
|
31
|
+
<%= sprintf "%.0f%%", pct_done %> complete;
|
32
|
+
<% if open_issues.empty? %>
|
33
|
+
ready for release!
|
34
|
+
<% else %>
|
35
|
+
<%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
|
36
|
+
<% end %>
|
37
|
+
<% end %>
|
38
|
+
</li>
|
39
|
+
<% end %>
|
40
|
+
</ul>
|
41
|
+
<% end %>
|
42
|
+
|
43
|
+
<h2>Past Releases</h2>
|
44
|
+
<% if past_releases.empty? %>
|
45
|
+
<p>No past releases.</p>
|
46
|
+
<% else %>
|
47
|
+
<ul>
|
48
|
+
<% past_releases.sort_by { |r| r.release_time }.reverse.each do |r| %>
|
49
|
+
<li><%= link_to r, "Release #{r.name}" %>, released <%= r.release_time.pretty_date %>. </li>
|
50
|
+
<% end %>
|
51
|
+
</ul>
|
52
|
+
<% end %>
|
53
|
+
|
54
|
+
<h2>Unassigned issues</h2>
|
55
|
+
<%
|
56
|
+
issues = project.unassigned_issues
|
57
|
+
open_issues = issues.select { |i| i.open? }
|
58
|
+
%>
|
59
|
+
<p>
|
60
|
+
<% if issues.empty? %>
|
61
|
+
No unassigned issues.
|
62
|
+
<% else %>
|
63
|
+
<%= link_to "unassigned", "unassigned issue".pluralize(issues.size).capitalize %>; <%= open_issues.size.to_pretty_s %> of them open.
|
64
|
+
<% end %>
|
65
|
+
</p>
|
66
|
+
|
67
|
+
<% if components.size > 1 %>
|
68
|
+
<h2>Open Issues by component</h2>
|
69
|
+
<ul>
|
70
|
+
<% components.each do |c| %>
|
71
|
+
<%
|
72
|
+
open_issues = project.group_issues(project.issues_for_component(c).select { |i| i.open? })
|
73
|
+
%>
|
74
|
+
<li>
|
75
|
+
<% if open_issues.empty? %>
|
76
|
+
<span class="dimmed">
|
77
|
+
<%= link_to c, c.name %>:
|
78
|
+
no open issues.
|
79
|
+
</span>
|
80
|
+
<% else %>
|
81
|
+
<%= link_to c, c.name %>:
|
82
|
+
<%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
|
83
|
+
<% end %>
|
84
|
+
</li>
|
85
|
+
<% end %>
|
86
|
+
</ul>
|
87
|
+
<% end %>
|
88
|
+
|
89
|
+
<h2>Recent activity</h2>
|
90
|
+
|
91
|
+
<table>
|
92
|
+
<% project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
|
93
|
+
flatten_one_level.
|
94
|
+
sort_by { |e| e.first.first }.
|
95
|
+
reverse[0 ... 10].
|
96
|
+
each do |(date, who, what, comment), i| %>
|
97
|
+
<tr>
|
98
|
+
<td><%= date.pretty_date %></td>
|
99
|
+
<td class="issuename">
|
100
|
+
<%= link_issue_names project, h(i.name) %>
|
101
|
+
<%= what %> by <%= who.shortened_email %></td>
|
102
|
+
</tr>
|
103
|
+
<% if comment && comment =~ /\S/ %>
|
104
|
+
<tr><td></td><td><i><%= link_issue_names project, h(comment) %></i></td></tr>
|
105
|
+
<% end %>
|
106
|
+
<% end %>
|
107
|
+
</table>
|
108
|
+
|
109
|
+
<p class="footer">Generated by <a
|
110
|
+
href="http://ditz.rubyforge.org/">ditz</a>.</p>
|
111
|
+
|
112
|
+
</body>
|
113
|
+
</html>
|
data/lib/issue.rhtml
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
+
|
4
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
5
|
+
<head>
|
6
|
+
<title><%= issue.title %></title>
|
7
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
|
8
|
+
<link rel="stylesheet" href="style.css" type="text/css" />
|
9
|
+
</head>
|
10
|
+
|
11
|
+
<body>
|
12
|
+
|
13
|
+
<div><%= link_to "index", "« #{project.name} project page" %></div>
|
14
|
+
|
15
|
+
<h1><%= link_issue_names project, issue.title %></h1>
|
16
|
+
|
17
|
+
<%= link_issue_names project, p(issue.desc) %>
|
18
|
+
|
19
|
+
<table>
|
20
|
+
<tr>
|
21
|
+
<td class="attrname">Id:</td>
|
22
|
+
<td class="attrval"><%= issue.id %></td>
|
23
|
+
</tr>
|
24
|
+
|
25
|
+
<tr>
|
26
|
+
<td class="attrname">Type:</td>
|
27
|
+
<td class="attrval"><%= issue.type %></td>
|
28
|
+
</tr>
|
29
|
+
|
30
|
+
<tr>
|
31
|
+
<td class="attrname">Creation time:</td>
|
32
|
+
<td class="attrval"><%= issue.creation_time %></td>
|
33
|
+
</tr>
|
34
|
+
|
35
|
+
<tr>
|
36
|
+
<td class="attrname">Creator:</td>
|
37
|
+
<td class="attrval"><%=obscured_email issue.reporter %></td>
|
38
|
+
</tr>
|
39
|
+
|
40
|
+
<% unless issue.references.empty? %>
|
41
|
+
<tr>
|
42
|
+
<td class="attrname">References:</td>
|
43
|
+
<td class="attrval">
|
44
|
+
<% issue.references.each_with_index do |r, i| %>
|
45
|
+
[<%= i + 1 %>] <%= link_to r, r %><br/>
|
46
|
+
<% end %>
|
47
|
+
</td>
|
48
|
+
</tr>
|
49
|
+
|
50
|
+
<% end %>
|
51
|
+
|
52
|
+
<tr>
|
53
|
+
<td class="attrname">Release:</td>
|
54
|
+
<td class="attrval">
|
55
|
+
<% if release %>
|
56
|
+
<%= link_to release, release.name %>
|
57
|
+
<% if release.released? %>
|
58
|
+
(released <%= release.release_time.pretty_date %>)
|
59
|
+
<% else %>
|
60
|
+
(unreleased)
|
61
|
+
<% end %>
|
62
|
+
<% else %>
|
63
|
+
<%= link_to "unassigned", "unassigned" %>
|
64
|
+
<% end %>
|
65
|
+
</td>
|
66
|
+
</tr>
|
67
|
+
|
68
|
+
<tr>
|
69
|
+
<td class="attrname">Component:</td>
|
70
|
+
<td class="attrval"><%= link_to component, component.name %></td>
|
71
|
+
</tr>
|
72
|
+
|
73
|
+
<tr>
|
74
|
+
<td class="attrname">Status:</td>
|
75
|
+
<td class="attrval">
|
76
|
+
<%= issue.status_string %><% if issue.closed? %>: <%= issue.disposition_string %><% end %>
|
77
|
+
</td>
|
78
|
+
</tr>
|
79
|
+
|
80
|
+
<%= extra_summary_html %>
|
81
|
+
|
82
|
+
</table>
|
83
|
+
|
84
|
+
<%= extra_details_html %>
|
85
|
+
|
86
|
+
<h2>Issue log</h2>
|
87
|
+
|
88
|
+
<table>
|
89
|
+
<% issue.log_events.each_with_index do |(time, who, what, comment), i| %>
|
90
|
+
<% if i % 2 == 0 %>
|
91
|
+
<tr class="logentryeven">
|
92
|
+
<% else %>
|
93
|
+
<tr class="logentryodd">
|
94
|
+
<% end %>
|
95
|
+
<td class="logtime"><%=t time %></td>
|
96
|
+
<td class="logwho"><%=obscured_email who %></td>
|
97
|
+
<td class="logwhat"><%=h what %></td>
|
98
|
+
</tr>
|
99
|
+
<tr><td colspan="3" class="logcomment">
|
100
|
+
<% if comment.empty? %>
|
101
|
+
<% else %>
|
102
|
+
<%= link_issue_names project, p(comment) %>
|
103
|
+
<% end %>
|
104
|
+
</td></tr>
|
105
|
+
<% end %>
|
106
|
+
</table>
|
107
|
+
|
108
|
+
<p class="footer">Generated by <a
|
109
|
+
href="http://ditz.rubyforge.org/">ditz</a>.</p>
|
110
|
+
</body>
|
111
|
+
</html>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<table>
|
2
|
+
<% issues.sort_by { |i| i.sort_order }.each do |i| %>
|
3
|
+
<tr>
|
4
|
+
<td class="issuestatus_<%= i.status %>">
|
5
|
+
<% if i.closed? %>
|
6
|
+
<%= i.disposition_string %>
|
7
|
+
<% else %>
|
8
|
+
<%= i.status_string %>
|
9
|
+
<% end %>
|
10
|
+
</td>
|
11
|
+
<td class="issuename">
|
12
|
+
<%= fancy_issue_link_for i %>
|
13
|
+
<%= i.bug? ? '(bug)' : '' %>
|
14
|
+
</td>
|
15
|
+
<% if show_release %>
|
16
|
+
<td class="issuerelease">
|
17
|
+
<% if i.release %>
|
18
|
+
<% r = project.release_for i.release %>
|
19
|
+
in <%= link_to r, "release #{i.release}" %>
|
20
|
+
(<%= r.status %>)
|
21
|
+
<% else %>
|
22
|
+
<% end %>
|
23
|
+
</td>
|
24
|
+
<% end %>
|
25
|
+
<% if show_component %>
|
26
|
+
<td class="issuecomponent">
|
27
|
+
component <%= link_to project.component_for(i.component), i.component %>
|
28
|
+
</td>
|
29
|
+
<% end %>
|
30
|
+
</tr>
|
31
|
+
<% end %>
|
32
|
+
</table>
|
33
|
+
|
data/lib/lowline.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require "util"
|
3
|
+
|
4
|
+
class Numeric
|
5
|
+
def to_pretty_s
|
6
|
+
%w(zero one two three four five six seven eight nine ten)[self] || to_s
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class String
|
11
|
+
def dcfirst; self[0..0].downcase + self[1..-1] end
|
12
|
+
def blank?; self =~ /\A\s*\z/ end
|
13
|
+
def underline; self + "\n" + ("-" * self.length) end
|
14
|
+
def pluralize n, b=true
|
15
|
+
s = (n == 1 ? self : (self == 'bugfix' ? 'bugfixes' : self + "s")) # oh yeah
|
16
|
+
b ? n.to_pretty_s + " " + s : s
|
17
|
+
end
|
18
|
+
def shortened_email; self =~ /<?(\S+?)@.+/ ? $1 : self end
|
19
|
+
def multistrip; strip.gsub(/\n\n+/, "\n\n") end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Array
|
23
|
+
def listify prefix=""
|
24
|
+
return "" if empty?
|
25
|
+
"\n" +
|
26
|
+
map_with_index { |x, i| x.to_s.gsub(/^/, "#{prefix}#{i + 1}. ") }.
|
27
|
+
join("\n")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Time
|
32
|
+
def pretty; strftime "%c" end
|
33
|
+
def pretty_date; strftime "%Y-%m-%d" end
|
34
|
+
def ago
|
35
|
+
diff = (Time.now - self).to_i.abs
|
36
|
+
if diff < 60
|
37
|
+
"second".pluralize diff
|
38
|
+
elsif diff < 60*60*3
|
39
|
+
"minute".pluralize(diff / 60)
|
40
|
+
elsif diff < 60*60*24*3
|
41
|
+
"hour".pluralize(diff / (60*60))
|
42
|
+
elsif diff < 60*60*24*7*2
|
43
|
+
"day".pluralize(diff / (60*60*24))
|
44
|
+
elsif diff < 60*60*24*7*8
|
45
|
+
"week".pluralize(diff / (60*60*24*7))
|
46
|
+
elsif diff < 60*60*24*7*52
|
47
|
+
"month".pluralize(diff / (60*60*24*7*4))
|
48
|
+
else
|
49
|
+
"year".pluralize(diff / (60*60*24*7*52))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module Lowline
|
55
|
+
def run_editor
|
56
|
+
f = Tempfile.new "ditz"
|
57
|
+
yield f
|
58
|
+
f.close
|
59
|
+
|
60
|
+
editor = ENV["EDITOR"] || "/usr/bin/vi"
|
61
|
+
cmd = "#{editor} #{f.path.inspect}"
|
62
|
+
|
63
|
+
mtime = File.mtime f.path
|
64
|
+
system cmd or raise Error, "cannot execute command: #{cmd.inspect}"
|
65
|
+
|
66
|
+
File.mtime(f.path) == mtime ? nil : f.path
|
67
|
+
end
|
68
|
+
|
69
|
+
def ask q, opts={}
|
70
|
+
default_s = case opts[:default]
|
71
|
+
when nil; nil
|
72
|
+
when ""; " (enter for none)"
|
73
|
+
else; " (enter for #{opts[:default].inspect})"
|
74
|
+
end
|
75
|
+
|
76
|
+
tail = case q
|
77
|
+
when /[:?]$/; " "
|
78
|
+
when /[:?]\s+$/; ""
|
79
|
+
else; ": "
|
80
|
+
end
|
81
|
+
|
82
|
+
while true
|
83
|
+
prompt = [q, default_s, tail].compact.join
|
84
|
+
if Ditz::has_readline?
|
85
|
+
ans = Readline::readline(prompt)
|
86
|
+
else
|
87
|
+
print prompt
|
88
|
+
ans = STDIN.gets.strip
|
89
|
+
end
|
90
|
+
if opts[:default]
|
91
|
+
ans = opts[:default] if ans.blank?
|
92
|
+
else
|
93
|
+
next if ans.blank? && !opts[:empty_ok]
|
94
|
+
end
|
95
|
+
break ans unless (opts[:restrict] && ans !~ opts[:restrict])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def ask_via_editor q, default=nil
|
100
|
+
fn = run_editor do |f|
|
101
|
+
f.puts q.gsub(/^/, "## ")
|
102
|
+
f.puts "##"
|
103
|
+
f.puts "## Enter your text below. Lines starting with a '#' will be ignored."
|
104
|
+
f.puts
|
105
|
+
f.puts default if default
|
106
|
+
end
|
107
|
+
return unless fn
|
108
|
+
IO.read(fn).gsub(/^#.*$/, "").multistrip
|
109
|
+
end
|
110
|
+
|
111
|
+
def ask_multiline q
|
112
|
+
puts "#{q} (ctrl-d, ., or /stop to stop, /edit to edit, /reset to reset):"
|
113
|
+
ans = ""
|
114
|
+
while true
|
115
|
+
if Ditz::has_readline?
|
116
|
+
line = Readline::readline('> ')
|
117
|
+
else
|
118
|
+
(line = STDIN.gets) && line.strip!
|
119
|
+
end
|
120
|
+
if line
|
121
|
+
if Ditz::has_readline?
|
122
|
+
Readline::HISTORY.push(line)
|
123
|
+
end
|
124
|
+
case line
|
125
|
+
when /^\.$/, "/stop"
|
126
|
+
break
|
127
|
+
when "/reset"
|
128
|
+
return ask_multiline(q)
|
129
|
+
when "/edit"
|
130
|
+
return ask_via_editor(q, ans)
|
131
|
+
else
|
132
|
+
ans << line + "\n"
|
133
|
+
end
|
134
|
+
else
|
135
|
+
puts
|
136
|
+
break
|
137
|
+
end
|
138
|
+
end
|
139
|
+
ans.multistrip
|
140
|
+
end
|
141
|
+
|
142
|
+
def ask_yon q
|
143
|
+
while true
|
144
|
+
print "#{q} (y/n): "
|
145
|
+
a = STDIN.gets.strip
|
146
|
+
break a if a =~ /^[yn]$/i
|
147
|
+
end =~ /y/i
|
148
|
+
end
|
149
|
+
|
150
|
+
def ask_for_many plural_name, name=nil
|
151
|
+
name ||= plural_name.gsub(/s$/, "")
|
152
|
+
stuff = []
|
153
|
+
|
154
|
+
while true
|
155
|
+
puts
|
156
|
+
puts "Current #{plural_name}:"
|
157
|
+
if stuff.empty?
|
158
|
+
puts "None!"
|
159
|
+
else
|
160
|
+
stuff.each_with_index { |c, i| puts " #{i + 1}) #{c}" }
|
161
|
+
end
|
162
|
+
puts
|
163
|
+
ans = ask "(A)dd #{name}, (r)emove #{name}, or (d)one"
|
164
|
+
case ans
|
165
|
+
when "a", "A"
|
166
|
+
ans = ask "#{name.capitalize} name", ""
|
167
|
+
stuff << ans unless ans =~ /^\s*$/
|
168
|
+
when "r", "R"
|
169
|
+
ans = ask "Remove which #{name}? (1--#{stuff.size})"
|
170
|
+
stuff.delete_at(ans.to_i - 1) if ans
|
171
|
+
when "d", "D"
|
172
|
+
break
|
173
|
+
end
|
174
|
+
end
|
175
|
+
stuff
|
176
|
+
end
|
177
|
+
|
178
|
+
def ask_for_selection stuff, name, to_string=:to_s
|
179
|
+
puts "Choose a #{name}:"
|
180
|
+
stuff.each_with_index do |c, i|
|
181
|
+
pretty = case to_string
|
182
|
+
when block_given? && to_string # heh
|
183
|
+
yield c
|
184
|
+
when Symbol
|
185
|
+
c.send to_string
|
186
|
+
when Proc
|
187
|
+
to_string.call c
|
188
|
+
else
|
189
|
+
raise ArgumentError, "unknown to_string argument type; expecting Proc or Symbol"
|
190
|
+
end
|
191
|
+
puts " #{i + 1}) #{pretty}"
|
192
|
+
end
|
193
|
+
|
194
|
+
j = while true
|
195
|
+
i = ask "#{name.capitalize} (1--#{stuff.size})"
|
196
|
+
break i.to_i if i && (1 .. stuff.size).member?(i.to_i)
|
197
|
+
end
|
198
|
+
|
199
|
+
stuff[j - 1]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|