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