sapis 0.1.0
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.
- checksums.yaml +7 -0
- data/README.md +179 -0
- data/lib/sapis/bash_helper.rb +80 -0
- data/lib/sapis/computations_helper.rb +95 -0
- data/lib/sapis/concurrency_helper.rb +104 -0
- data/lib/sapis/configuration_helper.rb +128 -0
- data/lib/sapis/desktop_helper.rb +50 -0
- data/lib/sapis/generic_helper.rb +241 -0
- data/lib/sapis/gnome_helper.rb +36 -0
- data/lib/sapis/graphing_helper.rb +202 -0
- data/lib/sapis/interactions_helper.rb +148 -0
- data/lib/sapis/multimedia_helper.rb +281 -0
- data/lib/sapis/sqlite_layer.rb +166 -0
- data/lib/sapis/system_helper.rb +141 -0
- data/lib/sapis/version.rb +3 -0
- data/lib/sapis.rb +33 -0
- metadata +141 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
|
|
3
|
+
<generic_helper.rb> - Part of Sav's APIs.
|
|
4
|
+
Copyright (C) 2011 Saverio Miroddi
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
=end
|
|
20
|
+
|
|
21
|
+
require 'date'
|
|
22
|
+
|
|
23
|
+
module GenericHelper
|
|
24
|
+
def self.do_retry(options={}, &block)
|
|
25
|
+
max_retries = options[:max_retries] || 3
|
|
26
|
+
sleep_interval = options[:sleep ] || 0
|
|
27
|
+
|
|
28
|
+
current_retries = 0
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
yield
|
|
32
|
+
rescue
|
|
33
|
+
current_retries += 1
|
|
34
|
+
|
|
35
|
+
if current_retries <= max_retries
|
|
36
|
+
sleep sleep_interval
|
|
37
|
+
retry
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Trivial version. Can easily be done with a regex.
|
|
45
|
+
#
|
|
46
|
+
def self.camelize(string)
|
|
47
|
+
buffer = ""
|
|
48
|
+
capitalize_next = true
|
|
49
|
+
|
|
50
|
+
string.chars.each do | char |
|
|
51
|
+
if char == '_'
|
|
52
|
+
capitalize_next = true
|
|
53
|
+
elsif capitalize_next
|
|
54
|
+
buffer << char.upcase
|
|
55
|
+
capitalize_next = false
|
|
56
|
+
else
|
|
57
|
+
buffer << char
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
buffer
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# options:
|
|
65
|
+
#
|
|
66
|
+
# :past: [false] the date can be future (supported only by some cases)
|
|
67
|
+
#
|
|
68
|
+
# Format:
|
|
69
|
+
#
|
|
70
|
+
# 'YYYY-MM-DD', 'YYYYMMDD', 'MMM/DD/YYYY', 'MMM/D(D)', 'D(D)/MMM' (curr. year), 'MMDD' (curr. year)
|
|
71
|
+
# 'to[day]', 'ye[sterday]', 'mon' (monday), 'tue-' (last tuesday),
|
|
72
|
+
# '+3' (today+3), '-2' (today-2)
|
|
73
|
+
#
|
|
74
|
+
# Note that the easiest way to pass '-<value>' in bash, is to use '--' (end of options).
|
|
75
|
+
#
|
|
76
|
+
def decode_date(encoded_date, future: true)
|
|
77
|
+
case encoded_date.downcase
|
|
78
|
+
# YYYY-MM-DD
|
|
79
|
+
when %r{^(\d\d\d\d)-(\d\d)-(\d\d)$}
|
|
80
|
+
Date.new($1.to_i, $2.to_i, $3.to_i)
|
|
81
|
+
# YYYYMMDD
|
|
82
|
+
when /^(\d\d\d\d)(\d\d)(\d\d)$/
|
|
83
|
+
Date.new($1.to_i, $2.to_i, $3.to_i)
|
|
84
|
+
# MMM/DD/YYYY
|
|
85
|
+
when %r{^(\w{3})/(\d\d)/(\d\d\d\d)$}
|
|
86
|
+
month_index_for_time = Date.strptime($1, "%b").month
|
|
87
|
+
Date.new($3.to_i, month_index_for_time, $2.to_i)
|
|
88
|
+
# MMM/D(D) (+future)
|
|
89
|
+
when %r{^(\w{3})\/(\d{1,2})$}
|
|
90
|
+
month_index_for_time = Date.strptime($1, "%b").month
|
|
91
|
+
Date.new(Date.today.year, month_index_for_time, $2.to_i)
|
|
92
|
+
.then { !future && it > Date.today ? it << 12 : it }
|
|
93
|
+
# D(D)/MMM/YYYY
|
|
94
|
+
when %r{^(\d{1,2})/(\w{3})/(\d\d\d\d)$}
|
|
95
|
+
month_index_for_time = Date.strptime($2, "%b").month
|
|
96
|
+
Date.new($3.to_i, month_index_for_time, $1.to_i)
|
|
97
|
+
# D(D)/MMM
|
|
98
|
+
when %r{^(\d{1,2})\/(\w{3})$}
|
|
99
|
+
month_index_for_time = Date.strptime($2, "%b").month
|
|
100
|
+
Date.new(Date.today.year, month_index_for_time, $1.to_i)
|
|
101
|
+
# MMDD
|
|
102
|
+
when /^(\d\d)(\d\d)$/
|
|
103
|
+
Date.new(Time.now.year, $1.to_i, $2.to_i)
|
|
104
|
+
when 'to', 'today'
|
|
105
|
+
Date.today
|
|
106
|
+
when 'ye', 'yesterday'
|
|
107
|
+
Date.today - 1
|
|
108
|
+
when /^(sun|mon|tue|wed|thu|fri|sat)$/
|
|
109
|
+
diff = (Date.strptime($1, "%a") - Date.today).to_i
|
|
110
|
+
diff += 7 if diff <= 0
|
|
111
|
+
Date.today + diff
|
|
112
|
+
when /^(sun|mon|tue|wed|thu|fri|sat)-$/
|
|
113
|
+
diff = (Date.strptime($1, "%a") - Date.today).to_i
|
|
114
|
+
diff -= 7 if diff >= 0
|
|
115
|
+
Date.today + diff
|
|
116
|
+
when /^\+(\d+)$/
|
|
117
|
+
Date.today + $1.to_i
|
|
118
|
+
when /^-(\d+)$/
|
|
119
|
+
Date.today - $1.to_i
|
|
120
|
+
else
|
|
121
|
+
raise "Unrecognized date: #{ encoded_date }"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Params:
|
|
126
|
+
#
|
|
127
|
+
# day, start, end
|
|
128
|
+
#
|
|
129
|
+
# Format:
|
|
130
|
+
#
|
|
131
|
+
# * DAY: 'YYYY-MM-DD', 'YYYYMMDD', 'MMDD', 'to[day]', 'ye[sterday]', 'thu' (thursday of the current week), 'mon-' (last monday), 'tue+' (next tuesday), 'MMM/DD'
|
|
132
|
+
# * START: 'HH', 'HH:MM'
|
|
133
|
+
# * END: '3d', '2h', '45m', 'HH', 'HH:MM'
|
|
134
|
+
#
|
|
135
|
+
# Possible combinations:
|
|
136
|
+
#
|
|
137
|
+
# * (nothing) all today
|
|
138
|
+
# * DAY on the day, for all day
|
|
139
|
+
# * DAY,END from day to end, for all day
|
|
140
|
+
# * DAY,START,END on the day, from start to end
|
|
141
|
+
# * START,END today, from start to end
|
|
142
|
+
#
|
|
143
|
+
# Note that the 'current week' starts on Sunday.
|
|
144
|
+
#==
|
|
145
|
+
# In the regexes, numbered repetitions ('{n}') are not used for consistency.
|
|
146
|
+
#
|
|
147
|
+
# The Date class pretty much sucks. Between the other things, it doesn't accept string
|
|
148
|
+
# values when instantiating.
|
|
149
|
+
#
|
|
150
|
+
def decode_interval(*daytime)
|
|
151
|
+
current_token = daytime.shift
|
|
152
|
+
|
|
153
|
+
if current_token
|
|
154
|
+
base_day = decode_date(current_token)
|
|
155
|
+
|
|
156
|
+
if base_day.nil?
|
|
157
|
+
base_day = Date.today
|
|
158
|
+
daytime.unshift(current_token)
|
|
159
|
+
end
|
|
160
|
+
else
|
|
161
|
+
base_day = Date.today
|
|
162
|
+
daytime.unshift(current_token)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
raise "Wrong start year format. Non-consumed tokens: #{ daytime }" if base_day.nil?
|
|
166
|
+
|
|
167
|
+
current_token = daytime.shift
|
|
168
|
+
|
|
169
|
+
# The next token could be both a start (e.g. DAY,START,END) or an END (e.g. DAY,END).
|
|
170
|
+
# Since we can't rely on character patterns because of the HH:MM case, we rely on the
|
|
171
|
+
# number of tokens: START can be present only if at this point there are two tokens.
|
|
172
|
+
#
|
|
173
|
+
if daytime.size == 1
|
|
174
|
+
case current_token
|
|
175
|
+
when /^(\d{1,2})$/
|
|
176
|
+
start_daytime = Time.local(base_day.year, base_day.month, base_day.day, $1)
|
|
177
|
+
when /^(\d{1,2}):(\d\d)$/
|
|
178
|
+
start_daytime = Time.local(base_day.year, base_day.month, base_day.day, $1, $2)
|
|
179
|
+
else
|
|
180
|
+
daytime.unshift(current_token)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
all_day = false
|
|
184
|
+
else
|
|
185
|
+
start_daytime = Time.local(base_day.year, base_day.month, base_day.day)
|
|
186
|
+
all_day = true
|
|
187
|
+
daytime.unshift(current_token)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
raise "Wrong start daytime format. Non-consumed tokens: #{ daytime }" if start_daytime.nil?
|
|
191
|
+
|
|
192
|
+
current_token = daytime.shift
|
|
193
|
+
|
|
194
|
+
if current_token
|
|
195
|
+
if all_day
|
|
196
|
+
# Only full days allowed in case of all-day timespan
|
|
197
|
+
#
|
|
198
|
+
if current_token =~ /^(\d+)d$/
|
|
199
|
+
end_daytime = add_days(start_daytime, $1.to_i)
|
|
200
|
+
else
|
|
201
|
+
daytime.unshift(current_token)
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
case current_token.downcase
|
|
205
|
+
when /^(\d+)d$/
|
|
206
|
+
end_daytime = start_daytime + $1.to_i * 24 * 60 * 60
|
|
207
|
+
when /^(\d+)h$/
|
|
208
|
+
end_daytime = start_daytime + $1.to_i * 60 * 60
|
|
209
|
+
when /^(\d+)m$/
|
|
210
|
+
end_daytime = start_daytime + $1.to_i * 60
|
|
211
|
+
when /^(\d{1,2})$/
|
|
212
|
+
end_daytime = Time.local(start_daytime.year, start_daytime.month, start_daytime.day, $1)
|
|
213
|
+
when /^(\d{1,2}):(\d\d)$/
|
|
214
|
+
end_daytime = Time.local(start_daytime.year, start_daytime.month, start_daytime.day, $1, $2)
|
|
215
|
+
else
|
|
216
|
+
daytime.unshift(current_token)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
else
|
|
220
|
+
# We're here if none of START/END have been passed; we default to +1 day.
|
|
221
|
+
# At this point, there are no tokens to unshift
|
|
222
|
+
#
|
|
223
|
+
end_daytime = add_days(start_daytime, 1)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
raise "Wrong end daytime format. Non-consumed tokens: #{ daytime }" if end_daytime.nil?
|
|
227
|
+
|
|
228
|
+
raise "Non-consumed tokens found: #{ daytime }" if daytime.size > 0
|
|
229
|
+
|
|
230
|
+
[start_daytime, end_daytime, all_day]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
# Add the given days, ignoring the daylight saving.
|
|
236
|
+
#
|
|
237
|
+
def add_days(base_time, days)
|
|
238
|
+
result_date = Date.new(base_time.year, base_time.month, base_time.day) + days
|
|
239
|
+
Time.local(result_date.year, result_date.month, result_date.day)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
|
|
3
|
+
<gnome_helper.rb> - Part of Sav's APIs.
|
|
4
|
+
Copyright (C) 2011 Saverio Miroddi
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
=end
|
|
20
|
+
|
|
21
|
+
require_relative 'bash_helper'
|
|
22
|
+
|
|
23
|
+
module GnomeHelper
|
|
24
|
+
|
|
25
|
+
include BashHelper
|
|
26
|
+
|
|
27
|
+
def set_gnome_background( pic_path )
|
|
28
|
+
simple_bash_execute "gconftool -t string -s /desktop/gnome/background/picture_filename", pic_path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def get_gnome_background_filename
|
|
32
|
+
simple_bash_execute "gconftool -g /desktop/gnome/background/picture_filename"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
|
|
3
|
+
<graphing_helper.rb> - Part of Sav's APIs.
|
|
4
|
+
Copyright (C) 2011 Saverio Miroddi
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
=end
|
|
20
|
+
|
|
21
|
+
module GraphingHelper
|
|
22
|
+
|
|
23
|
+
require 'gruff'
|
|
24
|
+
require 'tempfile'
|
|
25
|
+
|
|
26
|
+
# Transpose from format:
|
|
27
|
+
#
|
|
28
|
+
# label1, labelN, <ignored>
|
|
29
|
+
# value, value, day1
|
|
30
|
+
# value, value, day2
|
|
31
|
+
#
|
|
32
|
+
# <source_data> is destroyed.
|
|
33
|
+
# Labels must be unique.
|
|
34
|
+
# Days can be either a string or a Date.
|
|
35
|
+
#
|
|
36
|
+
def self.transpose_data_top_headers( source_data, options={} )
|
|
37
|
+
fill_missing_days = ! options.has_key?( :fill_missing_days ) || !! options[ :fill_missing_days ]
|
|
38
|
+
|
|
39
|
+
labels = source_data.shift[ 0...-1 ]
|
|
40
|
+
|
|
41
|
+
source_data.each { | row | row[ row.size - 1 ] = Date.strptime( row.last ) unless row.last.is_a?( Date ) }
|
|
42
|
+
|
|
43
|
+
min_day = source_data.map { | row | row.last }.min
|
|
44
|
+
max_day = source_data.map { | row | row.last }.max
|
|
45
|
+
|
|
46
|
+
# Pre-fill the output matrix (hash)
|
|
47
|
+
|
|
48
|
+
output_data = labels.inject( {} ) do | current_output_data, label |
|
|
49
|
+
current_output_data[ label ] = [ nil ] * ( max_day - min_day )
|
|
50
|
+
current_output_data
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Fill the values
|
|
54
|
+
|
|
55
|
+
source_data.each do | row |
|
|
56
|
+
day = row.last
|
|
57
|
+
|
|
58
|
+
labels.each_with_index do | label, i |
|
|
59
|
+
output_data[ label ][ day - min_day ] = row[ i ]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Apply final manipulations and convert output data to array
|
|
64
|
+
|
|
65
|
+
output_data.each { | label, values | values.compact! } if ! fill_missing_days
|
|
66
|
+
|
|
67
|
+
output_data = output_data.map { | label, values | [ label, values ] }
|
|
68
|
+
days = ( min_day..max_day ).to_a
|
|
69
|
+
|
|
70
|
+
[ output_data, days ]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Transpose from format:
|
|
74
|
+
#
|
|
75
|
+
# label1, value, day1
|
|
76
|
+
# labelM, value, day2
|
|
77
|
+
# label1, value, day3
|
|
78
|
+
# labelN, value, day3
|
|
79
|
+
#
|
|
80
|
+
def self.transpose_data_left_headers( source_data )
|
|
81
|
+
source_data.each { | row | row[ row.size - 1 ] = Date.strptime( row.last ) unless row.last.is_a?( Date ) }
|
|
82
|
+
|
|
83
|
+
# Fill a map day => { label => value, ... }
|
|
84
|
+
|
|
85
|
+
data_by_day_by_label = {}
|
|
86
|
+
|
|
87
|
+
source_data.each do | label, value, day |
|
|
88
|
+
day = Date.strptime( day ) unless day.is_a?( Date )
|
|
89
|
+
|
|
90
|
+
data_by_day_by_label[ day ] ||= {}
|
|
91
|
+
data_by_day_by_label[ day ][ label ] = value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Convert to tabular form by label
|
|
95
|
+
|
|
96
|
+
labels = source_data.map { | row | row.first }.uniq
|
|
97
|
+
|
|
98
|
+
output_data = labels.inject( {} ) do | current_output_data, label |
|
|
99
|
+
current_output_data[ label ] = []
|
|
100
|
+
current_output_data
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
days = data_by_day_by_label.keys.sort
|
|
104
|
+
|
|
105
|
+
days.each do | day |
|
|
106
|
+
labels_values = data_by_day_by_label[ day ]
|
|
107
|
+
|
|
108
|
+
labels.each do | label |
|
|
109
|
+
value = labels_values[ label ]
|
|
110
|
+
output_data[ label ] << value
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Apply final manipulations and convert output data to array
|
|
115
|
+
|
|
116
|
+
output_data = output_data.map { | label, values | [ label, values ] }
|
|
117
|
+
|
|
118
|
+
[ output_data, days ]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Ouput a line graph, optionally to a file.
|
|
122
|
+
#
|
|
123
|
+
# Assumes that the row contains the headers.
|
|
124
|
+
#
|
|
125
|
+
# options:
|
|
126
|
+
# :out_file output file. if not passed, the graph is displayed live
|
|
127
|
+
#
|
|
128
|
+
def self.format_as_line_graph( data, days, options={} )
|
|
129
|
+
out_file = options[ :out_file ]
|
|
130
|
+
|
|
131
|
+
graph = Gruff::Line.new
|
|
132
|
+
|
|
133
|
+
data.each { | label_data | graph.data( *label_data ) }
|
|
134
|
+
|
|
135
|
+
graph.labels = {
|
|
136
|
+
0 => days.first.to_s,
|
|
137
|
+
days.size - 1 => days.last.to_s,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if out_file
|
|
141
|
+
graph.write( out_file )
|
|
142
|
+
else
|
|
143
|
+
Tempfile.open( 'tracking_graph' ) do | f |
|
|
144
|
+
# Base#write doesn't work because the tempfile doesn't have any extension
|
|
145
|
+
#
|
|
146
|
+
rendered_data = graph.to_blob
|
|
147
|
+
f << rendered_data
|
|
148
|
+
|
|
149
|
+
images_display_app = get_images_display_app
|
|
150
|
+
`#{ images_display_app } #{ f.path }`
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Assumes that the number of fields for each row is constant.
|
|
156
|
+
#
|
|
157
|
+
# options:
|
|
158
|
+
# :separator default: '|'
|
|
159
|
+
# :align hash { <field> => :left }. makes sense only if the first row is the headers.
|
|
160
|
+
#
|
|
161
|
+
def self.format_as_table( rows, options={} )
|
|
162
|
+
return "" if rows.empty?
|
|
163
|
+
|
|
164
|
+
separator = options[ :separator ] || '|'
|
|
165
|
+
align = options[ :align ] || {}
|
|
166
|
+
|
|
167
|
+
max_field_sizes = nil
|
|
168
|
+
alignment_symbols = nil
|
|
169
|
+
|
|
170
|
+
rows.each_with_index do | row, row_num |
|
|
171
|
+
if row_num == 0
|
|
172
|
+
max_field_sizes = row.map { | value | value.to_s.size }
|
|
173
|
+
alignment_symbols = row.map { | value | '-' if align[ value ] == :left }
|
|
174
|
+
else
|
|
175
|
+
row.each_with_index do | value, value_pos |
|
|
176
|
+
max_field_sizes[ value_pos ] = value.to_s.size if value.to_s.size > max_field_sizes[ value_pos ]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
template = separator + max_field_sizes.zip( alignment_symbols ).map { | size, alignment_symbol | " %#{ alignment_symbol }#{ size }s #{ separator }" }.join
|
|
182
|
+
|
|
183
|
+
rows.inject( "" ) do | buffer, row |
|
|
184
|
+
buffer << template % row << "\n"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
def self.get_images_display_app
|
|
191
|
+
case RUBY_PLATFORM
|
|
192
|
+
when /linux/
|
|
193
|
+
'eog'
|
|
194
|
+
when /darwin/
|
|
195
|
+
'open -W'
|
|
196
|
+
else
|
|
197
|
+
raise "Unsupported platform: #{ RUBY_PLATFORM }"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
end
|
|
202
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
|
|
3
|
+
<interactions_helper.rb> - Part of Sav's APIs.
|
|
4
|
+
Copyright (C) 2011 Saverio Miroddi
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
=end
|
|
20
|
+
|
|
21
|
+
module InteractionsHelper
|
|
22
|
+
|
|
23
|
+
require 'highline/import'
|
|
24
|
+
|
|
25
|
+
def self.secure_ask( question='Insert password: ' )
|
|
26
|
+
HighLine.new.ask( question ) { | q | q.echo = '*' }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Asks a question, optionally using a default.
|
|
30
|
+
#
|
|
31
|
+
# Format:
|
|
32
|
+
#
|
|
33
|
+
# <header> [default]?
|
|
34
|
+
#
|
|
35
|
+
def self.ask_entry( header, default=nil )
|
|
36
|
+
while true
|
|
37
|
+
print "#{ header }"
|
|
38
|
+
print " [#{ default }]" if default
|
|
39
|
+
print "? "
|
|
40
|
+
|
|
41
|
+
answer = STDIN.gets.chomp
|
|
42
|
+
|
|
43
|
+
if answer = '' && default
|
|
44
|
+
return default
|
|
45
|
+
elsif answer != ''
|
|
46
|
+
return answer
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Displays a numbered (or user-choosen) list of entries, in the format:
|
|
52
|
+
#
|
|
53
|
+
# entry_a) value_a
|
|
54
|
+
# entry_b) value_b
|
|
55
|
+
#
|
|
56
|
+
# or
|
|
57
|
+
#
|
|
58
|
+
# 0) value_a
|
|
59
|
+
# 1) value_b
|
|
60
|
+
#
|
|
61
|
+
# depending on <entries> being respectively a Hash or an Array.
|
|
62
|
+
#
|
|
63
|
+
# options:
|
|
64
|
+
# :default: default entry, if the user doesn't choose
|
|
65
|
+
# :autochoose_if_one: [false] automatically choose the entry if it's only one
|
|
66
|
+
# :filter_by: [nil] String (for simplicity), which is matched case-insensitively.
|
|
67
|
+
# If there are more matches and the pattern matches exactly one of
|
|
68
|
+
# them, it's automatically chosen.
|
|
69
|
+
#
|
|
70
|
+
def self.ask_entries_with_points( header, entries, options={} )
|
|
71
|
+
raise ArgumentError.new("No entries passed! [#{header}]") if entries.empty?
|
|
72
|
+
|
|
73
|
+
default = options[ :default ]
|
|
74
|
+
autochoose_if_one = options[ :autochoose_if_one ]
|
|
75
|
+
filtering_pattern = options[ :filter_by ]
|
|
76
|
+
|
|
77
|
+
raise "Pattern must be a String, Regexp is not supported" if filtering_pattern.is_a?( Regexp )
|
|
78
|
+
|
|
79
|
+
# Convert to Hash if it's an array
|
|
80
|
+
#
|
|
81
|
+
if entries.is_a?( Array )
|
|
82
|
+
entries = ( 0 ... entries.size ).zip( entries )
|
|
83
|
+
|
|
84
|
+
entries = entries.inject( {} ) do | current_entries, ( i, entry ) |
|
|
85
|
+
current_entries[ i.to_s ] = entry
|
|
86
|
+
current_entries
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if filtering_pattern
|
|
91
|
+
exact_matches = entries.select { | _, entry_value | entry_value.downcase == filtering_pattern.downcase }
|
|
92
|
+
|
|
93
|
+
return exact_matches.values.first if exact_matches.size == 1
|
|
94
|
+
|
|
95
|
+
entries = entries.select { | _, entry_value | entry_value.downcase.include?( filtering_pattern.downcase ) }
|
|
96
|
+
|
|
97
|
+
raise ArgumentError.new("No entries after filtering! [#{header}, #{filtering_pattern}]") if entries.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if entries.size == 1 && (autochoose_if_one || filtering_pattern)
|
|
101
|
+
return entries.values.first
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
while true
|
|
105
|
+
puts "#{ header }:"
|
|
106
|
+
|
|
107
|
+
entries.each do | point, entry |
|
|
108
|
+
print " #{ point }"
|
|
109
|
+
print default.to_s == entry ? '*' : ')'
|
|
110
|
+
puts " #{ entry }"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
answer = STDIN.gets.chomp
|
|
114
|
+
|
|
115
|
+
if answer == '' && default
|
|
116
|
+
break default
|
|
117
|
+
elsif entries.has_key?( answer )
|
|
118
|
+
break entries[ answer ]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Displays a list of entries, in the format:
|
|
124
|
+
#
|
|
125
|
+
# header: entry_a,entry_b [default]?
|
|
126
|
+
#
|
|
127
|
+
def self.ask_entries_in_line( header, entries, default=nil )
|
|
128
|
+
while true
|
|
129
|
+
print "#{ header }: "
|
|
130
|
+
|
|
131
|
+
print entries.join( ',' )
|
|
132
|
+
|
|
133
|
+
print " [#{ default }]" if default
|
|
134
|
+
|
|
135
|
+
print "? "
|
|
136
|
+
|
|
137
|
+
answer = STDIN.gets.chomp
|
|
138
|
+
|
|
139
|
+
if answer == '' && default
|
|
140
|
+
break default
|
|
141
|
+
elsif entries.include?( answer )
|
|
142
|
+
break answer
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
end
|
|
148
|
+
|