typingpool 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +23 -0
- data/bin/tp-assign +240 -0
- data/bin/tp-collect +50 -0
- data/bin/tp-config +114 -0
- data/bin/tp-finish +101 -0
- data/bin/tp-make +169 -0
- data/bin/tp-review +175 -0
- data/lib/typingpool/amazon.rb +732 -0
- data/lib/typingpool/app.rb +634 -0
- data/lib/typingpool/config.rb +344 -0
- data/lib/typingpool/error.rb +22 -0
- data/lib/typingpool/filer.rb +396 -0
- data/lib/typingpool/project.rb +593 -0
- data/lib/typingpool/template.rb +175 -0
- data/lib/typingpool/templates/assignment/amazon-init.js +38 -0
- data/lib/typingpool/templates/assignment/interview/nameless.html.erb +13 -0
- data/lib/typingpool/templates/assignment/interview/noisy.html.erb +12 -0
- data/lib/typingpool/templates/assignment/interview/partials/voices.html.erb +10 -0
- data/lib/typingpool/templates/assignment/interview/phone.html.erb +12 -0
- data/lib/typingpool/templates/assignment/interview.html.erb +11 -0
- data/lib/typingpool/templates/assignment/main.css +20 -0
- data/lib/typingpool/templates/assignment/partials/entry.html.erb +19 -0
- data/lib/typingpool/templates/assignment/partials/footer.html.erb +3 -0
- data/lib/typingpool/templates/assignment/partials/header.html.erb +11 -0
- data/lib/typingpool/templates/assignment/partials/labeling-example.html.erb +4 -0
- data/lib/typingpool/templates/assignment/partials/labeling.html.erb +5 -0
- data/lib/typingpool/templates/assignment/partials/length-description.html.erb +6 -0
- data/lib/typingpool/templates/assignment/partials/voices.html.erb +10 -0
- data/lib/typingpool/templates/assignment/speech.html.erb +11 -0
- data/lib/typingpool/templates/config.yml +21 -0
- data/lib/typingpool/templates/project/audio/chunks/.empty_directory +0 -0
- data/lib/typingpool/templates/project/audio/originals/.empty_directory +0 -0
- data/lib/typingpool/templates/project/data/.empty_directory +0 -0
- data/lib/typingpool/templates/project/etc/ About these files - read me.txt +8 -0
- data/lib/typingpool/templates/project/etc/audio-compat.js +25 -0
- data/lib/typingpool/templates/project/etc/player/audio-player.js +4 -0
- data/lib/typingpool/templates/project/etc/player/license.txt +19 -0
- data/lib/typingpool/templates/project/etc/player/player.swf +0 -0
- data/lib/typingpool/templates/project/etc/transcript.css +49 -0
- data/lib/typingpool/templates/transcript.html.erb +23 -0
- data/lib/typingpool/test/fixtures/amazon-question-html.html +95 -0
- data/lib/typingpool/test/fixtures/amazon-question-url.txt +1 -0
- data/lib/typingpool/test/fixtures/audio/mp3/interview.1.mp3 +0 -0
- data/lib/typingpool/test/fixtures/audio/mp3/interview.2.mp3 +0 -0
- data/lib/typingpool/test/fixtures/audio/wma/VN620007.WMA +0 -0
- data/lib/typingpool/test/fixtures/audio/wma/VN620052.WMA +0 -0
- data/lib/typingpool/test/fixtures/config-1 +20 -0
- data/lib/typingpool/test/fixtures/config-2 +25 -0
- data/lib/typingpool/test/fixtures/not_yaml.txt +4 -0
- data/lib/typingpool/test/fixtures/template-2.html.erb +10 -0
- data/lib/typingpool/test/fixtures/template-3.html.erb +22 -0
- data/lib/typingpool/test/fixtures/template.html.erb +10 -0
- data/lib/typingpool/test/fixtures/tp_collect_id.txt +1 -0
- data/lib/typingpool/test/fixtures/tp_collect_sandbox-assignment.csv +8 -0
- data/lib/typingpool/test/fixtures/tp_review_id.txt +1 -0
- data/lib/typingpool/test/fixtures/tp_review_sandbox-assignment.csv +8 -0
- data/lib/typingpool/test/fixtures/transcript-chunks.csv +226 -0
- data/lib/typingpool/test/fixtures/utf8_transcript.txt +7 -0
- data/lib/typingpool/test/fixtures/vcr/tp-collect-1.yml +2712 -0
- data/lib/typingpool/test/fixtures/vcr/tp-collect-2.yml +2718 -0
- data/lib/typingpool/test/fixtures/vcr/tp-collect-3.yml +2768 -0
- data/lib/typingpool/test/fixtures/vcr/tp-review-1.yml +570 -0
- data/lib/typingpool/test/fixtures/vcr/tp-review-2.yml +351 -0
- data/lib/typingpool/test.rb +418 -0
- data/lib/typingpool/transcript.rb +181 -0
- data/lib/typingpool/utility.rb +272 -0
- data/lib/typingpool.rb +500 -0
- data/test/make_amazon_question_fixture.rb +24 -0
- data/test/make_tp_collect_fixture_1.rb +26 -0
- data/test/make_tp_collect_fixture_2.rb +16 -0
- data/test/make_tp_collect_fixture_3.rb +15 -0
- data/test/make_tp_collect_fixture_4.rb +17 -0
- data/test/make_tp_review_fixture_1.rb +26 -0
- data/test/make_tp_review_fixture_2.rb +30 -0
- data/test/make_transcript_chunks_fixture.rb +53 -0
- data/test/test_integration_script_1_tp_config.rb +108 -0
- data/test/test_integration_script_2_tp_make.rb +119 -0
- data/test/test_integration_script_3_tp_assign.rb +152 -0
- data/test/test_integration_script_4_tp_review.rb +72 -0
- data/test/test_integration_script_5_tp_collect.rb +44 -0
- data/test/test_integration_script_6_tp_finish.rb +123 -0
- data/test/test_unit_amazon.rb +153 -0
- data/test/test_unit_config.rb +94 -0
- data/test/test_unit_filer.rb +202 -0
- data/test/test_unit_project.rb +168 -0
- data/test/test_unit_project_local.rb +68 -0
- data/test/test_unit_project_remote.rb +157 -0
- data/test/test_unit_template.rb +111 -0
- data/test/test_unit_transcript.rb +77 -0
- metadata +234 -0
@@ -0,0 +1,396 @@
|
|
1
|
+
module Typingpool
|
2
|
+
|
3
|
+
#Convenience wrapper for basic file operations. Base class for
|
4
|
+
#wrappers for specialized file types (audio, CSV) and for file
|
5
|
+
#collections.
|
6
|
+
class Filer
|
7
|
+
require 'fileutils'
|
8
|
+
include Utility::Castable
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
#Fully-expanded path to file
|
12
|
+
attr_reader :path
|
13
|
+
|
14
|
+
#Constructor.
|
15
|
+
# ==== Params
|
16
|
+
#[path] Fully expanded path to file.
|
17
|
+
#[encoding] Optional. Encoding for all text operations on the
|
18
|
+
# file. Should be compatiable with :encoding arg to
|
19
|
+
# IO.read. Default is 'UTF-8'.
|
20
|
+
def initialize(path, encoding='UTF-8')
|
21
|
+
@path = path
|
22
|
+
@encoding = encoding
|
23
|
+
end
|
24
|
+
|
25
|
+
def <=>(other)
|
26
|
+
path <=> other.path
|
27
|
+
end
|
28
|
+
|
29
|
+
#Returns contents of file or nil if the file does not exist.
|
30
|
+
def read
|
31
|
+
if File.exists? @path
|
32
|
+
IO.read(@path, :encoding => @encoding)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
#Write data to the file.
|
37
|
+
def write(data, mode='w')
|
38
|
+
File.open(@path, mode, :encoding => @encoding) do |out|
|
39
|
+
out << data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#Moves the underlying file AND updates the @path of the Filer instance.
|
44
|
+
def mv!(to)
|
45
|
+
FileUtils.mv(@path, to)
|
46
|
+
if File.directory? to
|
47
|
+
to = File.join(to, File.basename(path))
|
48
|
+
end
|
49
|
+
@path = to
|
50
|
+
end
|
51
|
+
|
52
|
+
#Returns the underlying file as an IO stream. Convenient for
|
53
|
+
#Project::Remote#put.
|
54
|
+
def to_stream(mode='r')
|
55
|
+
File.new(@path, mode, :encoding => @encoding)
|
56
|
+
end
|
57
|
+
|
58
|
+
#Filer objects always stringify to their path. We might change
|
59
|
+
#this later such that to_str gives the path but to_s gives the
|
60
|
+
#content of the file as text.
|
61
|
+
def to_s
|
62
|
+
@path
|
63
|
+
end
|
64
|
+
alias :to_str :to_s
|
65
|
+
|
66
|
+
#Returns the parent dir of the underlying file as a Filer::Dir
|
67
|
+
#instance.
|
68
|
+
def dir
|
69
|
+
Filer::Dir.new(File.dirname(@path))
|
70
|
+
end
|
71
|
+
|
72
|
+
#Cast this file into a new Filer subtype, e.g. Filer::Audio.
|
73
|
+
# ==== Params
|
74
|
+
# [sym] Symbol corresponding to Filer subclass to cast into. For
|
75
|
+
# example, passing :audio will cast into a Filer::Audio.
|
76
|
+
# ==== Returns
|
77
|
+
# Instance of new Filer subclass
|
78
|
+
def as(sym)
|
79
|
+
#super calls into Utility::Castable mixin
|
80
|
+
super(sym, @path)
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
#Convenience wrapper for CSV files. Makes them Enumerable, so you
|
85
|
+
#can iterate through rows with each, map, select, etc. You can
|
86
|
+
#also modify in place with each!. See Filer base class for other
|
87
|
+
#methods.
|
88
|
+
class CSV < Filer
|
89
|
+
include Enumerable
|
90
|
+
require 'csv'
|
91
|
+
|
92
|
+
#Reads into an array of hashes, with hash keys determined by the
|
93
|
+
#first row of the CSV file. Parsing rules are the default for
|
94
|
+
#CSV.parse.
|
95
|
+
def read
|
96
|
+
raw = super or return []
|
97
|
+
rows = ::CSV.parse(raw.to_s)
|
98
|
+
headers = rows.shift or raise Error::File, "No CSV at #{@path}"
|
99
|
+
rows.map{|row| Utility.array_to_hash(row, headers) }
|
100
|
+
end
|
101
|
+
|
102
|
+
#Takes array of hashes followed by optional list of keys (by
|
103
|
+
#default keys are determined by looking at all the
|
104
|
+
#hashes). Lines are written per the defaults of
|
105
|
+
#CSV.generate_line.
|
106
|
+
def write(hashes, headers=hashes.map{|h| h.keys}.flatten.uniq)
|
107
|
+
super(
|
108
|
+
::CSV.generate_line(headers, :encoding => @encoding) +
|
109
|
+
hashes.map do |hash|
|
110
|
+
::CSV.generate_line(headers.map{|header| hash[header] }, :encoding => @encoding)
|
111
|
+
end.join
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
#Takes an array of arrays, corresponding to the rows, and a list
|
116
|
+
#of headers/keys to write at the top.
|
117
|
+
def write_arrays(arrays, headers)
|
118
|
+
write(arrays.map{|array| Utility.array_to_hash(array, headers) }, headers)
|
119
|
+
end
|
120
|
+
|
121
|
+
#Enumerate through the rows, with each row represented by a
|
122
|
+
#hash.
|
123
|
+
def each
|
124
|
+
read.each do |row|
|
125
|
+
yield row
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
#Same as each, but any changes to the rows will be written back
|
130
|
+
#out to the underlying CSV file.
|
131
|
+
def each!
|
132
|
+
#each_with_index doesn't return the array, so we have to use each
|
133
|
+
write(each{|hash| yield(hash) })
|
134
|
+
end
|
135
|
+
end #CSV
|
136
|
+
|
137
|
+
#Convenience wrapper for audio files.You can convert to mp3s,
|
138
|
+
#split into multiple files, and dynamically read the bitrate.
|
139
|
+
class Audio < Filer
|
140
|
+
require 'open3'
|
141
|
+
|
142
|
+
#Does the file have a '.mp3' extension?
|
143
|
+
def mp3?
|
144
|
+
File.extname(@path).downcase.eql?('.mp3')
|
145
|
+
end
|
146
|
+
|
147
|
+
#Convert to mp3 via ffmpeg.
|
148
|
+
# ==== Params
|
149
|
+
# [dest] Filer object corresponding to the path the mp3 version
|
150
|
+
# should end up at.
|
151
|
+
# [bitrate] If passed, bitrate should be an integer
|
152
|
+
# corresponding to kb/s. If not, we use the bitrate
|
153
|
+
# from the current file or, if that can't be read,
|
154
|
+
# default to 192kbps. Does not check if the file is
|
155
|
+
# already an mp3. Returns a new Filer::Audio
|
156
|
+
# representing the new mp3 file.
|
157
|
+
# ==== Returns
|
158
|
+
# Filer::Audio containing the new mp3.
|
159
|
+
def to_mp3(dest=self.dir.file("#{File.basename(@path, '.*') }.mp3"), bitrate=nil)
|
160
|
+
bitrate ||= self.bitrate || 192
|
161
|
+
Utility.system_quietly('ffmpeg', '-i', @path, '-acodec', 'libmp3lame', '-ab', "#{bitrate}k", '-ac', '2', dest)
|
162
|
+
File.exists?(dest) or raise Error::Shell, "Could not found output from `ffmpeg` on #{path}"
|
163
|
+
self.class.new(dest.path)
|
164
|
+
end
|
165
|
+
|
166
|
+
#Reads the bitrate of the audio file via ffmpeg. Returns an
|
167
|
+
#integer corresponding to kb/s, or nil if the bitrate could not
|
168
|
+
#be determined.
|
169
|
+
def bitrate
|
170
|
+
out, err, status = Open3.capture3('ffmpeg', '-i', @path)
|
171
|
+
bitrate = err.match(/(\d+) kb\/s/)
|
172
|
+
return bitrate ? bitrate[1].to_i : nil
|
173
|
+
end
|
174
|
+
|
175
|
+
#Splits an mp3 into smaller files.
|
176
|
+
# ==== Params
|
177
|
+
# [interval_in_min_dot_seconds] Split the file into chunks this
|
178
|
+
# large. The interval should be of the format
|
179
|
+
# minute.seconds, for example 2 minutes 15 seconds
|
180
|
+
# would be written as "2.15". For further details on
|
181
|
+
# interval format, consult the documentation for
|
182
|
+
# mp3split, a command-line unix utility.
|
183
|
+
# [basename] Name the new chunks using this base. Default is the
|
184
|
+
# basename of the original file.
|
185
|
+
# [dest] Destination directory for the new chunks as a
|
186
|
+
# Filer::Dir. Default is the same directory as the
|
187
|
+
# original file.
|
188
|
+
# ==== Returns
|
189
|
+
# Filer::Files containing the new files.
|
190
|
+
def split(interval_in_min_dot_seconds, basename=File.basename(path, '.*'), dest=dir)
|
191
|
+
#We have to cd into the wrapfile directory and do everything
|
192
|
+
#there because old/packaged versions of mp3splt were
|
193
|
+
#retarded at handling absolute directory paths
|
194
|
+
::Dir.chdir(dir.path) do
|
195
|
+
Utility.system_quietly('mp3splt', '-t', interval_in_min_dot_seconds, '-o', "#{basename}.@m.@s", File.basename(path))
|
196
|
+
end
|
197
|
+
files = Filer::Files::Audio.new(dir.select{|file| File.basename(file.path).match(/^#{Regexp.escape(basename) }\.\d+\.\d+\.mp3$/) })
|
198
|
+
if files.to_a.empty?
|
199
|
+
raise Error::Shell, "Could not find output from `mp3splt` on #{path}"
|
200
|
+
end
|
201
|
+
if dest.path != dir.path
|
202
|
+
files.mv!(dest)
|
203
|
+
end
|
204
|
+
files.sort
|
205
|
+
end
|
206
|
+
|
207
|
+
#Extracts from the filename the offset time of the chunk
|
208
|
+
#relative to the original from which it was split. Format is
|
209
|
+
#minute.seconds. Suitable for use on files created by 'split'
|
210
|
+
#method.
|
211
|
+
def offset
|
212
|
+
match = File.basename(@path).match(/\d+\.\d\d\b/)
|
213
|
+
return match[0] if match
|
214
|
+
end
|
215
|
+
end #Audio
|
216
|
+
|
217
|
+
#Handler for collection of Filer instances. Makes them enumerable,
|
218
|
+
#Allows easy re-casting to Filer::Files subclasses,
|
219
|
+
#and provides various other convenience methods.
|
220
|
+
class Files
|
221
|
+
include Enumerable
|
222
|
+
include Utility::Castable
|
223
|
+
require 'fileutils'
|
224
|
+
|
225
|
+
#Array of Filer instances included in the collection
|
226
|
+
attr_reader :files
|
227
|
+
|
228
|
+
#Constructor. Takes array of Filer instances.
|
229
|
+
def initialize(files)
|
230
|
+
@files = files
|
231
|
+
end
|
232
|
+
|
233
|
+
#Enumerate through Filer instances.
|
234
|
+
def each
|
235
|
+
files.each do |file|
|
236
|
+
yield file
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
#Cast this collection into a new Filer::Files subtype,
|
241
|
+
#e.g. Filer::Files::Audio.
|
242
|
+
# ==== Params
|
243
|
+
# [sym] Symbol corresponding to Filer::Files subclass to cast
|
244
|
+
# into. For example, passing :audio will cast into a
|
245
|
+
# Filer::Files::Audio.
|
246
|
+
# ==== Returns
|
247
|
+
# Instance of new Filer::Files subclass
|
248
|
+
def as(sym)
|
249
|
+
#super calls into Utility::Castable mixin
|
250
|
+
super(sym, files)
|
251
|
+
end
|
252
|
+
|
253
|
+
#Returns array of IO streams created by calling to_stream on
|
254
|
+
#each Filer instance in the collection.
|
255
|
+
def to_streams
|
256
|
+
self.map{|file| file.to_stream }
|
257
|
+
end
|
258
|
+
|
259
|
+
#Calls mv! on each Filer instance in the collection. See
|
260
|
+
#documentation for Filer#mv! for definition of "to" param and
|
261
|
+
#for return value.
|
262
|
+
def mv!(to)
|
263
|
+
files.map{|file| file.mv! to }
|
264
|
+
end
|
265
|
+
|
266
|
+
#Handler for collection of Filer::Audio instances. Does
|
267
|
+
#everything Filer::Files does, plus can batch convert to mp3 an
|
268
|
+
#can merge the Filer::Audio instances into a single audio file,
|
269
|
+
#provided they are in mp3 format.
|
270
|
+
class Audio < Files
|
271
|
+
|
272
|
+
#Constructor. Takes an array of Filer or Filer subclass instances.
|
273
|
+
def initialize(files)
|
274
|
+
@files = files.map{|file| self.file(file.path) }
|
275
|
+
end
|
276
|
+
|
277
|
+
def file(path)
|
278
|
+
Filer::Audio.new(path)
|
279
|
+
end
|
280
|
+
|
281
|
+
#Batch convert Filer::Audio instances to mp3 format.
|
282
|
+
# ==== Params
|
283
|
+
# [dest_dir] Filer::Dir instance corresponding to directory
|
284
|
+
# into which mp3 file versions will be created.
|
285
|
+
# [bitrate] See documentation for Filer::Audio#bitrate.
|
286
|
+
# ==== Returns
|
287
|
+
# Filer::Files::Audio instance corresponding to new mp3
|
288
|
+
# versions of the original files or, in the case where the
|
289
|
+
# original file was already in mp3 format, corresponding to
|
290
|
+
# the original files themselves.
|
291
|
+
def to_mp3(dest_dir, bitrate=nil)
|
292
|
+
mp3s = self.map do |file|
|
293
|
+
if file.mp3?
|
294
|
+
file
|
295
|
+
else
|
296
|
+
yield(file) if block_given?
|
297
|
+
file.to_mp3(dest_dir.file("#{File.basename(file.path, '.*') }.mp3"), bitrate)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
self.class.new(mp3s)
|
301
|
+
end
|
302
|
+
|
303
|
+
#Merge Filer::Audio instances into a single new file, provided
|
304
|
+
#they are all in mp3 format.
|
305
|
+
# ==== Params
|
306
|
+
#[into_file] Filer or Filer subclass instance corresponding to
|
307
|
+
#the location of the new, merged file that should be created.
|
308
|
+
# ==== Returns
|
309
|
+
# Filer::Audio instance corresponding to the new, merged file.
|
310
|
+
def merge(into_file)
|
311
|
+
raise Error::Argument, "No files to merge" if self.to_a.empty?
|
312
|
+
if self.count > 1
|
313
|
+
Utility.system_quietly('mp3wrap', into_file, *self.to_a)
|
314
|
+
written = File.join(into_file.dir, "#{File.basename(into_file.path, '.*') }_MP3WRAP.mp3")
|
315
|
+
FileUtils.mv(written, into_file)
|
316
|
+
else
|
317
|
+
FileUtils.cp(self.first, into_file)
|
318
|
+
end
|
319
|
+
self.file(into_file.path)
|
320
|
+
end
|
321
|
+
end #Audio
|
322
|
+
end #Files
|
323
|
+
|
324
|
+
#Convenience wrapper for basic directory operations and for
|
325
|
+
#casting files to specific filer types (CSV, Audio).
|
326
|
+
class Dir < Files
|
327
|
+
|
328
|
+
#Full expanded path to the dir
|
329
|
+
attr_reader :path
|
330
|
+
|
331
|
+
#Constructor. Takes full expanded path to the dir. Does NOT
|
332
|
+
#create dir in the filesystem.
|
333
|
+
def initialize(path)
|
334
|
+
@path = path
|
335
|
+
end
|
336
|
+
|
337
|
+
class << self
|
338
|
+
|
339
|
+
#Constructor. Takes full expanded path to the dir and creates
|
340
|
+
#the dir in the filesystem. Returns new Filer::Dir.
|
341
|
+
def create(path)
|
342
|
+
FileUtils.mkdir(path)
|
343
|
+
new(path)
|
344
|
+
end
|
345
|
+
|
346
|
+
#Constructor. Takes directory name and full expanded path of
|
347
|
+
#the parent directory. If the so-named directory exists within
|
348
|
+
#the parent directory, returns it. If not, returns nil.
|
349
|
+
def named(name, in_dir)
|
350
|
+
path = File.join(in_dir, name)
|
351
|
+
if File.exists?(path) && File.directory?(path)
|
352
|
+
new(path)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end #class << self
|
356
|
+
|
357
|
+
#Filer::Dir isntances stringify to their path.
|
358
|
+
def to_s
|
359
|
+
@path
|
360
|
+
end
|
361
|
+
alias :to_str :to_s
|
362
|
+
|
363
|
+
#Takes an aribtrary number of path elements relative to the
|
364
|
+
#Filer::Dir instance. So a file in the subdir path/to/file.txt
|
365
|
+
#would be referenced via file('path', 'to', 'file.txt'). Returns
|
366
|
+
#a new Filer instance wrapping the referenced file. Does not
|
367
|
+
#guarantee that the referenced file exists.
|
368
|
+
def file(*relative_path)
|
369
|
+
Filer.new(file_path(*relative_path))
|
370
|
+
end
|
371
|
+
|
372
|
+
#Returns the files in the Filer::Dir directory as Filer
|
373
|
+
#instances. Excludes files whose names start with a dot.
|
374
|
+
def files
|
375
|
+
::Dir.entries(@path).select{|entry| File.file? file_path(entry) }.reject{|entry| entry.match(/^\./) }.map{|entry| self.file(entry) }
|
376
|
+
end
|
377
|
+
|
378
|
+
#Takes relative path elements as params just like the file
|
379
|
+
#method. Returns a new Filer::Dir instance wrapping the
|
380
|
+
#referenced subdir.
|
381
|
+
def subdir(*relative_path)
|
382
|
+
Dir.new(file_path(*relative_path))
|
383
|
+
end
|
384
|
+
|
385
|
+
#OS X specific. Opens the dir in the Finder via the 'open' command.
|
386
|
+
def finder_open
|
387
|
+
system('open', @path)
|
388
|
+
end
|
389
|
+
|
390
|
+
def file_path(*relative_path)
|
391
|
+
File.join(@path, *relative_path)
|
392
|
+
end
|
393
|
+
|
394
|
+
end #Dir
|
395
|
+
end #Filer
|
396
|
+
end #Typingpool
|