labrat 0.1.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
|