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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +900 -0
- data/doc/README.md +74 -0
- data/doc/bitfield.md +123 -0
- data/doc/charts.md +1370 -0
- data/doc/check.rb +75 -0
- data/doc/cli.md +483 -0
- data/doc/composition.md +599 -0
- data/doc/getting-started.md +121 -0
- data/doc/glyphs.md +134 -0
- data/doc/images.md +325 -0
- data/doc/library.md +239 -0
- data/doc/lint.md +270 -0
- data/doc/meter.md +210 -0
- data/doc/rule.md +51 -0
- data/doc/seq.md +169 -0
- data/doc/sparkline.md +63 -0
- data/doc/table.md +347 -0
- data/doc/targets.md +222 -0
- data/doc/text.md +84 -0
- data/doc/tree.md +108 -0
- data/doc/wave.md +122 -0
- data/examples/README.md +103 -0
- data/examples/ages.txt +9 -0
- data/examples/bad.csv +3 -0
- data/examples/boxes.txt +3 -0
- data/examples/broken.txt +4 -0
- data/examples/calibration.rb +12 -0
- data/examples/cities.csv +4 -0
- data/examples/cjk.rb +13 -0
- data/examples/comment.rb +9 -0
- data/examples/comment.sh +13 -0
- data/examples/cpu.txt +60 -0
- data/examples/dashboard.sh +25 -0
- data/examples/datasheet.rb +16 -0
- data/examples/disc.pgm +0 -0
- data/examples/disks.sh +18 -0
- data/examples/disks.txt +5 -0
- data/examples/dose.txt +7 -0
- data/examples/drift.txt +4 -0
- data/examples/ethernet.txt +3 -0
- data/examples/figure.c +5 -0
- data/examples/fit.rb +16 -0
- data/examples/floors.rb +21 -0
- data/examples/gallery.sh +20 -0
- data/examples/gauges.sh +31 -0
- data/examples/gradient.rb +16 -0
- data/examples/growth.txt +6 -0
- data/examples/hosts.csv +4 -0
- data/examples/icmp.txt +3 -0
- data/examples/ipv4.txt +12 -0
- data/examples/kinds.csv +4 -0
- data/examples/langs.csv +4 -0
- data/examples/lint.rb +16 -0
- data/examples/load.rb +14 -0
- data/examples/login.txt +10 -0
- data/examples/neg.csv +4 -0
- data/examples/normal.txt +200 -0
- data/examples/note.txt +2 -0
- data/examples/notes-fixed.md +17 -0
- data/examples/notes.md +17 -0
- data/examples/ohlc.txt +7 -0
- data/examples/os.csv +10 -0
- data/examples/outline.txt +8 -0
- data/examples/plot.txt +5 -0
- data/examples/refusals.rb +17 -0
- data/examples/report.sh +21 -0
- data/examples/rfc.sh +22 -0
- data/examples/samples.txt +7 -0
- data/examples/shapes.pgm +0 -0
- data/examples/sizes.tsv +3 -0
- data/examples/spi.txt +6 -0
- data/examples/status.sh +23 -0
- data/examples/stem.txt +12 -0
- data/examples/table-drift.txt +4 -0
- data/examples/temps.txt +31 -0
- data/examples/trig.sh +13 -0
- data/examples/trig.txt +102 -0
- data/examples/week.txt +8 -0
- data/examples/wide.txt +2 -0
- data/exe/tessella +6 -0
- data/lib/tessella/axis.rb +71 -0
- data/lib/tessella/bitfield.rb +194 -0
- data/lib/tessella/block.rb +56 -0
- data/lib/tessella/borders.rb +85 -0
- data/lib/tessella/canvas/area.rb +155 -0
- data/lib/tessella/canvas/ascii.rb +120 -0
- data/lib/tessella/canvas/block.rb +40 -0
- data/lib/tessella/canvas/braille.rb +30 -0
- data/lib/tessella/canvas/clip.rb +49 -0
- data/lib/tessella/canvas/density.rb +51 -0
- data/lib/tessella/canvas/dot.rb +27 -0
- data/lib/tessella/canvas/eighths.rb +43 -0
- data/lib/tessella/canvas/half.rb +51 -0
- data/lib/tessella/canvas/sextant.rb +36 -0
- data/lib/tessella/canvas.rb +168 -0
- data/lib/tessella/cell.rb +10 -0
- data/lib/tessella/chart.rb +211 -0
- data/lib/tessella/check.rb +52 -0
- data/lib/tessella/cli/bitfield.rb +121 -0
- data/lib/tessella/cli/chart.rb +615 -0
- data/lib/tessella/cli/chart_candle.rb +36 -0
- data/lib/tessella/cli/chart_heatmap.rb +21 -0
- data/lib/tessella/cli/chart_help.rb +85 -0
- data/lib/tessella/cli/chart_pie.rb +38 -0
- data/lib/tessella/cli/compose.rb +156 -0
- data/lib/tessella/cli/compose_options.rb +259 -0
- data/lib/tessella/cli/glyphs.rb +97 -0
- data/lib/tessella/cli/image.rb +250 -0
- data/lib/tessella/cli/input.rb +44 -0
- data/lib/tessella/cli/iso_time.rb +28 -0
- data/lib/tessella/cli/lint.rb +102 -0
- data/lib/tessella/cli/lint_options.rb +99 -0
- data/lib/tessella/cli/main.rb +121 -0
- data/lib/tessella/cli/meter.rb +80 -0
- data/lib/tessella/cli/rule.rb +57 -0
- data/lib/tessella/cli/seq.rb +117 -0
- data/lib/tessella/cli/sparkline.rb +72 -0
- data/lib/tessella/cli/table.rb +167 -0
- data/lib/tessella/cli/text.rb +78 -0
- data/lib/tessella/cli/tree.rb +100 -0
- data/lib/tessella/cli/wave.rb +148 -0
- data/lib/tessella/cli.rb +36 -0
- data/lib/tessella/color.rb +106 -0
- data/lib/tessella/compose.rb +243 -0
- data/lib/tessella/compose_fill.rb +176 -0
- data/lib/tessella/compose_grid.rb +36 -0
- data/lib/tessella/compose_shadow.rb +41 -0
- data/lib/tessella/error.rb +13 -0
- data/lib/tessella/frame.rb +170 -0
- data/lib/tessella/glyphs.rb +106 -0
- data/lib/tessella/image.rb +454 -0
- data/lib/tessella/image_octant.rb +46 -0
- data/lib/tessella/image_petscii.rb +116 -0
- data/lib/tessella/kinds/bar.rb +344 -0
- data/lib/tessella/kinds/box.rb +213 -0
- data/lib/tessella/kinds/box_upright.rb +110 -0
- data/lib/tessella/kinds/candlestick.rb +164 -0
- data/lib/tessella/kinds/columns.rb +259 -0
- data/lib/tessella/kinds/date_axis.rb +102 -0
- data/lib/tessella/kinds/error_bars.rb +71 -0
- data/lib/tessella/kinds/heatmap.rb +130 -0
- data/lib/tessella/kinds/line.rb +173 -0
- data/lib/tessella/kinds/multibar.rb +98 -0
- data/lib/tessella/kinds/multibar_columns.rb +130 -0
- data/lib/tessella/kinds/multibar_fine.rb +45 -0
- data/lib/tessella/kinds/multibar_rows.rb +93 -0
- data/lib/tessella/kinds/pie.rb +123 -0
- data/lib/tessella/kinds/pie_rim.rb +78 -0
- data/lib/tessella/kinds/sideways.rb +62 -0
- data/lib/tessella/kinds/stem.rb +134 -0
- data/lib/tessella/kinds/threshold.rb +73 -0
- data/lib/tessella/legend.rb +31 -0
- data/lib/tessella/lint/alphabet.rb +56 -0
- data/lib/tessella/lint/drift.rb +82 -0
- data/lib/tessella/lint/fences.rb +51 -0
- data/lib/tessella/lint/geometry.rb +120 -0
- data/lib/tessella/lint/geometry_checks.rb +224 -0
- data/lib/tessella/lint/lines.rb +115 -0
- data/lib/tessella/lint/mixing.rb +61 -0
- data/lib/tessella/lint/runner.rb +73 -0
- data/lib/tessella/lint/shadows.rb +120 -0
- data/lib/tessella/lint/target_rows.rb +66 -0
- data/lib/tessella/lint.rb +158 -0
- data/lib/tessella/meter.rb +133 -0
- data/lib/tessella/meter_upright.rb +43 -0
- data/lib/tessella/png.rb +456 -0
- data/lib/tessella/raster.rb +92 -0
- data/lib/tessella/rmagick_reader.rb +52 -0
- data/lib/tessella/rule.rb +59 -0
- data/lib/tessella/seq.rb +522 -0
- data/lib/tessella/sparkline.rb +57 -0
- data/lib/tessella/stats.rb +199 -0
- data/lib/tessella/table.rb +547 -0
- data/lib/tessella/table_cells.rb +225 -0
- data/lib/tessella/target.rb +105 -0
- data/lib/tessella/text.rb +76 -0
- data/lib/tessella/text_forms.rb +150 -0
- data/lib/tessella/tree.rb +221 -0
- data/lib/tessella/version.rb +5 -0
- data/lib/tessella/wave.rb +353 -0
- data/lib/tessella/width.rb +122 -0
- data/lib/tessella/width_class.rb +77 -0
- data/lib/tessella/width_invisible.rb +66 -0
- data/lib/tessella/width_invisible_high.rb +60 -0
- data/lib/tessella.rb +94 -0
- 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
|