labrat 0.1.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ class OptionError < StandardError; end
5
+
6
+ class DimensionError < StandardError; end
7
+
8
+ class LabelNameError < StandardError; end
9
+
10
+ class EmptyLabelError < StandardError; end
11
+
12
+ class RecursionError < StandardError; end
13
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ class Hash
3
+ # Transform hash keys to symbols suitable for calling as methods, i.e.,
4
+ # translate any hyphens to underscores. This is the form we want to keep
5
+ # config hashes in Labrat.
6
+ def methodize
7
+ transform_keys { |k| k.to_s.gsub('-', '_').to_sym }
8
+ end
9
+
10
+ # Convert the given Hash into a Array of Strings that represent an
11
+ # equivalent set of command-line args and pass them into the #parse method.
12
+ def optionize
13
+ options = []
14
+ each_pair do |k, v|
15
+ key = k.to_s.gsub('_', '-')
16
+ options <<
17
+ if [TrueClass, FalseClass].include?(v.class)
18
+ v ? "--#{key}" : "--no-#{key}"
19
+ else
20
+ "--#{key}=#{v}"
21
+ end
22
+ end
23
+ options
24
+ end
25
+
26
+ def report(title)
27
+ warn "#{title}:"
28
+ if empty?
29
+ warn " [[Empty]]"
30
+ else
31
+ each do |k, v|
32
+ val = v.class == Float ? v.round(2).to_s + 'pt' : v
33
+ warn " #{k}: #{val}"
34
+ end
35
+ end
36
+ warn ""
37
+ end
38
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ class Label
5
+ attr_reader :ops
6
+ attr_accessor :texts
7
+
8
+ def initialize(texts, ops)
9
+ @ops = ops
10
+ unless @ops.nl_sep.nil? || @ops.nl_sep == ''
11
+ @texts = texts.map { |t| t.gsub(ops.nl_sep, "\n") }
12
+ end
13
+ if @ops.copies > 1
14
+ duped_texts = []
15
+ @texts.each { |t| @ops.copies.times { duped_texts << t } }
16
+ @texts = duped_texts
17
+ end
18
+ end
19
+
20
+ def generate
21
+ # The default margin is 0.5in on all sides, way too big for labels, so
22
+ # it is important to set these here. The margins' designation as "top,"
23
+ # "left," "bottom," and "right" take into account the page layout. That
24
+ # is, the left margin is on the left in both portrait and landscape
25
+ # orientations. But I want the user to be able to set the margins
26
+ # according to the label type, independent of the orientation. I adopt
27
+ # the convention that the margins are named assuming a portrait
28
+ # orientation and swap them here so that when Prawn swaps them again,
29
+ # they come out correct.
30
+ layout = ops.landscape ? :landscape : :portrait
31
+ if layout == :portrait
32
+ tpm = ops.top_page_margin
33
+ bpm = ops.bottom_page_margin
34
+ lpm = ops.left_page_margin
35
+ rpm = ops.right_page_margin
36
+ else
37
+ lpm = ops.top_page_margin
38
+ rpm = ops.bottom_page_margin
39
+ tpm = ops.left_page_margin
40
+ bpm = ops.right_page_margin
41
+ end
42
+ out_file = File.expand_path(ops.out_file)
43
+ Prawn::Document.generate(out_file, page_size: [ops.page_width, ops.page_height],
44
+ left_margin: lpm, right_margin: rpm,
45
+ top_margin: tpm, bottom_margin: bpm,
46
+ page_layout: layout) do |pdf|
47
+ # Define a grid with each grid box to be used for a single label.
48
+ pdf.define_grid(rows: ops.rows, columns: ops.columns,
49
+ row_gutter: ops.row_gap, column_gutter: ops.column_gap)
50
+ if ops.verbose
51
+ warn "Page dimensions:"
52
+ warn " [pg_wd, pg_ht] = [#{ops.page_width.round(2)}pt,#{ops.page_height.round(2)}pt]"
53
+ warn " orientation: #{layout}"
54
+ warn " [rows, columns] = [#{ops.rows},#{ops.columns}]"
55
+ warn " [lpm, rpm] = [#{lpm.round(2)}pt,#{rpm.round(2)}pt]"
56
+ warn " [tpm, bpm] = [#{tpm.round(2)}pt,#{bpm.round(2)}pt]"
57
+ warn ""
58
+ end
59
+ if ops.template
60
+ # Replace any texts with the numbers and show the grid.
61
+ self.texts = (1..(ops.rows * ops.columns)).map(&:to_s)
62
+ ops.font_name = 'Helvetica'
63
+ ops.font_style = 'bold'
64
+ ops.font_size = 16
65
+ pdf.grid.show_all
66
+ end
67
+ raise EmptyLabelError, "Empty label" if waste_of_labels?
68
+
69
+ last_k = texts.size - 1
70
+ lab_dims_reported = false
71
+ texts.each_with_index do |text, k|
72
+ row, col = row_col(k + 1)
73
+ pdf.grid(row, col).bounding_box do
74
+ bounds = pdf.bounds
75
+ pdf.stroke_bounds if ops.grid
76
+ box_wd = (bounds.right - bounds.left) - ops.left_pad - ops.right_pad
77
+ box_ht = (bounds.top - bounds.bottom) - ops.top_pad - ops.bottom_pad
78
+ box_x = ops.left_pad + ops.delta_x
79
+ box_y = ops.bottom_pad + box_ht + ops.delta_y
80
+ pdf.font ops.font_name, style: ops.font_style, size: ops.font_size.to_f
81
+ pdf.text_box(text, width: box_wd, height: box_ht,
82
+ align: ops.h_align, valign: ops.v_align,
83
+ overflow: :truncate, at: [box_x, box_y])
84
+ if ops.verbose && !lab_dims_reported
85
+ warn "Label text box dimensions:"
86
+ warn " [box_wd, box_ht] = [#{box_wd.round(2)}pt,#{box_ht.round(2)}pt]"
87
+ warn " [box_x, box_y] = [#{box_x.round(2)}pt,#{box_y.round(2)}pt]"
88
+ warn " [delta_x, delta_y] = [#{ops.delta_x.round(2)}pt,#{ops.delta_y.round(2)}pt]"
89
+ warn ''
90
+ lab_dims_reported = true
91
+ end
92
+ if ops.verbose
93
+ warn "Label \##{(k % lpp) + 1} on page #{page_num(k)} at row #{row + 1}, column #{col + 1}:"
94
+ warn '-------------------'
95
+ warn text
96
+ warn '-------------------'
97
+ warn ''
98
+ end
99
+ pdf.start_new_page if needs_new_page?(k, last_k)
100
+ end
101
+ end
102
+ end
103
+ self
104
+ end
105
+
106
+ def print
107
+ cmd = ops.print_command.gsub('%p', ops.printer).gsub('%o', ops.out_file)
108
+ if ops.verbose
109
+ warn "Printing with:"
110
+ warn " #{cmd} &"
111
+ end
112
+ system("#{cmd} &")
113
+ end
114
+
115
+ def view
116
+ cmd = ops.view_command.gsub('%o', ops.out_file)
117
+ if ops.verbose
118
+ warn "Viewing with:"
119
+ warn " #{cmd} &"
120
+ end
121
+ system("#{cmd} &")
122
+ end
123
+
124
+ def remove
125
+ FileUtils.rm(ops.out_file)
126
+ end
127
+
128
+ # Labels per page
129
+ def lpp
130
+ ops.rows * ops.columns
131
+ end
132
+
133
+ # Page number of the kth label
134
+ def page_num(k)
135
+ k.divmod(lpp)[0] + 1
136
+ end
137
+
138
+ # Return the 0-based row and column within a page on which the k-th
139
+ # (1-based) label should be printed.
140
+ def row_col(k)
141
+ k_on_page = (k + ops.start_label - 2) % lpp
142
+ k_on_page.divmod(ops.columns)
143
+ end
144
+
145
+ # Should we emit a new page at this point?
146
+ def needs_new_page?(k, last_k)
147
+ return false if k == last_k
148
+ r, c = row_col(k + 1)
149
+ (r + 1 == ops.rows && c + 1 == ops.columns)
150
+ end
151
+
152
+ # Would we just be printing blank labels?
153
+ def waste_of_labels?
154
+ (texts.nil? || texts.empty? || texts.all?(&:blank?)) &&
155
+ !ops.view
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ module LabelDb
5
+ class << self
6
+ # Module-level variable to hold the merged database.
7
+ attr_accessor :db
8
+ end
9
+
10
+ # Read in the Labrat database of label settings, merging system and user
11
+ # databases.
12
+ def self.read(dir_prefix: '')
13
+ self.db = Config.read('labrat', base: 'labeldb', dir_prefix: dir_prefix)
14
+ .transform_keys(&:to_sym)
15
+ end
16
+
17
+ # Return a hash of config settings for the label named by labname.
18
+ def self.[](labname)
19
+ read unless db
20
+ db[labname.to_sym] || {}
21
+ end
22
+
23
+ # Set a runtime configuration for a single labelname.
24
+ def self.[]=(labname, config = {})
25
+ read unless db
26
+ db[labname.to_sym] = config
27
+ end
28
+
29
+ # Return an Array of label names.
30
+ def self.known_names
31
+ read unless db
32
+ db.keys.sort
33
+ end
34
+
35
+ def self.db_paths(dir_prefix = '')
36
+ system_db_paths(dir_prefix) + user_db_paths(dir_prefix)
37
+ end
38
+
39
+ def self.system_db_paths(dir_prefix = '')
40
+ paths = Config.config_paths('labrat', base: 'labeldb', dir_prefix: dir_prefix)
41
+ paths[:system]
42
+ end
43
+
44
+ def self.user_db_paths(dir_prefix = '')
45
+ paths = Config.config_paths('labrat', base: 'labeldb', dir_prefix: dir_prefix)
46
+ paths[:user]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ # The Options class is a glorified Hash, a container for the options
5
+ # settings gathered from the defaults, the config files, the command line,
6
+ # and perhaps environment. An Options instance can be handed off to the
7
+ # label-printing objects to inform its formatting, printing, etc.
8
+ class Options
9
+ attr_accessor :label, :page_width, :page_height,
10
+ :left_page_margin, :right_page_margin,
11
+ :top_page_margin, :bottom_page_margin,
12
+ :rows, :columns, :row_gap, :column_gap, :landscape,
13
+ :start_label, :grid,
14
+ :h_align, :v_align,
15
+ :left_pad, :right_pad, :top_pad, :bottom_pad,
16
+ :delta_x, :delta_y,
17
+ :font_name, :font_style, :font_size,
18
+ :in_file, :nl_sep, :label_sep, :copies,
19
+ :printer, :out_file, :print_command, :view_command, :view,
20
+ :template, :verbose, :msg
21
+
22
+ # Initialize with an optional hash of default values for the attributes.
23
+ def initialize(**init)
24
+ self.label = init[:label] || nil
25
+ # Per-page attributes
26
+ self.page_width = init[:page_width] || 24.mm
27
+ self.page_height = init[:page_height] || 87.mm
28
+ self.left_page_margin = init[:left_page_margin] || 5.mm
29
+ self.right_page_margin = init[:right_page_margin] || 5.mm
30
+ self.top_page_margin = init[:top_page_margin] || 0.mm
31
+ self.bottom_page_margin = init[:bottom_page_margin] || 0.mm
32
+ self.rows = init[:rows] || 1
33
+ self.columns = init[:columns] || 1
34
+ self.row_gap = init[:row_gap] || 0.mm
35
+ self.column_gap = init[:column_gap] || 0.mm
36
+ self.start_label = init[:start_label] || 1
37
+ self.landscape = init.fetch(:landscape, false)
38
+ # Per-label attributes
39
+ self.h_align = init[:h_align]&.to_sym || :center
40
+ self.v_align = init[:v_align]&.to_sym || :center
41
+ self.left_pad = init[:left_pad] || 4.5.mm
42
+ self.right_pad = init[:right_pad] || 4.5.mm
43
+ self.top_pad = init[:top_pad] || 0
44
+ self.bottom_pad = init[:bottom_pad] || 0
45
+ self.delta_x = init[:delta_x] || 0
46
+ self.delta_y = init[:delta_y] || 0
47
+ self.font_name = init[:font_name] || 'Helvetica'
48
+ self.font_style = init[:font_style]&.to_sym || :normal
49
+ self.font_size = init[:font_size] || 12
50
+ # Input attributes
51
+ self.in_file = init[:in_file] || nil
52
+ self.nl_sep = init[:nl_sep] || '++'
53
+ self.label_sep = init[:label_sep] || ']*['
54
+ self.copies = init[:copies] || 1
55
+ # Output attributes
56
+ self.printer = init[:printer] || ENV['LABRAT_PRINTER'] || ENV['PRINTER'] || 'dymo'
57
+ self.out_file = init[:out_file] || 'labrat.pdf'
58
+ self.print_command = init[:print_command] || 'lpr -P %p %o'
59
+ self.view_command = init[:view_command] || 'qpdfview --unique --instance labrat %o'
60
+ self.view = init.fetch(:view, false)
61
+ self.template = init.fetch(:landscape, false)
62
+ self.grid = init.fetch(:gid, false)
63
+ self.verbose = init.fetch(:verbose, false)
64
+ self.msg = init[:msg] || nil
65
+ end
66
+
67
+ # High-level setting of options from config files, and given command-line
68
+ # args.
69
+ def self.set_options(args, verbose: false)
70
+ # Default, built-in config; set verbose to param.
71
+ default_config = Labrat::Options.new(verbose: verbose).to_hash
72
+ default_config.report("Default settings") if verbose
73
+
74
+ # Config files
75
+ file_config = Labrat::Config.read('labrat', verbose: verbose)
76
+ file_config.report("Settings from merged config files") if verbose
77
+ file_options = Labrat::ArgParser.new.parse(file_config, prior: default_config, verbose: verbose)
78
+
79
+ # Command-line
80
+ if verbose
81
+ warn "Command-line:"
82
+ args.each do |arg|
83
+ warn arg.to_s
84
+ end
85
+ warn ""
86
+ end
87
+ Labrat::ArgParser.new.parse(args, prior: file_options, verbose: verbose)
88
+ end
89
+
90
+ # Return any string in msg, e.g., the usage help or error.
91
+ def to_s
92
+ msg
93
+ end
94
+
95
+ # Allow hash-like assignment to attributes. This allows an Options object
96
+ # to be used, for example, in the OptionParser#parse :into parameter.
97
+ def []=(att, val)
98
+ att = att.to_s.gsub('-', '_')
99
+ send("#{att}=", val)
100
+ end
101
+
102
+ # Allow hash-like access to attributes. This allows an Options object
103
+ # to be used, for example, in the OptionParser#parse :into parameter.
104
+ def [](att)
105
+ att = att.to_s.gsub('-', '_')
106
+ send(att.to_s)
107
+ end
108
+
109
+ # For testing, return an Array of the attributes as symbols.
110
+ def self.attrs
111
+ instance_methods(false).grep(/\A[a-z_]+=\Z/)
112
+ .map { |a| a.to_s.sub(/=\z/, '').to_sym }
113
+ end
114
+
115
+ # For testing, return an Array of the flags-form of the attributes, i.e.,
116
+ # with the underscores, _, replaced with hyphens.
117
+ def self.flags
118
+ attrs.map { |a| a.gsub('_', '-') }
119
+ end
120
+
121
+ # Return a hash of the values in this Options object. This is the
122
+ # canonical form of a Hash for Labrat, i.e., symbolic keys with any
123
+ # hyphens translated into underscores. Don't include the msg attribute.
124
+ def to_hash
125
+ {
126
+ label: label,
127
+ page_width: page_width,
128
+ page_height: page_height,
129
+ left_page_margin: left_page_margin,
130
+ right_page_margin: right_page_margin,
131
+ top_page_margin: top_page_margin,
132
+ bottom_page_margin: bottom_page_margin,
133
+ rows: rows,
134
+ columns: columns,
135
+ row_gap: row_gap,
136
+ column_gap: column_gap,
137
+ grid: grid,
138
+ start_label: start_label,
139
+ landscape: landscape,
140
+ # Per-label attributes
141
+ h_align: h_align,
142
+ v_align: v_align,
143
+ left_pad: left_pad,
144
+ right_pad: right_pad,
145
+ top_pad: top_pad,
146
+ bottom_pad: bottom_pad,
147
+ delta_x: delta_x,
148
+ delta_y: delta_y,
149
+ font_name: font_name,
150
+ font_style: font_style,
151
+ font_size: font_size,
152
+ # Input attributes
153
+ in_file: in_file,
154
+ nl_sep: nl_sep,
155
+ label_sep: label_sep,
156
+ copies: copies,
157
+ # Output attributes
158
+ printer: printer,
159
+ out_file: out_file,
160
+ print_command: print_command,
161
+ view_command: view_command,
162
+ view: view,
163
+ template: template,
164
+ verbose: verbose,
165
+ }
166
+ end
167
+
168
+ # Update the fields of this Option instance by merging in the values in
169
+ # hsh into self. Ignore any keys in hsh not corresponding to a setter for
170
+ # an Options object.
171
+ def merge!(hsh)
172
+ # Convert any separator hyphens in the hash keys to underscores
173
+ hsh = hsh.to_hash.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }
174
+ new_hash = to_hash.merge(hsh)
175
+ new_hash.each_pair do |k, val|
176
+ setter = "#{k}=".to_sym
177
+ next unless respond_to?(setter)
178
+
179
+ send(setter, val)
180
+ end
181
+ self
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ def self.read_label_texts(fname, nlsep)
5
+ file =
6
+ if fname
7
+ ofname = fname
8
+ fname = File.expand_path(fname)
9
+ unless File.readable?(fname)
10
+ raise "Cannot open label file '#{ofname}' for reading"
11
+ end
12
+ File.open(fname)
13
+ else
14
+ $stdin
15
+ end
16
+
17
+ texts = []
18
+ label = nil
19
+ file.each do |line|
20
+ next if line =~ /\A#/
21
+
22
+ if line =~ /\A\s*\z/
23
+ # At blank line record any accumulated label into texts, but remove
24
+ # the nlsep from the end.
25
+ if label
26
+ texts << label.sub(/#{Regexp.quote(nlsep)}\z/, '')
27
+ label = nil
28
+ end
29
+ else
30
+ # Append a non-blank line to the current label, creating it if
31
+ # necessary.
32
+ label ||= +''
33
+ label << line.chomp + nlsep
34
+ end
35
+ end
36
+ # Last label in the file.
37
+ texts << label.sub(/#{Regexp.quote(nlsep)}\z/, '') if label
38
+ texts
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ VERSION = "0.1.13"
5
+ end
data/lib/labrat.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext'
5
+ require 'fat_core/enumerable'
6
+ require 'prawn'
7
+ require 'prawn/measurement_extensions'
8
+ require 'optparse'
9
+ require 'yaml'
10
+ require 'pp'
11
+
12
+ require_relative "labrat/version"
13
+ require_relative "labrat/errors"
14
+ require_relative "labrat/hash"
15
+ require_relative "labrat/options"
16
+ require_relative "labrat/arg_parser"
17
+ require_relative "labrat/label"
18
+ require_relative "labrat/config"
19
+ require_relative "labrat/label_db"
20
+ require_relative "labrat/read_files"
@@ -0,0 +1,108 @@
1
+ ;;;; labrat -- Print labels using labrat from within a buffer.
2
+
3
+ ;;; Commentary:
4
+
5
+ ;; Lisp commands to print labels from within Emacs a buffer by invoking
6
+ ;; labrat, a Ruby application desgined for printing labels from the
7
+ ;; command-line rather than through a GUI. You must have labrat installed
8
+ ;; first. See https://github.com/ddoherty03/labrat for details.
9
+
10
+ ;;; Code:
11
+
12
+ (require 'thingatpt)
13
+ (require 's)
14
+ (require 'dash)
15
+
16
+ (defcustom labrat-executable "labrat"
17
+ "Executable for labrat.
18
+
19
+ If the executable is not in your variable `exec-path', set this
20
+ to the full path name of the executable,
21
+ e.g. ~/.rbenv/shims/labrat, for an rbenv ruby installation."
22
+ :type 'string
23
+ :group 'labrat)
24
+
25
+ (defcustom labrat-nl-sep "++"
26
+ "String to mark newlines in label text.
27
+
28
+ If you change this, you need to make a corresponding change in your
29
+ labrat configuration at ~/.config/labrat/config.yml."
30
+ :type 'string
31
+ :group 'labrat)
32
+
33
+ (defcustom labrat-label-sep "]*["
34
+ "String to mark the separation between labels on the labrat command-line.
35
+
36
+ If you change this, you need to make a corresponding change in your
37
+ labrat configuration at ~/.config/labrat/config.yml."
38
+ :type 'string
39
+ :group 'labrat)
40
+
41
+ (defun labrat/pars-in-region ()
42
+ "Return a string of paragraphs in region, separated by `labrat-label-sep'.
43
+
44
+ If the region is not active, just return the paragraph at or before point as
45
+ is done by `labrat-par-at-point'. In either case strip comment lines."
46
+ (if (region-active-p)
47
+ (progn
48
+ (let ((beg (region-beginning))
49
+ (end (save-excursion
50
+ (progn (goto-char (region-end))
51
+ (forward-paragraph)
52
+ (point))))
53
+ (pars ""))
54
+ (progn
55
+ (save-excursion
56
+ (goto-char beg)
57
+ (while (< (point) end)
58
+ (forward-paragraph)
59
+ (setq pars (s-concat pars (labrat/par-at-point) labrat-label-sep))))
60
+ (s-chop-prefix labrat-label-sep (s-chop-suffix labrat-label-sep pars)))))
61
+ (labrat/par-at-point)))
62
+
63
+ (defun labrat/par-at-point ()
64
+ "Return the paragraph at or before point.
65
+
66
+ Similar to the command `thing-at-point' for paragraph, but look
67
+ for preceding paragraph even if there are several blank lines
68
+ before point, trim white space, comments, and properties from the
69
+ result."
70
+ (save-excursion
71
+ (unless (looking-at ".+")
72
+ (re-search-backward "^.+$" nil 'to-bob))
73
+ (s-replace "\n" labrat-nl-sep
74
+ (labrat/remove-comments
75
+ (s-trim (thing-at-point 'paragraph t))))))
76
+
77
+ (defun labrat/remove-comments (str)
78
+ "Remove any lines from STR that start with the comment character '#'.
79
+
80
+ If STR consists of multiple new-line separated lines, the lines
81
+ that start with '#' are removed, and the remaining lines
82
+ returned"
83
+ (s-join "\n" (--remove (string-match "^#" it) (s-split "\n" str))))
84
+
85
+ (defun labrat-view ()
86
+ "View the paragraph at or before point as a label with labrat.
87
+
88
+ This invokes the \"labrat -V\ <label>\" command with the
89
+ paragraph at or before point inserted in the <label> position,
90
+ but with each new-line replaced with the value of the variable
91
+ labrat-nl-sep, '++' by default."
92
+ (interactive)
93
+ (call-process labrat-executable nil nil nil
94
+ "-V" (labrat/pars-in-region)))
95
+
96
+ (defun labrat-print ()
97
+ "Print the paragraph at or before point as a label with labrat.
98
+
99
+ This invokes the \"labrat -P <label>\" command with the paragraph
100
+ at or before point inserted in the <label> position, but with
101
+ each new-line replaced with the value of the variable
102
+ labrat-nl-sep, '++' by default."
103
+ (interactive)
104
+ (call-process labrat-executable nil nil nil
105
+ (labrat/pars-in-region)))
106
+
107
+ (provide 'labrat)
108
+ ;;; labrat.el ends here