labrat 0.1.13
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/.gitignore +17 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +102 -0
- data/README.org +644 -0
- data/Rakefile +12 -0
- data/TODO.org +7 -0
- data/bin/console +15 -0
- data/bin/labrat +74 -0
- data/bin/labrat-install +85 -0
- data/bin/setup +8 -0
- data/img/sample.jpg +0 -0
- data/img/sample.pdf +82 -0
- data/img/sample.png +0 -0
- data/labrat.gemspec +40 -0
- data/lib/config_files/config.yml +121 -0
- data/lib/config_files/labeldb.yml +1010 -0
- data/lib/config_files/labeldb_usr.yml +24 -0
- data/lib/labrat/arg_parser.rb +481 -0
- data/lib/labrat/config.rb +188 -0
- data/lib/labrat/errors.rb +13 -0
- data/lib/labrat/hash.rb +38 -0
- data/lib/labrat/label.rb +158 -0
- data/lib/labrat/label_db.rb +49 -0
- data/lib/labrat/options.rb +184 -0
- data/lib/labrat/read_files.rb +40 -0
- data/lib/labrat/version.rb +5 -0
- data/lib/labrat.rb +20 -0
- data/lib/lisp/labrat.el +108 -0
- metadata +219 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
# User database of label settings to override or augment system-level
|
2
|
+
# settings. For labels known from the system-level config, you might want to
|
3
|
+
# add user-specific settings, such as the destination printer for that
|
4
|
+
# particular label type.
|
5
|
+
dymo30327:
|
6
|
+
printer-name: dymo
|
7
|
+
|
8
|
+
# Or you might want a full definition of a label type not defined in the
|
9
|
+
# system-level database. Here is a commented-out definition of a common avery
|
10
|
+
# label type that you can adapt for your own label type. Just give it a
|
11
|
+
# unique name unless you want to replace the system-level definition.
|
12
|
+
|
13
|
+
# avery5160:
|
14
|
+
# page-width: 8.5in
|
15
|
+
# page-height: 11in
|
16
|
+
# rows: 10
|
17
|
+
# columns: 3
|
18
|
+
# top-page-margin: 13mm
|
19
|
+
# bottom-page-margin: 12mm
|
20
|
+
# left-page-margin: 5mm
|
21
|
+
# right-page-margin: 5mm
|
22
|
+
# row-gap: 0mm
|
23
|
+
# column-gap: 3mm
|
24
|
+
# landscape: false
|
@@ -0,0 +1,481 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Labrat
|
4
|
+
# An ArgParser object implements parsing of command-line arguments and
|
5
|
+
# gathering them into an Options object. By using the from_hash method, you
|
6
|
+
# can get an ArgParser object to also treat a Hash as if it were a set of
|
7
|
+
# command-line arguments so that config files, converted to a Hash can also
|
8
|
+
# be used with an ArgParser.
|
9
|
+
class ArgParser
|
10
|
+
attr_reader :parser, :options, :labels_seen
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@labels_seen = Set.new
|
14
|
+
@options = Labrat::Options.new
|
15
|
+
@parser = OptionParser.new
|
16
|
+
@parser.summary_width = 30
|
17
|
+
define_options
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parse and set the options object to reflect the values of the given
|
21
|
+
# args, after merging in any prior settings into options. Return the
|
22
|
+
# resulting options instance. The args argument can be either a Hash or,
|
23
|
+
# as usual, an Array of Strings from the command-line.x Throw an exception
|
24
|
+
# for errors encountered parsing the args.
|
25
|
+
def parse(args, prior: {}, verbose: false)
|
26
|
+
options.msg = nil
|
27
|
+
options.verbose = verbose
|
28
|
+
options.merge!(prior)
|
29
|
+
case args
|
30
|
+
when Hash
|
31
|
+
parser.parse!(args.optionize)
|
32
|
+
when Array
|
33
|
+
parser.parse!(args)
|
34
|
+
else
|
35
|
+
raise "ArgParser cannot parse args of class '#{args.class}'"
|
36
|
+
end
|
37
|
+
options
|
38
|
+
rescue OptionParser::ParseError => e
|
39
|
+
options.msg = "Error: #{e}\n\nTry `labrat --help` for usage."
|
40
|
+
raise Labrat::OptionError, options.msg
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Define the OptionParser rules for acceptable options in Labrat.
|
46
|
+
def define_options
|
47
|
+
parser.banner = "Usage: labrat [options] <label-text>"
|
48
|
+
parser.separator ""
|
49
|
+
parser.separator "Print or view (with -V) a label with the given <label-text>."
|
50
|
+
parser.separator "All non-option arguments are used for the label text with a special"
|
51
|
+
parser.separator "marker ('++' by default, see --nlsep) indicating a line-break."
|
52
|
+
parser.separator ""
|
53
|
+
parser.separator "Below, NUM indicates an integer, DIM, indicates a linear dimension,"
|
54
|
+
parser.separator "valid DIM units are: pt, mm, cm, dm, m, in, ft, yd."
|
55
|
+
parser.separator "A DIM with no units assumes pt (points)."
|
56
|
+
parser.separator ""
|
57
|
+
parser.separator "Meta options:"
|
58
|
+
label_name_option
|
59
|
+
list_labels_option
|
60
|
+
|
61
|
+
parser.separator ""
|
62
|
+
parser.separator "Page setup options:"
|
63
|
+
page_dimension_options
|
64
|
+
page_margin_options
|
65
|
+
|
66
|
+
parser.separator ""
|
67
|
+
parser.separator "Label setup options:"
|
68
|
+
landscape_option
|
69
|
+
portrait_option
|
70
|
+
padding_options
|
71
|
+
align_options
|
72
|
+
font_options
|
73
|
+
delta_options
|
74
|
+
|
75
|
+
parser.separator ""
|
76
|
+
parser.separator "Processing options:"
|
77
|
+
start_label_option
|
78
|
+
nl_sep_option
|
79
|
+
label_sep_option
|
80
|
+
copies_option
|
81
|
+
in_file_option
|
82
|
+
out_file_option
|
83
|
+
printer_name_option
|
84
|
+
command_options
|
85
|
+
view_option
|
86
|
+
grid_option
|
87
|
+
template_option
|
88
|
+
verbose_option
|
89
|
+
|
90
|
+
parser.separator ""
|
91
|
+
parser.separator "Common options:"
|
92
|
+
# Normally, options.msg is nil. If it is set to a string, we want
|
93
|
+
# the main program to print the string and exit.
|
94
|
+
options.msg = nil
|
95
|
+
parser.on_tail("--help", "Show this message") do
|
96
|
+
# NB: parser.to_s returns the usage message.
|
97
|
+
options.msg = parser.to_s
|
98
|
+
end
|
99
|
+
# Another typical switch to print the version.
|
100
|
+
parser.on_tail("--version", "Show version") do
|
101
|
+
options.msg = "labrat version #{VERSION}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Use the facilities of 'prawn/measurement_extensions' to convert
|
106
|
+
# dimensions given in pt, mm, cm, dm, m, in, ft, yd into Adobe points, or
|
107
|
+
# "big points" in TeX jargon.
|
108
|
+
def parse_dimension(str, where = '')
|
109
|
+
unless (match = str.match(/\A\s*(?<measure>[-+]?[0-9.]+)\s*(?<unit>[A-Za-z]*)\s*\z/))
|
110
|
+
raise Labrat::DimensionError, "illegal #{where} dimension: '#{str}'"
|
111
|
+
end
|
112
|
+
|
113
|
+
if match[:unit].empty?
|
114
|
+
match[:measure].to_f
|
115
|
+
else
|
116
|
+
meas = match[:measure].to_f
|
117
|
+
u_meth = match[:unit].to_sym
|
118
|
+
unless meas.respond_to?(u_meth)
|
119
|
+
msg = "Error: unknown #{where} unit: '#{match[:unit]}'\n"\
|
120
|
+
" valid units are: pt, mm, cm, dm, m, in, ft, yd"
|
121
|
+
raise Labrat::DimensionError, msg
|
122
|
+
end
|
123
|
+
points = meas.send(u_meth)
|
124
|
+
warn " ::#{where} <- #{str} (#{points.round(2)}pt)::" if options.verbose
|
125
|
+
points
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Define options for specifying the dimensions of a page of labels to be
|
130
|
+
# printed on.
|
131
|
+
def page_dimension_options
|
132
|
+
# Specifies an optional option argument
|
133
|
+
parser.on("-wDIM", "--page-width=DIM",
|
134
|
+
"Horizontal dimension of a page of labels as it comes out of the printer") do |wd|
|
135
|
+
options.page_width = parse_dimension(wd, 'page-width')
|
136
|
+
end
|
137
|
+
parser.on("-hDIM", "--page-height=DIM",
|
138
|
+
"Vertical dimension of a page of labels as it comes out of the printer") do |ht|
|
139
|
+
options.page_height = parse_dimension(ht, 'page-height')
|
140
|
+
end
|
141
|
+
parser.on("-RNUM", "--rows=NUM", Integer,
|
142
|
+
"Number of rows of labels on a page") do |n|
|
143
|
+
options.rows = n
|
144
|
+
warn " ::rows <- #{n}::" if options.verbose
|
145
|
+
end
|
146
|
+
parser.on("-CNUM", "--columns=NUM", Integer,
|
147
|
+
"Number of columns of labels on a page") do |n|
|
148
|
+
options.columns = n
|
149
|
+
warn " ::columns <- #{n}::" if options.verbose
|
150
|
+
end
|
151
|
+
parser.on("--row-gap=DIM",
|
152
|
+
"Vertical space between rows of labels on a page") do |gap|
|
153
|
+
options.row_gap = parse_dimension(gap, 'row-gap')
|
154
|
+
end
|
155
|
+
parser.on("--column-gap=DIM",
|
156
|
+
"Column gap:",
|
157
|
+
"Horizontal space between columns of labels on a page") do |gap|
|
158
|
+
options.column_gap = parse_dimension(gap, 'column-gap')
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Set the page margins for printing on a page of labels. Left, right,
|
163
|
+
# top, and bottom are named assuming a portrait orientation, that is the
|
164
|
+
# orientation of the page as it comes out of the printer.
|
165
|
+
def page_margin_options
|
166
|
+
parser.on("--right-page-margin=DIM",
|
167
|
+
"Distance from right side of page (in portrait) to print area") do |x|
|
168
|
+
options.right_page_margin = parse_dimension(x, 'right-page-margin')
|
169
|
+
end
|
170
|
+
parser.on("--left-page-margin=DIM",
|
171
|
+
"Distance from left side of page (in portrait) to print area") do |x|
|
172
|
+
options.left_page_margin = parse_dimension(x, 'left-page-margin')
|
173
|
+
end
|
174
|
+
parser.on("--top-page-margin=DIM",
|
175
|
+
"Distance from top side of page (in portrait) to print area") do |x|
|
176
|
+
options.top_page_margin = parse_dimension(x, 'top-page-margin')
|
177
|
+
end
|
178
|
+
parser.on("--bottom-page-margin=DIM",
|
179
|
+
"Distance from bottom side of page (in portrait) to print area") do |x|
|
180
|
+
options.bottom_page_margin = parse_dimension(x, 'bottom-page-margin')
|
181
|
+
end
|
182
|
+
parser.on("--h-page-margin=DIM",
|
183
|
+
"Distance from left and right sides of page (in portrait) to print area") do |x|
|
184
|
+
options.left_page_margin = options.right_page_margin = parse_dimension(x, 'h-page-margin')
|
185
|
+
end
|
186
|
+
parser.on("--v-page-margin=DIM",
|
187
|
+
"Distance from top and bottom sides of page (in portrait) to print area") do |x|
|
188
|
+
options.top_page_margin = options.bottom_page_margin = parse_dimension(x, 'v-page-margin')
|
189
|
+
end
|
190
|
+
parser.on("--page-margin=DIM",
|
191
|
+
"Distance from all sides of page (in portrait) to print area") do |x|
|
192
|
+
options.left_page_margin = options.right_page_margin =
|
193
|
+
options.top_page_margin = options.bottom_page_margin = parse_dimension(x, 'margin')
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Define a label name. Perhaps the config files could contain a database
|
198
|
+
# of common labels with their dimensions, so that --width and --height
|
199
|
+
# need not be specified.
|
200
|
+
def label_name_option
|
201
|
+
parser.on("-lNAME", "--label=NAME",
|
202
|
+
"Use options for label type NAME from label database") do |name|
|
203
|
+
if labels_seen.include?(name)
|
204
|
+
msg = "label option '#{name}' has circular reference"
|
205
|
+
labels_seen.each do |nm|
|
206
|
+
msg += "\n -> #{nm}"
|
207
|
+
end
|
208
|
+
msg += "\n -> #{name}"
|
209
|
+
raise RecursionError, msg
|
210
|
+
else
|
211
|
+
options.label = name
|
212
|
+
labels_seen << options.label
|
213
|
+
end
|
214
|
+
|
215
|
+
# Insert at this point the option args found in the Label.db
|
216
|
+
lab_hash = LabelDb[name.to_sym]
|
217
|
+
lab_hash.report("\nConfig from labeldb entry '#{name}'") if options.verbose
|
218
|
+
raise LabelNameError,
|
219
|
+
"Unknown label name '#{name}'." if lab_hash.empty?
|
220
|
+
|
221
|
+
lab_args = lab_hash.optionize
|
222
|
+
parse(lab_args, verbose: options.verbose)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def list_labels_option
|
227
|
+
parser.on("--list-labels",
|
228
|
+
"List known label types from label database and exit") do
|
229
|
+
db_paths = Labrat::LabelDb.db_paths
|
230
|
+
lab_names = Labrat::LabelDb.known_names
|
231
|
+
if db_paths.empty?
|
232
|
+
warn "Have you run labrat-install yet?"
|
233
|
+
else
|
234
|
+
warn "Label databases at:"
|
235
|
+
db_paths.each do |p|
|
236
|
+
warn "#{p}\n"
|
237
|
+
end
|
238
|
+
warn "\nKnown labels:\n"
|
239
|
+
lab_names.groups_of(6).each do |_n, grp|
|
240
|
+
warn " #{grp.join(', ')}"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
exit(0)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Set the name, size, and style of font.
|
248
|
+
def align_options
|
249
|
+
parser.on("--h-align=[left|center|right|justify]", [:left, :center, :right, :justify],
|
250
|
+
"Horizontal alignment of label text (default center)") do |al|
|
251
|
+
options.h_align = al.to_sym
|
252
|
+
warn " ::h-align <- #{al}::" if options.verbose
|
253
|
+
end
|
254
|
+
parser.on("--v-align=[top|center|bottom]", [:top, :center, :bottom],
|
255
|
+
"Vertical alignment of label text (default center)") do |al|
|
256
|
+
options.v_align = al.to_sym
|
257
|
+
warn " ::v-align <- #{al}::" if options.verbose
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Set the margins between the sides of the label and the bounding box to
|
262
|
+
# hold the text of the label. Left, right, top, and bottom are named
|
263
|
+
# assuming a portrait orientation, that is the orientation the label has
|
264
|
+
# as it comes out of the printer.
|
265
|
+
def padding_options
|
266
|
+
parser.on("--right-pad=DIM",
|
267
|
+
"Distance from right side of label (in portrait) to print area") do |x|
|
268
|
+
options.right_pad = parse_dimension(x, 'right-pad')
|
269
|
+
end
|
270
|
+
parser.on("--left-pad=DIM",
|
271
|
+
"Distance from left side of label (in portrait) to print area") do |x|
|
272
|
+
options.left_pad = parse_dimension(x, 'left-pad')
|
273
|
+
end
|
274
|
+
parser.on("--top-pad=DIM",
|
275
|
+
"Distance from top side of label (in portrait) to print area") do |x|
|
276
|
+
options.top_pad = parse_dimension(x, 'top-pad')
|
277
|
+
end
|
278
|
+
parser.on("--bottom-pad=DIM",
|
279
|
+
"Distance from bottom side of label (in portrait) to print area") do |x|
|
280
|
+
options.bottom_pad = parse_dimension(x, 'bottom-pad')
|
281
|
+
end
|
282
|
+
parser.on("--h-pad=DIM",
|
283
|
+
"Distance from left and right sides of label (in portrait) to print area") do |x|
|
284
|
+
options.left_pad = options.right_pad = parse_dimension(x, 'h-pad')
|
285
|
+
end
|
286
|
+
parser.on("--v-pad=DIM",
|
287
|
+
"Distance from top and bottom sides of label (in portrait) to print area") do |x|
|
288
|
+
options.top_pad = options.bottom_pad = parse_dimension(x, 'v-pad')
|
289
|
+
end
|
290
|
+
parser.on("--pad=DIM",
|
291
|
+
"Distance from all sides of label (in portrait) to print area") do |x|
|
292
|
+
options.left_pad = options.right_pad =
|
293
|
+
options.top_pad = options.bottom_pad = parse_dimension(x, 'pad')
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Set the name, size, and style of font.
|
298
|
+
def font_options
|
299
|
+
parser.on("--font-name=NAME",
|
300
|
+
"Name of font to use (default Helvetica)") do |nm|
|
301
|
+
options.font_name = nm
|
302
|
+
warn " ::font-name <- '#{nm}'::" if options.verbose
|
303
|
+
end
|
304
|
+
parser.on("--font-size=NUM",
|
305
|
+
"Size of font to use in points (default 12)") do |pt|
|
306
|
+
options.font_size = pt
|
307
|
+
warn " ::font-size <- #{pt}::" if options.verbose
|
308
|
+
end
|
309
|
+
parser.on("--font-style=[normal|bold|italic|bold-italic]",
|
310
|
+
%w[normal bold italic bold-italic],
|
311
|
+
"Style of font to use for text (default normal)") do |sty|
|
312
|
+
# Prawn requires :bold_italic, not :"bold-italic"
|
313
|
+
options.font_style = sty.tr('-', '_').to_sym
|
314
|
+
warn " ::font-style <- #{sty}::" if options.verbose
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# Even with accurate dimensions for labels, a combination of drivers, PDF
|
319
|
+
# settings, and perhaps a particular printer may result in text not
|
320
|
+
# sitting precisely where the user intends on the printed label. These
|
321
|
+
# options tweak the PDF settings to compensate for any such anomalies.
|
322
|
+
def delta_options
|
323
|
+
parser.on('-xDIM', "--delta-x=DIM",
|
324
|
+
"Left-right adjustment as label text is oriented") do |x|
|
325
|
+
options.delta_x = parse_dimension(x, 'delta-x')
|
326
|
+
end
|
327
|
+
parser.on('-yDIM', "--delta-y=DIM",
|
328
|
+
"Up-down adjustment as label text is oriented") do |y|
|
329
|
+
options.delta_y = parse_dimension(y, 'delta-y')
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# The name of the printer to send the job to.
|
334
|
+
def printer_name_option
|
335
|
+
parser.on("-pNAME", "--printer=NAME",
|
336
|
+
"Name of the label printer to print on") do |name|
|
337
|
+
options.printer = name
|
338
|
+
warn " ::printer <- '#{name}'::" if options.verbose
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def start_label_option
|
343
|
+
parser.on("-SNUM", "--start-label=NUM", Integer,
|
344
|
+
"Start printing at label number NUM (starting at 1, left-to-right, top-to-bottom)",
|
345
|
+
" within first page only. Later pages always start at label 1.") do |n|
|
346
|
+
options.start_label = n
|
347
|
+
warn " ::start-label <- #{n}::" if options.verbose
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# On a command-line, specifying where a line-break should occur is not
|
352
|
+
# convenient when shell interpretation and quoting rules are taken into
|
353
|
+
# account. This allows the user to use some distinctive marker ('++' by
|
354
|
+
# default) to designate where a line break should occur.
|
355
|
+
def nl_sep_option
|
356
|
+
parser.on("-nSEP", "--nl-sep=SEPARATOR",
|
357
|
+
"Specify text to be translated into a line-break (default '++')") do |nl|
|
358
|
+
options.nl_sep = nl
|
359
|
+
warn " ::nl-sep <- '#{nl}'::" if options.verbose
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# On a command-line, specifying where a new label should be started. This
|
364
|
+
# allows the user to use some distinctive marker ('][' by default) to
|
365
|
+
# designate where a new label shoul be started.
|
366
|
+
def label_sep_option
|
367
|
+
parser.on("--label-sep=SEPARATOR",
|
368
|
+
"Specify text that indicates the start of a new label (default ']*[')") do |ls|
|
369
|
+
options.label-sep = ls
|
370
|
+
warn " ::label-sep <- '#{ls}'::" if options.verbose
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def copies_option
|
375
|
+
parser.on("-cNUM", "--copies=NUM", Integer,
|
376
|
+
"Number of copies of each label to generate") do |n|
|
377
|
+
options.copies = n.abs
|
378
|
+
warn " ::copies <- #{options.copies}::" if options.verbose
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# For batch printing of labels, the user might want to just feed a file of
|
383
|
+
# labels to be printed. This option allows a file name to be give.
|
384
|
+
def in_file_option
|
385
|
+
parser.on("-fFILENAME", "--in-file=FILENAME",
|
386
|
+
"Read labels from given file instead of command-line") do |file|
|
387
|
+
options.in_file = file.strip
|
388
|
+
warn " ::in-file <- '#{file}'::" if options.verbose
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# For batch printing of labels, the user might want to just feed a file of
|
393
|
+
# labels to be printed. This option allows a file name to be give.
|
394
|
+
def out_file_option
|
395
|
+
parser.on("-oFILENAME", "--out-file=FILENAME",
|
396
|
+
"Put generated label in the given file") do |file|
|
397
|
+
file = file.strip
|
398
|
+
unless file =~ /\.pdf\z/i
|
399
|
+
file = "#{file}.pdf"
|
400
|
+
end
|
401
|
+
options.out_file = file
|
402
|
+
warn " ::out-file <- '#{file}'::" if options.verbose
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def command_options
|
407
|
+
# NB: the % is supposed to remind me of the rollers on a printer
|
408
|
+
parser.on("-%PRINTCMD", "--print-command=PRINTCMD",
|
409
|
+
"Command to use for printing with %p for printer name; %o for label file name") do |cmd|
|
410
|
+
options.print_command = cmd.strip
|
411
|
+
warn " ::print-command <- '#{cmd}'::" if options.verbose
|
412
|
+
end
|
413
|
+
# NB: the : is supposed to remind me of two eyeballs viewing the PDF
|
414
|
+
parser.on("-:VIEWCMD", "--view-command=VIEWCMD",
|
415
|
+
"Command to use for viewing with %o for label file name") do |cmd|
|
416
|
+
options.view_command = cmd.strip
|
417
|
+
warn " ::view-command <- '#{cmd}'::" if options.verbose
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# Whether the label ought to be printed in landscape orientation, that is,
|
422
|
+
# the text turned 90 degrees clockwise from the orientation the label has
|
423
|
+
# coming out of the printer.
|
424
|
+
def landscape_option
|
425
|
+
parser.on("-L", "--[no-]landscape",
|
426
|
+
"Orient label in landscape (default false), i.e., with the left of",
|
427
|
+
"the label text starting at the top as the label as printed") do |l|
|
428
|
+
options.landscape = l
|
429
|
+
warn " ::landscape <- #{l}::" if options.verbose
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
# The inverse of landscape, i.e., no rotation is done.
|
434
|
+
def portrait_option
|
435
|
+
parser.on("-P", "--[no-]portrait",
|
436
|
+
"Orient label in portrait (default true), i.e., left-to-right",
|
437
|
+
"top-to-bottom as the label as printed. Negated landscape") do |p|
|
438
|
+
options.landscape = !p
|
439
|
+
warn " portrait option executed as ::landscape <- #{!p}::" if options.verbose
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Whether to preview with the view command instead of print
|
444
|
+
def view_option
|
445
|
+
# Boolean switch.
|
446
|
+
parser.on("-V", "--[no-]view", "View rather than print") do |v|
|
447
|
+
options.view = v
|
448
|
+
warn " ::view <- #{v}::" if options.verbose
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
# Whether to add label grid outline to output
|
453
|
+
def grid_option
|
454
|
+
# Boolean switch.
|
455
|
+
parser.on("-g", "--[no-]grid", "Add grid lines to output") do |g|
|
456
|
+
options.grid = g
|
457
|
+
warn " ::grid <- #{g}::" if options.verbose
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# Ignore any content from in-file, stdin, or the command-line and just
|
462
|
+
# produce a template showing the boundaries of each label on a page of
|
463
|
+
# labels.
|
464
|
+
def template_option
|
465
|
+
parser.on("-T", "--[no-]template",
|
466
|
+
"Print a template of a page of labels and ignore any content.") do |t|
|
467
|
+
options.template = t
|
468
|
+
warn " ::template <- #{t}::" if options.verbose
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# Whether we ought to be blabby about what we're up to.
|
473
|
+
def verbose_option
|
474
|
+
# Boolean switch.
|
475
|
+
parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
476
|
+
options.verbose = v
|
477
|
+
warn " ::verbose <- #{v}::" if options.verbose
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Labrat
|
4
|
+
# This class is responsible for finding a config files, reading them, and
|
5
|
+
# returning a Hash to reflect the configuration. We use YAML as the
|
6
|
+
# configuration format and look for the config file in the standard places.
|
7
|
+
class Config
|
8
|
+
# Return a Hash of the YAML-ized config files for app_name directories.
|
9
|
+
# Config file may be located in either the xdg locations (containing any
|
10
|
+
# variant of base: base, base.yml, or base.yaml) or in the classic
|
11
|
+
# locations (/etc/app_namerc, /etc/app_name, ~/.app_namerc~, or
|
12
|
+
# ~/.app_name/base[.ya?ml]). Return a hash that reflects the merging of
|
13
|
+
# those files according to the following priorities, from highest to
|
14
|
+
# lowest:
|
15
|
+
#
|
16
|
+
# 1. A config file pointed to by the environment variable APPNAME_CONFIG
|
17
|
+
# 2. User classic config files
|
18
|
+
# 3. User xdg config files for app_name,
|
19
|
+
# 4. A config file pointed to by the environment variable APPNAME_SYS_CONFIG
|
20
|
+
# 5. System classic config files,
|
21
|
+
# 6. System xdg config files for for app_name,
|
22
|
+
#
|
23
|
+
# If an environment variable is found, the search for xdg and classic
|
24
|
+
# config files is skipped. Any dir_prefix is pre-pended to search
|
25
|
+
# locations environment, xdg and classic config paths so you can run this
|
26
|
+
# on a temporary directory set up for testing.
|
27
|
+
def self.read(app_name, base: 'config', dir_prefix: '', xdg: true, verbose: false)
|
28
|
+
paths = config_paths(app_name, base: base, dir_prefix: dir_prefix, xdg: xdg)
|
29
|
+
sys_configs = paths[:system]
|
30
|
+
usr_configs = paths[:user]
|
31
|
+
merge_configs_from((sys_configs + usr_configs).compact, verbose: verbose)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.config_paths(app_name, base: 'config', dir_prefix: '', xdg: true)
|
35
|
+
sys_configs = []
|
36
|
+
sys_env_name = "#{app_name.upcase}_SYS_CONFIG"
|
37
|
+
if ENV[sys_env_name]
|
38
|
+
sys_fname = File.join(dir_prefix, File.expand_path(ENV[sys_env_name]))
|
39
|
+
sys_configs << sys_fname if File.readable?(sys_fname)
|
40
|
+
else
|
41
|
+
sys_configs +=
|
42
|
+
if xdg
|
43
|
+
find_xdg_sys_config_files(app_name, base, dir_prefix)
|
44
|
+
else
|
45
|
+
find_classic_sys_config_files(app_name, base, dir_prefix)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
usr_configs = []
|
50
|
+
usr_env_name = "#{app_name.upcase}_CONFIG"
|
51
|
+
if ENV[usr_env_name]
|
52
|
+
usr_fname = File.join(dir_prefix, File.expand_path(ENV[usr_env_name]))
|
53
|
+
usr_configs << usr_fname if File.readable?(usr_fname)
|
54
|
+
else
|
55
|
+
usr_configs <<
|
56
|
+
if xdg
|
57
|
+
find_xdg_user_config_file(app_name, base, dir_prefix)
|
58
|
+
else
|
59
|
+
find_classic_user_config_file(app_name, dir_prefix)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
{ system: sys_configs.compact, user: usr_configs.compact }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Merge the settings from the given Array of config files in order from
|
66
|
+
# lowest priority to highest priority, starting with an empty hash. Any
|
67
|
+
# values of the top-level hash that are themselves Hashes are merged
|
68
|
+
# recursively.
|
69
|
+
def self.merge_configs_from(files = [], verbose: false)
|
70
|
+
hash = {}
|
71
|
+
files.each do |f|
|
72
|
+
next unless File.readable?(f)
|
73
|
+
|
74
|
+
yml_hash = YAML.load(File.read(f))&.methodize || {}
|
75
|
+
yml_hash.report("Merging from file '#{f}") if verbose
|
76
|
+
hash.deep_merge!(yml_hash)
|
77
|
+
end
|
78
|
+
hash
|
79
|
+
end
|
80
|
+
|
81
|
+
########################################################################
|
82
|
+
# XDG config files
|
83
|
+
########################################################################
|
84
|
+
|
85
|
+
# From the XDG standard:
|
86
|
+
# Your application should store and load data and configuration files to/from
|
87
|
+
# the directories pointed by the following environment variables:
|
88
|
+
#
|
89
|
+
# $XDG_CONFIG_HOME (default: "$HOME/.config"): user-specific configuration files.
|
90
|
+
# $XDG_CONFIG_DIRS (default: "/etc/xdg"): precedence-ordered set of system configuration directories.
|
91
|
+
|
92
|
+
# Return the absolute path names of all XDG system config files for
|
93
|
+
# app_name with the basename variants of base. Return the lowest priority
|
94
|
+
# files first, highest last. Prefix the search locations with dir_prefix
|
95
|
+
# if given.
|
96
|
+
def self.find_xdg_sys_config_files(app_name, base, dir_prefix)
|
97
|
+
configs = []
|
98
|
+
xdg_search_dirs = ENV['XDG_CONFIG_DIRS']&.split(':')&.reverse || ['/etc/xdg']
|
99
|
+
xdg_search_dirs.each do |dir|
|
100
|
+
dir = File.expand_path(File.join(dir, app_name))
|
101
|
+
dir = File.join(dir_prefix, dir) unless dir_prefix.nil? || dir_prefix.strip.empty?
|
102
|
+
base = app_name if base.nil? || base.strip.empty?
|
103
|
+
base_candidates = [base.to_s, "#{base}.yml", "#{base}.yaml",
|
104
|
+
"#{base}.cfg", "#{base}.config"]
|
105
|
+
config_fname = base_candidates.find { |b| File.readable?(File.join(dir, b)) }
|
106
|
+
configs << File.join(dir, config_fname) if config_fname
|
107
|
+
end
|
108
|
+
configs
|
109
|
+
end
|
110
|
+
|
111
|
+
# Return the absolute path name of any XDG user config files for app_name
|
112
|
+
# with the basename variants of base. The XDG_CONFIG_HOME environment
|
113
|
+
# variable for the user configs is intended to be the name of a single xdg
|
114
|
+
# config directory, not a list of colon-separated directories as for the
|
115
|
+
# system config. Return the name of a config file for this app in
|
116
|
+
# XDG_CONFIG_HOME (or ~/.config by default). Prefix the search location
|
117
|
+
# with dir_prefix if given.
|
118
|
+
def self.find_xdg_user_config_file(app_name, base, dir_prefix)
|
119
|
+
dir_prefix ||= ''
|
120
|
+
base ||= (base&.strip || app_name)
|
121
|
+
xdg_search_dir = ENV['XDG_CONFIG_HOME'] || ['~/.config']
|
122
|
+
dir = File.expand_path(File.join(xdg_search_dir, app_name))
|
123
|
+
dir = File.join(dir_prefix, dir) unless dir_prefix.strip.empty?
|
124
|
+
return nil unless Dir.exist?(dir)
|
125
|
+
|
126
|
+
base_candidates = [base.to_s, "#{base}.yml", "#{base}.yaml",
|
127
|
+
"#{base}.cfg", "#{base}.config"]
|
128
|
+
config_fname = base_candidates.find { |b| File.readable?(File.join(dir, b)) }
|
129
|
+
if config_fname
|
130
|
+
File.join(dir, config_fname)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
########################################################################
|
135
|
+
# Classic config files
|
136
|
+
########################################################################
|
137
|
+
|
138
|
+
# Return the absolute path names of all "classic" system config files for
|
139
|
+
# app_name with the basename variants of base. Return the lowest priority
|
140
|
+
# files first, highest last. Prefix the search locations with dir_prefix
|
141
|
+
# if given.
|
142
|
+
def self.find_classic_sys_config_files(app_name, base, dir_prefix)
|
143
|
+
dir_prefix ||= ''
|
144
|
+
configs = []
|
145
|
+
env_config = ENV["#{app_name.upcase}_SYS_CONFIG"]
|
146
|
+
if env_config && File.readable?((config = File.join(dir_prefix, File.expand_path(env_config))))
|
147
|
+
configs = [config]
|
148
|
+
elsif File.readable?(config = File.join(dir_prefix, "/etc/#{app_name}"))
|
149
|
+
configs = [config]
|
150
|
+
elsif File.readable?(config = File.join(dir_prefix, "/etc/#{app_name}rc"))
|
151
|
+
configs = [config]
|
152
|
+
else
|
153
|
+
dir = File.join(dir_prefix, "/etc/#{app_name}")
|
154
|
+
if Dir.exist?(dir)
|
155
|
+
base = app_name if base.nil? || base.strip.empty?
|
156
|
+
base_candidates = ["#{base}" "#{base}.yml", "#{base}.yaml",
|
157
|
+
"#{base}.cfg", "#{base}.config"]
|
158
|
+
config = base_candidates.find { |b| File.readable?(File.join(dir, b)) }
|
159
|
+
configs = [File.join(dir, config)] if config
|
160
|
+
end
|
161
|
+
end
|
162
|
+
configs
|
163
|
+
end
|
164
|
+
|
165
|
+
# Return the absolute path names of all "classic" system config files for
|
166
|
+
# app_name with the basename variants of base. Return the lowest priority
|
167
|
+
# files first, highest last. Prefix the search locations with dir_prefix if
|
168
|
+
# given.
|
169
|
+
def self.find_classic_user_config_file(app_name, dir_prefix)
|
170
|
+
dir_prefix ||= ''
|
171
|
+
config_fname = nil
|
172
|
+
env_config = ENV["#{app_name.upcase}_CONFIG"]
|
173
|
+
if env_config && File.readable?((config = File.join(dir_prefix, File.expand_path(env_config))))
|
174
|
+
config_fname = config
|
175
|
+
elsif Dir.exist?(config_dir = File.join(dir_prefix, File.expand_path("~/.#{app_name}")))
|
176
|
+
base_candidates = ["config.yml", "config.yaml", "config"]
|
177
|
+
base_fname = base_candidates.find { |b| File.readable?(File.join(config_dir, b)) }
|
178
|
+
config_fname = File.join(config_dir, base_fname)
|
179
|
+
elsif Dir.exist?(config_dir = File.join(dir_prefix, File.expand_path('~/')))
|
180
|
+
base_candidates = [".#{app_name}", ".#{app_name}rc", ".#{app_name}.yml", ".#{app_name}.yaml",
|
181
|
+
".#{app_name}.cfg", ".#{app_name}.config"]
|
182
|
+
base_fname = base_candidates.find { |b| File.readable?(File.join(config_dir, b)) }
|
183
|
+
config_fname = File.join(config_dir, base_fname)
|
184
|
+
end
|
185
|
+
config_fname
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|