tessella 0.3.0

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.
Files changed (188) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +900 -0
  4. data/doc/README.md +74 -0
  5. data/doc/bitfield.md +123 -0
  6. data/doc/charts.md +1370 -0
  7. data/doc/check.rb +75 -0
  8. data/doc/cli.md +483 -0
  9. data/doc/composition.md +599 -0
  10. data/doc/getting-started.md +121 -0
  11. data/doc/glyphs.md +134 -0
  12. data/doc/images.md +325 -0
  13. data/doc/library.md +239 -0
  14. data/doc/lint.md +270 -0
  15. data/doc/meter.md +210 -0
  16. data/doc/rule.md +51 -0
  17. data/doc/seq.md +169 -0
  18. data/doc/sparkline.md +63 -0
  19. data/doc/table.md +347 -0
  20. data/doc/targets.md +222 -0
  21. data/doc/text.md +84 -0
  22. data/doc/tree.md +108 -0
  23. data/doc/wave.md +122 -0
  24. data/examples/README.md +103 -0
  25. data/examples/ages.txt +9 -0
  26. data/examples/bad.csv +3 -0
  27. data/examples/boxes.txt +3 -0
  28. data/examples/broken.txt +4 -0
  29. data/examples/calibration.rb +12 -0
  30. data/examples/cities.csv +4 -0
  31. data/examples/cjk.rb +13 -0
  32. data/examples/comment.rb +9 -0
  33. data/examples/comment.sh +13 -0
  34. data/examples/cpu.txt +60 -0
  35. data/examples/dashboard.sh +25 -0
  36. data/examples/datasheet.rb +16 -0
  37. data/examples/disc.pgm +0 -0
  38. data/examples/disks.sh +18 -0
  39. data/examples/disks.txt +5 -0
  40. data/examples/dose.txt +7 -0
  41. data/examples/drift.txt +4 -0
  42. data/examples/ethernet.txt +3 -0
  43. data/examples/figure.c +5 -0
  44. data/examples/fit.rb +16 -0
  45. data/examples/floors.rb +21 -0
  46. data/examples/gallery.sh +20 -0
  47. data/examples/gauges.sh +31 -0
  48. data/examples/gradient.rb +16 -0
  49. data/examples/growth.txt +6 -0
  50. data/examples/hosts.csv +4 -0
  51. data/examples/icmp.txt +3 -0
  52. data/examples/ipv4.txt +12 -0
  53. data/examples/kinds.csv +4 -0
  54. data/examples/langs.csv +4 -0
  55. data/examples/lint.rb +16 -0
  56. data/examples/load.rb +14 -0
  57. data/examples/login.txt +10 -0
  58. data/examples/neg.csv +4 -0
  59. data/examples/normal.txt +200 -0
  60. data/examples/note.txt +2 -0
  61. data/examples/notes-fixed.md +17 -0
  62. data/examples/notes.md +17 -0
  63. data/examples/ohlc.txt +7 -0
  64. data/examples/os.csv +10 -0
  65. data/examples/outline.txt +8 -0
  66. data/examples/plot.txt +5 -0
  67. data/examples/refusals.rb +17 -0
  68. data/examples/report.sh +21 -0
  69. data/examples/rfc.sh +22 -0
  70. data/examples/samples.txt +7 -0
  71. data/examples/shapes.pgm +0 -0
  72. data/examples/sizes.tsv +3 -0
  73. data/examples/spi.txt +6 -0
  74. data/examples/status.sh +23 -0
  75. data/examples/stem.txt +12 -0
  76. data/examples/table-drift.txt +4 -0
  77. data/examples/temps.txt +31 -0
  78. data/examples/trig.sh +13 -0
  79. data/examples/trig.txt +102 -0
  80. data/examples/week.txt +8 -0
  81. data/examples/wide.txt +2 -0
  82. data/exe/tessella +6 -0
  83. data/lib/tessella/axis.rb +71 -0
  84. data/lib/tessella/bitfield.rb +194 -0
  85. data/lib/tessella/block.rb +56 -0
  86. data/lib/tessella/borders.rb +85 -0
  87. data/lib/tessella/canvas/area.rb +155 -0
  88. data/lib/tessella/canvas/ascii.rb +120 -0
  89. data/lib/tessella/canvas/block.rb +40 -0
  90. data/lib/tessella/canvas/braille.rb +30 -0
  91. data/lib/tessella/canvas/clip.rb +49 -0
  92. data/lib/tessella/canvas/density.rb +51 -0
  93. data/lib/tessella/canvas/dot.rb +27 -0
  94. data/lib/tessella/canvas/eighths.rb +43 -0
  95. data/lib/tessella/canvas/half.rb +51 -0
  96. data/lib/tessella/canvas/sextant.rb +36 -0
  97. data/lib/tessella/canvas.rb +168 -0
  98. data/lib/tessella/cell.rb +10 -0
  99. data/lib/tessella/chart.rb +211 -0
  100. data/lib/tessella/check.rb +52 -0
  101. data/lib/tessella/cli/bitfield.rb +121 -0
  102. data/lib/tessella/cli/chart.rb +615 -0
  103. data/lib/tessella/cli/chart_candle.rb +36 -0
  104. data/lib/tessella/cli/chart_heatmap.rb +21 -0
  105. data/lib/tessella/cli/chart_help.rb +85 -0
  106. data/lib/tessella/cli/chart_pie.rb +38 -0
  107. data/lib/tessella/cli/compose.rb +156 -0
  108. data/lib/tessella/cli/compose_options.rb +259 -0
  109. data/lib/tessella/cli/glyphs.rb +97 -0
  110. data/lib/tessella/cli/image.rb +250 -0
  111. data/lib/tessella/cli/input.rb +44 -0
  112. data/lib/tessella/cli/iso_time.rb +28 -0
  113. data/lib/tessella/cli/lint.rb +102 -0
  114. data/lib/tessella/cli/lint_options.rb +99 -0
  115. data/lib/tessella/cli/main.rb +121 -0
  116. data/lib/tessella/cli/meter.rb +80 -0
  117. data/lib/tessella/cli/rule.rb +57 -0
  118. data/lib/tessella/cli/seq.rb +117 -0
  119. data/lib/tessella/cli/sparkline.rb +72 -0
  120. data/lib/tessella/cli/table.rb +167 -0
  121. data/lib/tessella/cli/text.rb +78 -0
  122. data/lib/tessella/cli/tree.rb +100 -0
  123. data/lib/tessella/cli/wave.rb +148 -0
  124. data/lib/tessella/cli.rb +36 -0
  125. data/lib/tessella/color.rb +106 -0
  126. data/lib/tessella/compose.rb +243 -0
  127. data/lib/tessella/compose_fill.rb +176 -0
  128. data/lib/tessella/compose_grid.rb +36 -0
  129. data/lib/tessella/compose_shadow.rb +41 -0
  130. data/lib/tessella/error.rb +13 -0
  131. data/lib/tessella/frame.rb +170 -0
  132. data/lib/tessella/glyphs.rb +106 -0
  133. data/lib/tessella/image.rb +454 -0
  134. data/lib/tessella/image_octant.rb +46 -0
  135. data/lib/tessella/image_petscii.rb +116 -0
  136. data/lib/tessella/kinds/bar.rb +344 -0
  137. data/lib/tessella/kinds/box.rb +213 -0
  138. data/lib/tessella/kinds/box_upright.rb +110 -0
  139. data/lib/tessella/kinds/candlestick.rb +164 -0
  140. data/lib/tessella/kinds/columns.rb +259 -0
  141. data/lib/tessella/kinds/date_axis.rb +102 -0
  142. data/lib/tessella/kinds/error_bars.rb +71 -0
  143. data/lib/tessella/kinds/heatmap.rb +130 -0
  144. data/lib/tessella/kinds/line.rb +173 -0
  145. data/lib/tessella/kinds/multibar.rb +98 -0
  146. data/lib/tessella/kinds/multibar_columns.rb +130 -0
  147. data/lib/tessella/kinds/multibar_fine.rb +45 -0
  148. data/lib/tessella/kinds/multibar_rows.rb +93 -0
  149. data/lib/tessella/kinds/pie.rb +123 -0
  150. data/lib/tessella/kinds/pie_rim.rb +78 -0
  151. data/lib/tessella/kinds/sideways.rb +62 -0
  152. data/lib/tessella/kinds/stem.rb +134 -0
  153. data/lib/tessella/kinds/threshold.rb +73 -0
  154. data/lib/tessella/legend.rb +31 -0
  155. data/lib/tessella/lint/alphabet.rb +56 -0
  156. data/lib/tessella/lint/drift.rb +82 -0
  157. data/lib/tessella/lint/fences.rb +51 -0
  158. data/lib/tessella/lint/geometry.rb +120 -0
  159. data/lib/tessella/lint/geometry_checks.rb +224 -0
  160. data/lib/tessella/lint/lines.rb +115 -0
  161. data/lib/tessella/lint/mixing.rb +61 -0
  162. data/lib/tessella/lint/runner.rb +73 -0
  163. data/lib/tessella/lint/shadows.rb +120 -0
  164. data/lib/tessella/lint/target_rows.rb +66 -0
  165. data/lib/tessella/lint.rb +158 -0
  166. data/lib/tessella/meter.rb +133 -0
  167. data/lib/tessella/meter_upright.rb +43 -0
  168. data/lib/tessella/png.rb +456 -0
  169. data/lib/tessella/raster.rb +92 -0
  170. data/lib/tessella/rmagick_reader.rb +52 -0
  171. data/lib/tessella/rule.rb +59 -0
  172. data/lib/tessella/seq.rb +522 -0
  173. data/lib/tessella/sparkline.rb +57 -0
  174. data/lib/tessella/stats.rb +199 -0
  175. data/lib/tessella/table.rb +547 -0
  176. data/lib/tessella/table_cells.rb +225 -0
  177. data/lib/tessella/target.rb +105 -0
  178. data/lib/tessella/text.rb +76 -0
  179. data/lib/tessella/text_forms.rb +150 -0
  180. data/lib/tessella/tree.rb +221 -0
  181. data/lib/tessella/version.rb +5 -0
  182. data/lib/tessella/wave.rb +353 -0
  183. data/lib/tessella/width.rb +122 -0
  184. data/lib/tessella/width_class.rb +77 -0
  185. data/lib/tessella/width_invisible.rb +66 -0
  186. data/lib/tessella/width_invisible_high.rb +60 -0
  187. data/lib/tessella.rb +94 -0
  188. metadata +226 -0
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tessella
4
+ # Marked series (add's mark:): one glyph in the cell of each point,
5
+ # in place of the kind's own drawing. Kept apart from Chart so
6
+ # Chart's own class body stays under budget.
7
+ module Marks
8
+ private
9
+
10
+ def check_mark!(mark)
11
+ return if mark.nil?
12
+ unless mark.is_a?(String) && mark.length == 1
13
+ raise ArgumentError,
14
+ "mark must be one character: #{mark.inspect}"
15
+ end
16
+ return unless @ascii && !mark.ascii_only?
17
+
18
+ raise ArgumentError,
19
+ "mark must be ASCII under ascii: true: #{mark.inspect}"
20
+ end
21
+
22
+ def draw_or_mark(canvas, series)
23
+ return draw_series(canvas, series) unless series.mark
24
+
25
+ series.x.zip(series.y).each do |x, y|
26
+ canvas.mark(x, y, series.mark, color: series.color)
27
+ end
28
+ end
29
+
30
+ # A marked series shows its glyph before its name.
31
+ def legend(series) = [series.mark, series.name].compact.join(" ")
32
+ end
33
+
34
+ # What a chart refuses before it draws (DESIGN.md, "Chart and
35
+ # kinds"): unknown options, a width or height that is not a
36
+ # positive Integer, and a series whose x and y differ in length or
37
+ # hold a value an axis cannot place. Kept apart from Chart so
38
+ # Chart's own class body stays under budget.
39
+ module ChartChecks
40
+ private
41
+
42
+ def check_options!(opts)
43
+ unknown = opts.keys - self.class::OPTIONS
44
+ return check_sizes!(opts) if unknown.empty?
45
+
46
+ raise ArgumentError,
47
+ "unknown option: #{unknown.map(&:inspect).join(", ")}"
48
+ end
49
+
50
+ def check_sizes!(opts)
51
+ %i[width height].each do |key|
52
+ Check.positive_integer!(opts[key], key.to_s) if opts.key?(key)
53
+ end
54
+ end
55
+
56
+ # x (nil for 1..n) and y as stored: each through its scale,
57
+ # both finite, and the same length.
58
+ def series_values(x, y)
59
+ ys = Check.finite!(Array(y), "y")
60
+ xs = Array(x || (1..ys.length))
61
+ Check.same_size!(xs, ys, %w[x y])
62
+ [Check.finite!(xs.map(&@xscale), scaled_name(:x)),
63
+ Check.finite!(ys.map(&@yscale), scaled_name(:y))]
64
+ end
65
+
66
+ def scaled_name(axis)
67
+ scale = @opts[:"#{axis}scale"]
68
+ scale ? "#{axis} under #{axis}scale #{scale}" : axis.to_s
69
+ end
70
+ end
71
+
72
+ # Shared machinery behind every chart kind (DESIGN.md, "Chart and
73
+ # kinds"): option handling and defaults, series bookkeeping, colour
74
+ # cycling over Color::NAMES, canvas and frame assembly, and
75
+ # to_s / render. A subclass implements draw_series(canvas, series)
76
+ # and may override default_canvas_type, canvas_type, default_border,
77
+ # draw_grid? or add itself (Stairs reshapes its points there).
78
+ class Chart
79
+ include ChartChecks
80
+ include Marks
81
+
82
+ DEFAULT_WIDTH = 40
83
+ DEFAULT_HEIGHT = 15
84
+
85
+ # Canvases a chart reaches only through another's name.
86
+ WIDE = %i[half_wide eighths_wide].freeze
87
+ OPTIONS = %i[title xlabel ylabel width height xlim ylim canvas
88
+ ascii color border xscale].freeze
89
+
90
+ Series = Data.define(:x, :y, :name, :color, :mark)
91
+
92
+ def initialize(**opts)
93
+ check_options!(opts)
94
+ @opts = opts
95
+ @ascii = opts.fetch(:ascii) { Target.ascii? }
96
+ @color_on = opts.fetch(:color, false)
97
+ @border = opts[:border] || default_border
98
+ @xscale = Stats.transform(opts[:xscale])
99
+ @yscale = Stats.transform(opts[:yscale])
100
+ @series = []
101
+ @next_color = 0
102
+ end
103
+
104
+ # mark: a one-character glyph drawn in the cell of each point,
105
+ # instead of the kind's own drawing of the series.
106
+ def add(*args, name: nil, color: nil, mark: nil)
107
+ check_mark!(mark)
108
+ x, y = args.length == 1 ? [nil, *args] : args
109
+ xs, ys = series_values(x, y)
110
+ @series << Series.new(x: xs, y: ys, name: name,
111
+ color: color || auto_color, mark: mark)
112
+ self
113
+ end
114
+
115
+ def to_s
116
+ text = frame.to_s
117
+ return text unless @ascii
118
+
119
+ text.each_line.with_index(1) do |line, n|
120
+ Check.ascii!(line.chomp, "chart line #{n}")
121
+ end
122
+ text
123
+ end
124
+
125
+ def render(io = $stdout) = io.print(to_s)
126
+
127
+ private
128
+
129
+ def default_border = :box
130
+ def draw_grid? = true
131
+ def default_canvas_type = @ascii ? :ascii : Target.canvas
132
+
133
+ def auto_color
134
+ name = Color::NAMES[@next_color % Color::NAMES.size]
135
+ @next_color += 1
136
+ name
137
+ end
138
+
139
+ def canvas_type
140
+ type = @opts[:canvas] || default_canvas_type
141
+ return eighths_type if type == :eighths
142
+ return type if Canvas.types.key?(type) && !WIDE.include?(type)
143
+
144
+ raise ArgumentError, "unknown canvas: #{type.inspect}"
145
+ end
146
+
147
+ # The eighths canvas only fills (Canvas::Eighths): an area, up
148
+ # its columns or, sideways, along its rows -- not a threshold's
149
+ # dashes, which would read as bars.
150
+ def eighths_type
151
+ raise ArgumentError, "canvas: :eighths needs area: true" unless
152
+ fills?
153
+ raise ArgumentError, "canvas: :eighths takes no threshold:" if
154
+ @opts[:threshold]
155
+
156
+ @sideways ? :eighths_wide : :eighths
157
+ end
158
+
159
+ def fills? = false
160
+
161
+ def width = @opts.fetch(:width, DEFAULT_WIDTH)
162
+ def height = @opts.fetch(:height, DEFAULT_HEIGHT)
163
+ def xlim = Axis.limits(@series.flat_map(&:x), @opts[:xlim])
164
+ def ylim = Axis.limits(@series.flat_map(&:y), @opts[:ylim])
165
+
166
+ def canvas_extra
167
+ canvas_type == :density ? { ascii: @ascii } : {}
168
+ end
169
+
170
+ def build_canvas(xl, yl)
171
+ opts = { width: width, height: height, xlim: xl, ylim: yl,
172
+ **canvas_extra }
173
+ canvas = Canvas.build(canvas_type, **opts)
174
+ draw_axes(canvas, xl, yl)
175
+ @series.each { draw_or_mark(canvas, it) }
176
+ canvas
177
+ end
178
+
179
+ def draw_axes(canvas, xl, yl)
180
+ return unless draw_grid?
181
+
182
+ canvas.line(xl.first, 0, xl.last, 0) if
183
+ yl.first.negative? && yl.last.positive?
184
+ canvas.line(0, yl.first, 0, yl.last) if
185
+ xl.first.negative? && xl.last.positive?
186
+ end
187
+
188
+ def frame
189
+ xl = xlim
190
+ yl = ylim
191
+ Frame.new(**frame_options(build_canvas(xl, yl), xl, yl))
192
+ end
193
+
194
+ def frame_options(canvas, xl, yl)
195
+ { body: canvas.rows, border: @border, ascii: @ascii,
196
+ color: @color_on, title: @opts[:title],
197
+ xlabel: @opts[:xlabel], ylabel: @opts[:ylabel],
198
+ left: left_labels(yl), right: right_labels,
199
+ bottom: [Axis.tick(xl.first), Axis.tick(xl.last)] }
200
+ end
201
+
202
+ def left_labels(yl)
203
+ { 0 => Axis.tick(yl.last), (height - 1) => Axis.tick(yl.first) }
204
+ end
205
+
206
+ def right_labels
207
+ named = @series.reject { |s| s.name.to_s.empty? }
208
+ named.each_with_index.to_h { |s, i| [i, legend(s)] }
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tessella
4
+ # The argument checks every kind shares: each raises ArgumentError
5
+ # naming what it refused, before anything is drawn.
6
+ module Check
7
+ def self.positive_integer!(value, name)
8
+ return value if value.is_a?(Integer) && value.positive?
9
+
10
+ raise ArgumentError,
11
+ "#{name} must be a positive Integer: #{value.inspect}"
12
+ end
13
+
14
+ def self.positive!(value, name)
15
+ return value if value.is_a?(Numeric) && value.positive?
16
+
17
+ raise ArgumentError, "#{name} must be positive: #{value.inspect}"
18
+ end
19
+
20
+ # Only a finite number can be placed on an axis: nil, a String,
21
+ # NaN or +-Infinity is refused, and what produced it (data, or
22
+ # an xscale over a value <= 0) is named.
23
+ def self.finite!(values, name)
24
+ i = values.index { !finite_number?(it) }
25
+ return values unless i
26
+
27
+ raise ValueError,
28
+ "#{name} must be a finite number: #{values[i].inspect}"
29
+ end
30
+
31
+ def self.finite_number?(value)
32
+ value.is_a?(Numeric) && (!value.is_a?(Float) || value.finite?)
33
+ end
34
+
35
+ # Under ascii: true every text drawn into a figure is ASCII
36
+ # too: one that is not is refused, naming where it came from.
37
+ def self.ascii!(text, where)
38
+ return text if text.nil? || text.to_s.ascii_only? ||
39
+ Target.text_free?
40
+
41
+ raise ArgumentError, "#{where} is non-ASCII under ascii: " \
42
+ "true: #{text.to_s.inspect}"
43
+ end
44
+
45
+ def self.same_size!(a, b, names)
46
+ return if a.size == b.size
47
+
48
+ raise ArgumentError, "#{names[0]} and #{names[1]} differ in " \
49
+ "length: #{a.size} and #{b.size}"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tessella
4
+ # `tessella bitfield` (ported byte for byte from the tessella
5
+ # skill's scripts/bitfield.rb, minus its --no-lint: the lint pass
6
+ # stays in the skill). Exit 0 drawn, 1 bad input (a missing file, a
7
+ # bad line, a name too wide for its cell, output past --max-width),
8
+ # 2 usage.
9
+ class BitfieldCLI
10
+ class UsageError < StandardError; end
11
+ class DataError < StandardError; end
12
+
13
+ USAGE = <<~TEXT
14
+ Usage: tessella bitfield [options] [file] (stdin if none)
15
+ --word N bits per row: 8, 16, 32 (default), 64
16
+ --octets byte-oriented: no bit ruler, a tick
17
+ per octet
18
+ --no-ruler drop the two-row bit ruler
19
+ --abbrev truncate a name too wide for its cell
20
+ --ascii accepted; this command always draws
21
+ ASCII
22
+ --max-width N error if a line exceeds N columns
23
+ TEXT
24
+
25
+ def self.run(argv, stdin:, stdout:, stderr:)
26
+ return help(stdout) if %w[--help -h].include?(argv.first)
27
+
28
+ stdout.print(draw(argv.dup, stdin))
29
+ 0
30
+ rescue UsageError, OptionParser::ParseError => e
31
+ fail_with(stderr, e.message, 2)
32
+ rescue DataError => e
33
+ fail_with(stderr, e.message, 1)
34
+ end
35
+
36
+ def self.help(stdout)
37
+ stdout.print(USAGE)
38
+ 0
39
+ end
40
+
41
+ # No usage dump after the message (unlike compose's CLI): the
42
+ # parity gate requires bitfield's stderr to match the script's
43
+ # own, byte for byte but for the prefix.
44
+ def self.fail_with(stderr, message, status)
45
+ stderr.puts("tessella bitfield: #{message}")
46
+ status
47
+ end
48
+
49
+ def self.draw(argv, stdin)
50
+ opts, paths = BitfieldOptions.parse(argv)
51
+ raise UsageError, "too many arguments" if paths.size > 1
52
+
53
+ text = CLI.read_text(paths.first, stdin, DataError)
54
+ finish(text, opts)
55
+ end
56
+
57
+ # Usage is settled by now; what drawing still refuses (a bad
58
+ # line, a name too wide, output past --max-width) is bad input.
59
+ def self.finish(text, opts)
60
+ block = Tessella.bitfield(text, word: opts.fetch(:word, 32),
61
+ octets: opts[:octets] || false,
62
+ ruler: !opts[:no_ruler],
63
+ abbrev: opts[:abbrev] || false)
64
+ check_max_width!(block, opts[:max_width])
65
+ block.to_s
66
+ rescue ArgumentError => e
67
+ raise DataError, e.message
68
+ end
69
+
70
+ def self.check_max_width!(block, max_width)
71
+ return unless max_width
72
+
73
+ block.lines.each do |line|
74
+ width = Width.of(line)
75
+ next if width <= max_width
76
+
77
+ raise DataError, "a line is #{width} columns, over " \
78
+ "--max-width #{max_width}"
79
+ end
80
+ end
81
+ end
82
+
83
+ # bitfield's options: --word validated immediately (a usage error,
84
+ # as the script's own OptionParser block), the rest stored as-is.
85
+ class BitfieldOptions
86
+ def self.parse(argv)
87
+ new.parse(argv)
88
+ end
89
+
90
+ def initialize
91
+ @opts = {}
92
+ @parser = OptionParser.new { |op| add(op) }
93
+ end
94
+
95
+ def parse(argv)
96
+ @parser.parse!(argv)
97
+ [@opts, argv]
98
+ end
99
+
100
+ private
101
+
102
+ def add(op)
103
+ add_word(op)
104
+ op.on("--octets") { @opts[:octets] = true }
105
+ op.on("--no-ruler") { @opts[:no_ruler] = true }
106
+ op.on("--abbrev") { @opts[:abbrev] = true }
107
+ op.on("--ascii") { @opts[:ascii] = true }
108
+ op.on("--max-width N", Integer) { |n| @opts[:max_width] = n }
109
+ end
110
+
111
+ def add_word(op)
112
+ op.on("--word N", Integer) do |n|
113
+ unless Bitfield::WORD_SIZES.include?(n)
114
+ raise BitfieldCLI::UsageError,
115
+ "--word must be 8, 16, 32 or 64"
116
+ end
117
+ @opts[:word] = n
118
+ end
119
+ end
120
+ end
121
+ end