ascii_invoicer 2.5.5
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/bin/ascii +722 -0
- data/latex/ascii-brief.cls +425 -0
- data/latex/ascii-brief.sty +46 -0
- data/lib/ascii_invoicer.rb +17 -0
- data/lib/ascii_invoicer/InvoiceProject.rb +356 -0
- data/lib/ascii_invoicer/ascii_logger.rb +64 -0
- data/lib/ascii_invoicer/filters.rb +129 -0
- data/lib/ascii_invoicer/generators.rb +209 -0
- data/lib/ascii_invoicer/hash_transformer.rb +23 -0
- data/lib/ascii_invoicer/mixins.rb +268 -0
- data/lib/ascii_invoicer/projectFileReader.rb +106 -0
- data/lib/ascii_invoicer/rfc5322_regex.rb +40 -0
- data/lib/ascii_invoicer/settings_manager.rb +62 -0
- data/lib/ascii_invoicer/texwriter.rb +105 -0
- data/lib/ascii_invoicer/tweaks.rb +25 -0
- data/lib/ascii_invoicer/version.rb +3 -0
- data/repl/ascii +2 -0
- data/settings/default-settings.yml +76 -0
- data/settings/settings_template.yml +54 -0
- data/templates/default.yml.erb +74 -0
- data/templates/document.tex.erb +87 -0
- metadata +297 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
module Generators
|
2
|
+
def generate_hours_total full_data
|
3
|
+
hours = full_data[:hours]
|
4
|
+
hours[:salary] * hours[:time]
|
5
|
+
end
|
6
|
+
|
7
|
+
def generate_hours_time full_data
|
8
|
+
hours = full_data[:hours]
|
9
|
+
sum = 0
|
10
|
+
if hours[:caterers]
|
11
|
+
hours[:caterers].values.each{|v| sum += v.rationalize}
|
12
|
+
return sum.to_f
|
13
|
+
elsif hours[:time]
|
14
|
+
fail_at :caterers
|
15
|
+
return hours[:time]
|
16
|
+
end
|
17
|
+
sum
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate_client_fullname full_data
|
21
|
+
client = full_data[:client]
|
22
|
+
fail_at :client_first_name unless client[:first_name]
|
23
|
+
fail_at :client_last_name unless client[:last_name]
|
24
|
+
return fail_at :client_fullname unless client[:first_name] and client[:last_name]
|
25
|
+
return [client[:first_name], client[:last_name]].join ' '
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_client_addressing full_data
|
29
|
+
return fail_at(:client_addressing) unless full_data[:client]
|
30
|
+
return fail_at(:client_title) unless full_data[:client][:title]
|
31
|
+
lang = full_data[:lang]
|
32
|
+
client = full_data[:client]
|
33
|
+
title = client[:title].words.first.downcase
|
34
|
+
gender = @settings['gender_matches'][title]
|
35
|
+
addressing = @settings['lang_addressing'][lang][gender]
|
36
|
+
return "#{addressing} #{client[:title]} #{client[:last_name]}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def generate_caterers full_data
|
40
|
+
caterers = []
|
41
|
+
full_data[:hours][:caterers].each{|name, time| caterers.push name} if full_data[:hours][:caterers]
|
42
|
+
return caterers
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate_event_date full_data
|
46
|
+
Date.parse full_data[:event][:dates][0][:begin] unless full_data[:event][:dates].nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate_event_calendaritems full_data
|
50
|
+
begin
|
51
|
+
events = []
|
52
|
+
full_data[:event][:dates].each { |date|
|
53
|
+
# TODO event times is not implemented right
|
54
|
+
unless date[:times].nil?
|
55
|
+
|
56
|
+
## set specific times
|
57
|
+
date[:times].each { |time|
|
58
|
+
if time[:end]
|
59
|
+
dtstart = DateTime.parse( date[:begin].strftime("%d.%m.%Y ") + time[:begin] )
|
60
|
+
dtend = DateTime.parse( date[:begin].strftime("%d.%m.%Y ") + time[:end] )
|
61
|
+
else
|
62
|
+
dtstart = Icalendar::Values::Date.new( date[:begin].strftime "%Y%m%d")
|
63
|
+
dtend = Icalendar::Values::Date.new((date[:end]+1).strftime "%Y%m%d")
|
64
|
+
end
|
65
|
+
event = Icalendar::Event.new
|
66
|
+
event.dtstart = dtstart
|
67
|
+
event.dtend = dtend
|
68
|
+
events.push event unless event.dtstart.nil?
|
69
|
+
|
70
|
+
}
|
71
|
+
|
72
|
+
else
|
73
|
+
## set full day event
|
74
|
+
event = Icalendar::Event.new
|
75
|
+
event.dtstart = Icalendar::Values::Date.new( date[:begin].strftime "%Y%m%d")
|
76
|
+
event.dtend = Icalendar::Values::Date.new((date[:end]+1).strftime "%Y%m%d")
|
77
|
+
events.push event unless event.dtstart.nil?
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
events.each{ | event|
|
82
|
+
|
83
|
+
event.description = ""
|
84
|
+
event.summary = full_data[:event][:name]
|
85
|
+
event.summary = "CANCELED: #{ event.summary }" if full_data[:canceled]
|
86
|
+
|
87
|
+
event.description += "Verantwortung: " + full_data[:manager] + "\n" if full_data[:manager]
|
88
|
+
if full_data[:hours][:caterers]
|
89
|
+
event.description += "Caterer:\n"
|
90
|
+
full_data[:caterers].each {|caterer,time| event.description += " - #{ caterer}\n" }
|
91
|
+
end
|
92
|
+
|
93
|
+
event.description += full_data[:description] + "\n" if full_data[:description]
|
94
|
+
}
|
95
|
+
}
|
96
|
+
return events
|
97
|
+
rescue
|
98
|
+
@errors << :event_dates
|
99
|
+
return false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def generate_productsbytax full_data
|
104
|
+
list = {}
|
105
|
+
taxlist = {}
|
106
|
+
full_data[:products].each {|product|
|
107
|
+
list[product.tax_value] = [] unless list[product.tax_value]
|
108
|
+
list[product.tax_value] << product
|
109
|
+
}
|
110
|
+
list.keys.sort.each{|key| taxlist[key] = list[key] } # sorting a hash by keys
|
111
|
+
return taxlist
|
112
|
+
end
|
113
|
+
|
114
|
+
def generate_event_age full_data
|
115
|
+
(Date.today - full_data[:event][:date]).to_i
|
116
|
+
end
|
117
|
+
|
118
|
+
def sum_money key
|
119
|
+
sum = 0.to_euro
|
120
|
+
@data[:products].each{|p| sum += p.hash[key]} if @data[:products].class == Array
|
121
|
+
sum.to_euro
|
122
|
+
end
|
123
|
+
|
124
|
+
def generate_event_date full_data
|
125
|
+
full_data[:event][:dates][0][:begin] unless full_data[:event][:dates].nil?
|
126
|
+
end
|
127
|
+
|
128
|
+
def generate_event_prettydate full_data
|
129
|
+
return fail_at :event_prettydate if full_data[:event][:dates].nil?
|
130
|
+
date = full_data[:event][:dates][0]
|
131
|
+
first = date[:begin]
|
132
|
+
last = full_data[:event][:dates].last[:end]
|
133
|
+
last = full_data[:event][:dates].last[:begin] if last.nil?
|
134
|
+
|
135
|
+
return "#{first.strftime "%d"}-#{last.strftime "%d.%m.%Y"}" if first != last
|
136
|
+
return first.strftime "%d.%m.%Y" if first.class == Date
|
137
|
+
return first
|
138
|
+
end
|
139
|
+
|
140
|
+
def generate_offer_number full_data
|
141
|
+
appendix = full_data[:offer][:appendix]
|
142
|
+
full_data[:offer][:date].strftime "A%Y%m%d-#{appendix}"
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
# costs: price of all products summed up
|
147
|
+
# taxes: price of all products taxes summed up ( e.g. price*0.19 )
|
148
|
+
# total: costs + taxes
|
149
|
+
# final: total + salary * hours
|
150
|
+
|
151
|
+
|
152
|
+
def generate_offer_costs full_data
|
153
|
+
sum_money :cost_offer
|
154
|
+
end
|
155
|
+
|
156
|
+
def generate_offer_taxes full_data
|
157
|
+
sum_money :tax_offer
|
158
|
+
end
|
159
|
+
|
160
|
+
def generate_offer_total full_data
|
161
|
+
sum_money :total_offer
|
162
|
+
end
|
163
|
+
|
164
|
+
def generate_offer_final full_data
|
165
|
+
full_data[:offer][:total] + full_data[:hours][:total]
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
|
171
|
+
def generate_invoice_costs full_data
|
172
|
+
sum_money :cost_invoice
|
173
|
+
end
|
174
|
+
|
175
|
+
def generate_invoice_taxes full_data
|
176
|
+
sum_money :tax_invoice
|
177
|
+
end
|
178
|
+
|
179
|
+
def generate_invoice_total full_data
|
180
|
+
sum_money :total_invoice
|
181
|
+
end
|
182
|
+
|
183
|
+
def generate_invoice_final full_data
|
184
|
+
full_data[:invoice][:total] + full_data[:hours][:total]
|
185
|
+
end
|
186
|
+
|
187
|
+
def generate_invoice_delay full_data
|
188
|
+
return 0 if full_data[:canceled]
|
189
|
+
return -(full_data[:event][:date] - full_data[:invoice][:date] if full_data[:invoice][:date]).to_i
|
190
|
+
return -(full_data[:event][:date] - Date.today).to_i
|
191
|
+
end
|
192
|
+
|
193
|
+
def generate_invoice_paydelay full_data
|
194
|
+
if full_data[:invoice][:payed_date] and full_data[:invoice][:date]
|
195
|
+
delay = full_data[:invoice][:payed_date] - full_data[:invoice][:date]
|
196
|
+
fail_at :invoice_payed if delay < 0
|
197
|
+
return delay.to_i
|
198
|
+
end
|
199
|
+
return nil
|
200
|
+
end
|
201
|
+
|
202
|
+
def generate_invoice_longnumber full_data
|
203
|
+
if full_data[:invoice][:date]
|
204
|
+
year = full_data[:invoice][:date].year
|
205
|
+
full_data[:invoice][:number].gsub /^R/, "R#{year}-" if full_data[:invoice][:number]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'hash-graft'
|
2
|
+
|
3
|
+
class HashTransformer
|
4
|
+
attr_reader :original_hash, :new_hash
|
5
|
+
attr_writer :rules
|
6
|
+
|
7
|
+
def initialize(hash = {})
|
8
|
+
@original_hash = hash[:original_hash]
|
9
|
+
@original_hash ||= {}
|
10
|
+
@new_hash = {}
|
11
|
+
@rules = hash[:rules]
|
12
|
+
@rules ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def transform
|
16
|
+
@new_hash = @original_hash
|
17
|
+
@rules.each {|rule|
|
18
|
+
@new_hash.set_path(rule[:new], @original_hash.get_path(rule[:old]))
|
19
|
+
}
|
20
|
+
return @new_hash
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
require 'icalendar'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module AsciiMixins
|
5
|
+
|
6
|
+
## Use Option parser or leave it if only one argument is given
|
7
|
+
|
8
|
+
def render_project project, choice
|
9
|
+
project.validate choice
|
10
|
+
if project.valid_for[choice]
|
11
|
+
project.create_tex choice, options[:check]
|
12
|
+
else
|
13
|
+
$logger.error "#{project.name} is not ready for creating an #{choice.to_s}! #{project.data[:valid]} #{project.errors if project.errors.length > 0}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
##TODO turn color_from_date(date) into a loopuk into $SETTINGS
|
18
|
+
def color_from_date(date)
|
19
|
+
diff = date - Date.today
|
20
|
+
return (rand * 256**3).to_i.to_s(16) if Date.today.day == 1 and Date.today.month == 4 #april fools
|
21
|
+
return :magenta if diff < -28
|
22
|
+
return :cyan if diff < 0
|
23
|
+
return [:yellow,:bright] if diff == 0
|
24
|
+
return :red if diff < 7
|
25
|
+
return :yellow if diff < 14
|
26
|
+
return [:green]
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_project_list projects, hash = {}
|
30
|
+
table = Textboxes.new
|
31
|
+
table.style[:border] = false
|
32
|
+
table.style[:column_borders] = false
|
33
|
+
table.style[:row_borders] = false
|
34
|
+
table.style[:padding_horizontal] = 1
|
35
|
+
projects.each_index do |i|
|
36
|
+
project = projects[i]
|
37
|
+
if !hash[:colors].nil? and hash[:colors]
|
38
|
+
color = color_from_date(project.date)
|
39
|
+
color = :default if project.validate(:invoice)
|
40
|
+
color = [:blue] if project.status == :canceled
|
41
|
+
end
|
42
|
+
if hash[:verbose]
|
43
|
+
row = print_row_verbose project, hash
|
44
|
+
else
|
45
|
+
row = print_row_simple project, hash
|
46
|
+
end
|
47
|
+
|
48
|
+
row << project.data[:hours][:caterers].keys.join(", ") if hash[:caterers] and project.data[:hours][:caterers]
|
49
|
+
|
50
|
+
row << project.blockers(:archive) if hash[:blockers]
|
51
|
+
if hash[:details]
|
52
|
+
hash[:details].each {|detail|
|
53
|
+
row << project.data.get_path(detail)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
row << project.errors if hash[:errors] and project.status == :ok
|
58
|
+
row << project.status if hash[:errors] and project.status == :canceled
|
59
|
+
row.insert 0, i+1
|
60
|
+
table.add_row row, color
|
61
|
+
end
|
62
|
+
table.set_alignments(:r, :l, :l)
|
63
|
+
puts table
|
64
|
+
end
|
65
|
+
|
66
|
+
def print_row_simple(project,hash)
|
67
|
+
row = [
|
68
|
+
project.pretty_name,
|
69
|
+
project.data[:manager],
|
70
|
+
project.data[:event][:invoice_number],
|
71
|
+
project.data[:event][:date].strftime("%d.%m.%Y"),
|
72
|
+
#project.index
|
73
|
+
]
|
74
|
+
return row
|
75
|
+
end
|
76
|
+
|
77
|
+
def print_row_verbose (project, hash)
|
78
|
+
name = "##{project.data[:name]}#"
|
79
|
+
if not project.data[:event][:name].nil? and project.data[:event][:name].size > 0
|
80
|
+
name = project.data[:event][:name]
|
81
|
+
name = "CANCELED: " + name if project.data :canceled
|
82
|
+
end
|
83
|
+
row = [
|
84
|
+
name,
|
85
|
+
project.data[:manager],
|
86
|
+
project.data[:invoice][:number],
|
87
|
+
project.date.strftime("%d.%m.%Y"),
|
88
|
+
project.validate(:offer).print,
|
89
|
+
project.validate(:invoice).print,
|
90
|
+
project.validate(:payed).print($SETTINGS.currency_symbol),
|
91
|
+
# try these: ☑☒✉☕☀☻
|
92
|
+
]
|
93
|
+
return row
|
94
|
+
end
|
95
|
+
|
96
|
+
def print_project_list_paths(projects)
|
97
|
+
table = Textboxes.new
|
98
|
+
projects.each_index do |i|
|
99
|
+
p = projects[i]
|
100
|
+
table.add_row [
|
101
|
+
(i+1).to_s+".",
|
102
|
+
p.name.ljust(35),
|
103
|
+
p.data[:project_path]
|
104
|
+
]
|
105
|
+
end
|
106
|
+
table.set_alignments(:r, :l, :l)
|
107
|
+
puts table
|
108
|
+
end
|
109
|
+
|
110
|
+
def create_cal_file(projects)
|
111
|
+
cal = Icalendar::Calendar.new
|
112
|
+
projects.each_index do |i|
|
113
|
+
project = projects[i]
|
114
|
+
events = project.data[:event][:calendaritems]
|
115
|
+
if events
|
116
|
+
events.each { |event| cal.add_event event}
|
117
|
+
else
|
118
|
+
$logger.warn "Calendar can't be parsed. (#{project.data[:name]})", :file
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
cal_file_path = File.join(FileUtils.pwd, $SETTINGS.calendar_file)
|
123
|
+
cal_file = File.open(cal_file_path, ?w)
|
124
|
+
cal_file.write cal.to_ical
|
125
|
+
puts "created #{cal_file_path}"
|
126
|
+
end
|
127
|
+
|
128
|
+
#takes an array of invoices (@plumber.working_projects)
|
129
|
+
def print_project_list_yaml(projects)
|
130
|
+
projects.each do |p|
|
131
|
+
puts p.data.to_yaml
|
132
|
+
puts "...\n\n"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def caterers_string project, join = ", "
|
137
|
+
data = project.data
|
138
|
+
data[:hours][:caterers].map{|name, hours| "#{name} (#{hours})" if hours > 0 }.join join if data[:hours][:caterers]
|
139
|
+
end
|
140
|
+
|
141
|
+
#takes an array of invoices (@plumber.working_projects)
|
142
|
+
def print_project_list_csv(projects)
|
143
|
+
header = [
|
144
|
+
'Rnum',
|
145
|
+
'Bezeichnung',
|
146
|
+
'Datum',
|
147
|
+
'Rechnungsdatum',
|
148
|
+
'Betreuer',
|
149
|
+
'verantwortlich',
|
150
|
+
'Bezahlt am',
|
151
|
+
'Betrag',
|
152
|
+
'Canceled',
|
153
|
+
]
|
154
|
+
puts header.to_csv(col_sep:";")
|
155
|
+
projects.each do |p|
|
156
|
+
canceled = ""
|
157
|
+
canceled = "canceled" if p.data[:canceled]
|
158
|
+
line = [
|
159
|
+
p.data[:invoice][:number],
|
160
|
+
p.data[:event][:name],
|
161
|
+
p.data[:event][:date],
|
162
|
+
p.data[:invoice][:date],
|
163
|
+
caterers_string(p),
|
164
|
+
p.data[:manager].words[0],
|
165
|
+
p.data[:invoice][:payed_date],
|
166
|
+
p.data[:invoice][:final],
|
167
|
+
canceled,
|
168
|
+
# p.valid_for[:invoice]
|
169
|
+
]
|
170
|
+
canceled = "canceled" if p.data[:canceled]
|
171
|
+
line.map! {|v| v ? v : "" } # wow, that looks cryptic
|
172
|
+
puts line.to_csv(col_sep:";")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def display_products project, choice = :offer, standalone = true
|
177
|
+
table = Textboxes.new
|
178
|
+
table.style[:border] = standalone
|
179
|
+
table.title = "Project:" + "\"#{project.data[:event][:name]}\"".rjust(25) if standalone
|
180
|
+
table.add_row ["#", "name", "price", "cost"]
|
181
|
+
table.set_alignments :r, :l, :r, :r
|
182
|
+
project.data[:products].each {|product|
|
183
|
+
amount = product.amount choice
|
184
|
+
price = product.price
|
185
|
+
cost = product.cost choice
|
186
|
+
table.add_row [amount, product.name, price, cost]
|
187
|
+
}
|
188
|
+
table.add_row ["#{project.data[:hours][:time]}h", "service" , project.data[:hours][:salary], project.data[:hours][:total]] if project.data.get_path('hours/time').to_i> 0
|
189
|
+
table.add_row [nil, caterers_string(project)]
|
190
|
+
|
191
|
+
return table
|
192
|
+
end
|
193
|
+
|
194
|
+
def display_all project, choice, show_errors = true
|
195
|
+
raise "choice must be either :invoice or :offer" unless choice == :invoice or choice == :offer
|
196
|
+
data = project.data
|
197
|
+
|
198
|
+
table = Textboxes.new
|
199
|
+
table.style[:border] = true
|
200
|
+
table.title = "Project:" + "\"#{data[:event][:name]}\"".rjust(25)
|
201
|
+
table.add_row [nil, "name", "amount","price", "cost"]
|
202
|
+
table.set_alignments :r, :l, :r, :r, :r
|
203
|
+
|
204
|
+
i = 0
|
205
|
+
data[:products].each {|product|
|
206
|
+
amount = product.amount choice
|
207
|
+
price = product.price
|
208
|
+
cost = product.cost choice
|
209
|
+
table.add_row [i+=1,product.name, amount, price, cost]
|
210
|
+
}
|
211
|
+
table.add_row [i+1,"service", "#{data[:hours][:time]}h", data[:hours][:salary], data[:hours][:total]] if project.data.get_path('hours/time').to_i> 0
|
212
|
+
|
213
|
+
separator = table.column_widths.map{|w| ?=*w}
|
214
|
+
separator[0] = nil
|
215
|
+
table.add_row separator
|
216
|
+
|
217
|
+
table.add_row [nil,"Kosten",nil,nil,"#{data[choice][:costs]}"]
|
218
|
+
data[:productsbytax].each {|tax,products|
|
219
|
+
tpv = 0.to_euro # tax per value
|
220
|
+
tax = (tax.rationalize * 100).to_f
|
221
|
+
products.each{|p|
|
222
|
+
tpv += p.hash[:tax_offer] if choice == :offer
|
223
|
+
tpv += p.hash[:tax_invoice] if choice == :invoice
|
224
|
+
}
|
225
|
+
table.add_row [nil, "MWST #{tax}%",nil,nil,"#{tpv}"]
|
226
|
+
}
|
227
|
+
table.add_row [nil, "Final", nil, nil, "#{data[choice][:final]}"]
|
228
|
+
|
229
|
+
if show_errors
|
230
|
+
table.footer = "Errors: #{project.errors.length} (#{ project.errors.join ',' })" if project.errors.length >0
|
231
|
+
end
|
232
|
+
|
233
|
+
return table
|
234
|
+
end
|
235
|
+
|
236
|
+
def display_products_csv project
|
237
|
+
puts [['name', 'price', 'amount', 'sold', 'tax_value'].to_csv(col_sep:?;)]+
|
238
|
+
project.data[:products].map{|p| p.to_csv(col_sep:?;)}
|
239
|
+
end
|
240
|
+
|
241
|
+
def check_project(path)
|
242
|
+
project = InvoiceProject.new $SETTINGS
|
243
|
+
project.open path
|
244
|
+
unless project.validate(:offer)
|
245
|
+
puts "\nWARNING: the file you just edited contains errors! (#{project.errors})"
|
246
|
+
unless no? "would you like to edit it again? [y|N]"
|
247
|
+
edit_files path
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
## hand path to editor
|
253
|
+
def edit_files(paths, editor = $SETTINGS.editor)
|
254
|
+
paths = [paths] if paths.class == String
|
255
|
+
paths.select! {|path| path}
|
256
|
+
if paths.empty?
|
257
|
+
$logger.error "no paths to open"
|
258
|
+
return false
|
259
|
+
end
|
260
|
+
paths.map!{|path| "\"#{path}\"" }
|
261
|
+
paths = paths.join ' '
|
262
|
+
editor = $SETTINGS.editor unless editor
|
263
|
+
$logger.info "Opening #{paths} in #{editor}"
|
264
|
+
pid = spawn "#{editor} #{paths}"
|
265
|
+
Process.wait pid
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|