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,599 @@
1
+ # Composition
2
+
3
+ Every figure tessella draws can become a **block**: lines of text,
4
+ measured in display columns. Composition takes blocks (or anything
5
+ that can become one) and returns a new block, so figures nest: a
6
+ chart beside a table, framed, captioned, indented into a comment.
7
+
8
+ ```ruby
9
+ # frozen_string_literal: true
10
+
11
+ # examples/load.rb: a bar chart and a table side by side, framed and
12
+ # captioned -- composition of any two figures.
13
+ require "tessella"
14
+
15
+ rows = Tessella::Table.parse(File.read("langs.csv"))
16
+ labels = rows.drop(1).map(&:first)
17
+ values = rows.drop(1).map { Integer(it[1], exception: false) || it[1].to_f }
18
+ plot = Tessella.bar(labels, values, width: 20)
19
+ table = Tessella.table(rows, style: :rounded)
20
+ fig = Tessella.beside(plot, table, gap: 2, align: :middle)
21
+ puts Tessella.caption(Tessella.frame(fig, title: "Load"),
22
+ text: "Figure 1: lines of code, in thousands")
23
+ ```
24
+
25
+ ```text
26
+ $ ruby load.rb
27
+ ┌───────────────────── Load ─────────────────────┐
28
+ │ ╭──────┬─────╮ │
29
+ │ ┌ ┐ │ lang │ loc │ │
30
+ │ ruby ┤██████▍········· 12 ├──────┼─────┤ │
31
+ │ c ┤████████████████ 30 │ ruby │ 12 │ │
32
+ │ rust ┤████············ 7.5 │ c │ 30 │ │
33
+ │ └ ┘ │ rust │ 7.5 │ │
34
+ │ ╰──────┴─────╯ │
35
+ └────────────────────────────────────────────────┘
36
+
37
+ Figure 1: lines of code, in thousands
38
+ ```
39
+
40
+ ## Blocks
41
+
42
+ `Tessella::Block` is a finished figure:
43
+
44
+ ```ruby
45
+ block.lines # => Array of String, no trailing blank on any
46
+ block.width # => display columns of the widest line
47
+ block.height # => number of lines
48
+ block.to_s # => the lines joined, "\n"-terminated
49
+ block.check_width!(78) # ArgumentError naming the line, if wider
50
+ ```
51
+
52
+ Every composition function accepts a figure in any form
53
+ `Block.from` takes: a `Block`, a `String` of lines, or a chart (any
54
+ object whose `to_s` is the figure). The diagram factories
55
+ (`Tessella.tree`, `.table`, `.bitfield`, `.wave`, `.seq`, `.image`)
56
+ already return blocks.
57
+
58
+ Three rules hold for every block:
59
+
60
+ - **Width is display width.** A CJK character counts two columns, a
61
+ combining mark none, an ANSI colour code none, so a coloured or
62
+ CJK-labelled figure lines up by what it shows.
63
+ - **A block is never rewritten, only padded** on the right, so its
64
+ own alignment survives every composition. `stack`'s `center:` is
65
+ the one shift, and it moves a whole block at once.
66
+ - **What has no width of its own is refused.** A tab (its width
67
+ belongs to the viewer) and an empty block raise `ArgumentError`;
68
+ trailing blank lines are dropped.
69
+
70
+ ## frame
71
+
72
+ ```ruby
73
+ Tessella.frame(fig, title: nil, style: nil, pad: 1, fill: nil,
74
+ ascii: false)
75
+ ```
76
+
77
+ Draws a border around one figure, with an optional title centred in
78
+ its top edge. `pad:` is the blank columns on each side inside the
79
+ border; the frame hugs its content vertically.
80
+
81
+ ```text
82
+ $ tessella compose frame --title Notes --style rounded note.txt
83
+ ╭───── Notes ─────╮
84
+ │ ruby is 12 kLOC │
85
+ │ c is the bulk │
86
+ ╰─────────────────╯
87
+ ```
88
+
89
+ ```text
90
+ $ tessella compose frame --pad 0 note.txt
91
+ ┌───────────────┐
92
+ │ruby is 12 kLOC│
93
+ │c is the bulk │
94
+ └───────────────┘
95
+ ```
96
+
97
+ The styles are `:light` (the default), `:heavy`, `:double`,
98
+ `:rounded`, the four dashed ones `:dashed` (`┄┆`), `:dashed_fine`
99
+ (`┈┊`), `:heavy_dashed` (`┅┇`) and `:heavy_dashed_fine` (`┉┋`), the
100
+ two mixed doubles `:double_h` (double horizontals, `╒═╕`) and
101
+ `:double_v` (double verticals, `╓─╖`), and `:ascii`. A name may say
102
+ `-` for `_`, and `tessella glyphs border` lists them all with their
103
+ glyphs ([glyph sets](glyphs.md)). Four of them:
104
+
105
+ ```text
106
+ $ tessella compose frame --style heavy note.txt && \
107
+ tessella compose frame --style double note.txt
108
+ ┏━━━━━━━━━━━━━━━━━┓
109
+ ┃ ruby is 12 kLOC ┃
110
+ ┃ c is the bulk ┃
111
+ ┗━━━━━━━━━━━━━━━━━┛
112
+ ╔═════════════════╗
113
+ ║ ruby is 12 kLOC ║
114
+ ║ c is the bulk ║
115
+ ╚═════════════════╝
116
+ ```
117
+
118
+ ```text
119
+ $ tessella compose frame --style dashed note.txt && \
120
+ tessella compose frame --style double-h note.txt
121
+ ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
122
+ ┆ ruby is 12 kLOC ┆
123
+ ┆ c is the bulk ┆
124
+ └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
125
+ ╒═════════════════╕
126
+ │ ruby is 12 kLOC │
127
+ │ c is the bulk │
128
+ ╘═════════════════╛
129
+ ```
130
+
131
+ ### Custom borders
132
+
133
+ `style:` also takes a set of your own: a String of six glyphs in the
134
+ order `tl tr bl br h v` (top-left, top-right, bottom-left,
135
+ bottom-right, horizontal, vertical), or a
136
+ `Tessella::Borders::Set.new(tl, tr, bl, br, h, v)`. A String that
137
+ names a style is that style. Each glyph must take one column.
138
+
139
+ ```text
140
+ $ tessella compose frame --style '╓╖╙╜─║' --title Notes note.txt
141
+ ╓───── Notes ─────╖
142
+ ║ ruby is 12 kLOC ║
143
+ ║ c is the bulk ║
144
+ ╙─────────────────╜
145
+ ```
146
+
147
+ Under `ascii: true` a custom set is drawn if every glyph is ASCII, and
148
+ refused otherwise: it is never quietly replaced by `ascii`.
149
+
150
+ ```text
151
+ $ tessella compose frame --ascii --style '****=!' note.txt
152
+ *=================*
153
+ ! ruby is 12 kLOC !
154
+ ! c is the bulk !
155
+ *=================*
156
+ ```
157
+
158
+ <!-- tessella: allow-mixed -->
159
+ ```text
160
+ $ tessella compose frame --ascii --style '╓╖╙╜─║' note.txt
161
+ tessella compose: border glyph ╓ conflicts with ascii: true
162
+ Usage: tessella compose frame [--title T] [--style S] [--pad N]
163
+ [--fill P] [--halo H] BLOCK
164
+ tessella compose shadow [--glyph G] BLOCK
165
+ tessella compose fill [--pattern P] [--halo H] BLOCK
166
+ tessella compose grid [--columns N] [--gap N] [--row-gap N]
167
+ [--align A] BLOCK...
168
+ tessella compose beside [--gap N] [--align A] BLOCK...
169
+ tessella compose stack [--gap N] [--rule C] [--center] BLOCK...
170
+ tessella compose indent --prefix STR [--suffix STR] BLOCK
171
+ tessella compose caption --text T [--above] BLOCK
172
+ Every subcommand also takes --[no-]ascii and --max-width N.
173
+ ```
174
+
175
+ A figure framed in a custom set can still be checked by
176
+ [`tessella lint --geometry`](lint.md#geometry), given the same
177
+ glyphs with `--border`.
178
+
179
+ ## beside
180
+
181
+ ```ruby
182
+ Tessella.beside(fig, fig, ..., gap: 1, align: :top, ascii: false)
183
+ ```
184
+
185
+ Sets two or more figures side by side, `gap:` columns apart. A
186
+ shorter figure is placed by `align:`: `:top`, `:middle` or `:bottom`.
187
+ `plot.txt` is the output of `tessella chart --kind bar --width 20
188
+ langs.csv`.
189
+
190
+ ```text
191
+ $ tessella compose beside --gap 3 --align middle plot.txt note.txt
192
+ ┌ ┐
193
+ ruby ┤██████▍········· 12 ruby is 12 kLOC
194
+ c ┤████████████████ 30 c is the bulk
195
+ rust ┤████············ 7.5
196
+ └ ┘
197
+ ```
198
+
199
+ ## grid
200
+
201
+ ```ruby
202
+ Tessella.grid(fig, fig, ..., columns: 2, gap: 2, row_gap: 1,
203
+ align: :top, ascii: false)
204
+ ```
205
+
206
+ Lays figures out in rows of `columns:`, each row side by side and
207
+ the rows stacked, every column as wide as its widest figure: a
208
+ dashboard of charts, tables, meters and sparklines. A short last row
209
+ stays short. `gap:` is the blank columns between figures in a row,
210
+ `row_gap:` the blank rows between rows, and `align:` places a shorter
211
+ figure in its row, as `beside` does.
212
+
213
+ ```sh
214
+ #!/bin/sh
215
+ # examples/dashboard.sh -- a dashboard: a chart, a table, meters and a
216
+ # sparkline, each drawn to a file and laid out two to a row by
217
+ # `compose grid`, then framed.
218
+ # usage: sh dashboard.sh
219
+ set -eu
220
+
221
+ cd -- "$(dirname -- "$0")" || exit 1
222
+
223
+ tmpdir=
224
+ cleanup() { [ -z "$tmpdir" ] || rm -rf -- "$tmpdir"; }
225
+ trap cleanup EXIT INT HUP TERM
226
+ tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/dashboard.XXXXXX") || exit 1
227
+
228
+ tessella chart --kind bar --width 16 langs.csv > "$tmpdir/1"
229
+ tessella table --style rounded langs.csv > "$tmpdir/2"
230
+ {
231
+ tessella meter --width 12 --title cpu --title-width 4 0.62
232
+ tessella meter --width 12 --title disk --title-width 4 0.87
233
+ } > "$tmpdir/3"
234
+ tessella sparkline --width 20 < cpu.txt |
235
+ tessella compose caption --text 'cpu, last hour' - > "$tmpdir/4"
236
+ tessella compose grid --columns 2 --gap 3 --align middle \
237
+ "$tmpdir/1" "$tmpdir/2" "$tmpdir/3" "$tmpdir/4" |
238
+ tessella compose frame --title status -
239
+ ```
240
+
241
+ ```text
242
+ $ sh dashboard.sh
243
+ ┌──────────────────── status ─────────────────────┐
244
+ │ ╭──────┬─────╮ │
245
+ │ ┌ ┐ │ lang │ loc │ │
246
+ │ ruby ┤████▊······· 12 ├──────┼─────┤ │
247
+ │ c ┤████████████ 30 │ ruby │ 12 │ │
248
+ │ rust ┤███········· 7.5 │ c │ 30 │ │
249
+ │ └ ┘ │ rust │ 7.5 │ │
250
+ │ ╰──────┴─────╯ │
251
+ │ │
252
+ │ cpu [███████▌····] 62 % ▅▇██▇▆▄▃▂▁▁▃▄▆▇██▇▅▄ │
253
+ │ disk [██████████▌·] 87 % │
254
+ │ cpu, last hour │
255
+ └─────────────────────────────────────────────────┘
256
+ ```
257
+
258
+ ## stack
259
+
260
+ ```ruby
261
+ Tessella.stack(fig, fig, ..., gap: 0, rule: nil, center: false,
262
+ ascii: false)
263
+ ```
264
+
265
+ Sets two or more figures one under another. `gap:` is the number of
266
+ blank lines between two figures. `rule:` is one character, drawn as a
267
+ line as wide as the widest figure, with `gap:` blank lines on each
268
+ side of it. `center:` centres each narrower figure as a whole.
269
+
270
+ ```text
271
+ $ tessella compose stack --rule '─' --center plot.txt note.txt
272
+ ┌ ┐
273
+ ruby ┤██████▍········· 12
274
+ c ┤████████████████ 30
275
+ rust ┤████············ 7.5
276
+ └ ┘
277
+ ──────────────────────────────
278
+ ruby is 12 kLOC
279
+ c is the bulk
280
+ ```
281
+
282
+ ```text
283
+ $ tessella compose stack --gap 1 note.txt note.txt
284
+ ruby is 12 kLOC
285
+ c is the bulk
286
+
287
+ ruby is 12 kLOC
288
+ c is the bulk
289
+ ```
290
+
291
+ ## indent
292
+
293
+ ```ruby
294
+ Tessella.indent(fig, prefix:, suffix: nil, ascii: false)
295
+ ```
296
+
297
+ Puts `prefix:` before every line. A line that is empty gets the
298
+ prefix without its trailing blank, so `"# "` becomes `"#"` there and
299
+ the result keeps the no-trailing-blank rule. This is how a figure
300
+ goes into a source comment:
301
+
302
+ ```ruby
303
+ # frozen_string_literal: true
304
+
305
+ # examples/comment.rb: a tree, in ASCII, as a comment block for a
306
+ # source file kept to ASCII.
307
+ require "tessella"
308
+
309
+ outline = File.read("outline.txt")
310
+ puts Tessella.indent(Tessella.tree(outline, ascii: true),
311
+ prefix: "# ", ascii: true)
312
+ ```
313
+
314
+ ```text
315
+ $ ruby comment.rb
316
+ # tessella
317
+ # |-- lib # the library
318
+ # | |-- tessella.rb # the entry
319
+ # | `-- tessella
320
+ # | `-- cli
321
+ # |-- exe
322
+ # | `-- tessella # the command
323
+ # `-- test
324
+ ```
325
+
326
+ With `suffix:` (`--suffix STR`), every row is padded to the block's
327
+ width and closed by the suffix, a blank row too, so a comment wrapped
328
+ around each row closes at one column; a suffix ending in a blank is
329
+ refused. `tessella lint --prefix STR --suffix STR` charges both to
330
+ the budget.
331
+
332
+ ```text
333
+ $ printf 'a -> b\n\nc\n' | \
334
+ tessella compose indent --prefix '<!-- ' --suffix ' -->' -
335
+ <!-- a -> b -->
336
+ <!-- -->
337
+ <!-- c -->
338
+ ```
339
+
340
+ ## caption
341
+
342
+ ```ruby
343
+ Tessella.caption(fig, text:, above: false, ascii: false)
344
+ ```
345
+
346
+ Adds a line of text centred on the figure, below it (or above it
347
+ with `above: true`), one blank line away.
348
+
349
+ ```text
350
+ $ tessella compose caption --text 'Figure 2: lines of code' plot.txt
351
+ ┌ ┐
352
+ ruby ┤██████▍········· 12
353
+ c ┤████████████████ 30
354
+ rust ┤████············ 7.5
355
+ └ ┘
356
+
357
+ Figure 2: lines of code
358
+ ```
359
+
360
+ ## shadow
361
+
362
+ ```ruby
363
+ Tessella.shadow(fig, glyph: nil, ascii: false)
364
+ ```
365
+
366
+ Adds a drop shadow: a glyph one column right of every row but the
367
+ first, and a bar under the figure from its second column to the
368
+ shadow's. The glyph is `░`, `#` under `ascii: true`; `glyph:`
369
+ (`--glyph`) is any glyph one column wide.
370
+
371
+ ```text
372
+ $ tessella compose frame note.txt | tessella compose shadow -
373
+ ┌─────────────────┐
374
+ │ ruby is 12 kLOC │░
375
+ │ c is the bulk │░
376
+ └─────────────────┘░
377
+ ░░░░░░░░░░░░░░░░░░░
378
+ ```
379
+
380
+ ```text
381
+ $ tessella compose frame --ascii note.txt | tessella compose shadow --ascii -
382
+ +-----------------+
383
+ | ruby is 12 kLOC |#
384
+ | c is the bulk |#
385
+ +-----------------+#
386
+ ###################
387
+ ```
388
+
389
+ A shadow drawn this way is the one [the lint](lint.md) expects, so
390
+ the figure stays lint-clean.
391
+
392
+ ## fill
393
+
394
+ ```ruby
395
+ Tessella.fill(fig, pattern: nil, halo: true, ascii: false)
396
+ Tessella.frame(fig, ..., fill: nil, halo: true)
397
+ ```
398
+
399
+ Fills every enclosed blank cell with a pattern: inside a box, a
400
+ frame, any closed region. What is enclosed is what a flood from the
401
+ figure's edge, through blank cells, cannot reach; text, lines and
402
+ everything outside are left as they are. Here two hand-drawn boxes in
403
+ `boxes.txt`: the space between them is not enclosed, so it stays
404
+ blank.
405
+
406
+ ```text
407
+ $ tessella compose fill --pattern stipple boxes.txt
408
+ ┌────┐ ┌────┐
409
+ │∴∵∴∵│──│ ok │
410
+ └────┘ └────┘
411
+ ```
412
+
413
+ `frame`'s `fill:` (`--fill P`) fills only the background the frame
414
+ adds, flooding from just inside its border; boxes the content draws
415
+ keep their own inside:
416
+
417
+ ```text
418
+ $ tessella compose frame --fill light --pad 2 boxes.txt
419
+ ┌──────────────────┐
420
+ │░░┌────┐░░┌────┐░░│
421
+ │░░│ │──│ ok │░░│
422
+ │░░└────┘░░└────┘░░│
423
+ └──────────────────┘
424
+ ```
425
+
426
+ ### The halo
427
+
428
+ A pattern packed against letters makes them hard to read, so the fill
429
+ keeps a halo of blanks around text: by default (`halo: true`,
430
+ `--halo sides`) the cell on each side of a text glyph stays blank, so
431
+ a word keeps its shape and the gap between words survives:
432
+
433
+ ```text
434
+ $ tessella compose frame --fill light --pad 3 note.txt
435
+ ┌─────────────────────┐
436
+ │░░ ruby is 12 kLOC ░░│
437
+ │░░ c is the bulk ░░░░│
438
+ └─────────────────────┘
439
+ ```
440
+
441
+ `--halo around` (`halo: :around`) also keeps the cells above and
442
+ below text blank; `--halo none` or `--no-halo` (`halo: false`) fills
443
+ right up to the letters, and between words:
444
+
445
+ ```text
446
+ $ tessella compose frame --fill light --pad 3 --halo none note.txt
447
+ ┌─────────────────────┐
448
+ │░░░ruby░is░12░kLOC░░░│
449
+ │░░░c░is░the░bulk░░░░░│
450
+ └─────────────────────┘
451
+ ```
452
+
453
+ Text is any glyph but box drawing and `+ - |`, so lines and borders
454
+ take the pattern right up to them.
455
+
456
+ A pattern is a name from the registry (`light`, the default, `medium`,
457
+ `dark`, `solid`, `hatch`, `backhatch`, `cross`, `grid`, `stipple`,
458
+ `sparse`, `dotted`, `dots`, `colons`, `hash`, `at`; `tessella glyphs
459
+ pattern` lists them) or glyphs of one's own. Several glyphs tile
460
+ diagonally. Each glyph must be one column wide, and all must share
461
+ one [width class](glyphs.md#width-classes), else the fill would not
462
+ line up in every terminal:
463
+
464
+ ```text
465
+ $ tessella compose fill --pattern '░▒' boxes.txt
466
+ tessella compose: pattern "░▒" mixes width classes: N, A
467
+ ```
468
+
469
+ Under `ascii: true` the pattern must be ASCII, and the default is
470
+ `colons`:
471
+
472
+ ```text
473
+ $ tessella compose frame --ascii --fill hash --pad 3 note.txt
474
+ +---------------------+
475
+ |## ruby is 12 kLOC ##|
476
+ |## c is the bulk ####|
477
+ +---------------------+
478
+ ```
479
+
480
+ ## ASCII
481
+
482
+ Every function takes `ascii: true`. Its borders are then drawn in
483
+ `+ - |`, any other `style:` is refused, and so is any figure or text
484
+ holding a non-ASCII character: it is refused rather than rewritten,
485
+ so an ASCII figure is ASCII because every part of it was drawn so.
486
+
487
+ ```text
488
+ $ tessella chart --kind bar --ascii --width 20 langs.csv | \
489
+ tessella compose frame --ascii --title Load -
490
+ +------------- Load -------------+
491
+ | + + |
492
+ | ruby |######.......... 12 |
493
+ | c |################ 30 |
494
+ | rust |####............ 7.5 |
495
+ | + + |
496
+ +--------------------------------+
497
+ ```
498
+
499
+ ## A larger figure
500
+
501
+ Diagrams compose like charts. Here a register layout is captioned and
502
+ stacked over its timing, under one rule:
503
+
504
+ ```ruby
505
+ # frozen_string_literal: true
506
+
507
+ # examples/datasheet.rb: a register layout over its timing, as a
508
+ # datasheet draws them, stacked under one rule.
509
+ require "tessella"
510
+
511
+ register = Tessella.bitfield([["MODE", 4], ["DIV", 8], ["EN", 4]],
512
+ word: 16)
513
+ timing = Tessella.wave(<<~WAVE, period: 4)
514
+ sclk: p.......
515
+ cs: 10.....1
516
+ mosi: x=.=.=.x | CMD ADDR DATA
517
+ WAVE
518
+ puts Tessella.stack(Tessella.caption(register, text: "CTRL (0x04)",
519
+ above: true),
520
+ timing, gap: 1, rule: "─")
521
+ ```
522
+
523
+ ```text
524
+ $ ruby datasheet.rb
525
+ CTRL (0x04)
526
+
527
+ 0 1
528
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
529
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
530
+ | MODE | DIV | EN |
531
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
532
+
533
+ ─────────────────────────────────────
534
+
535
+ ──┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
536
+ sclk └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─
537
+
538
+ ────┐ ┌───
539
+ cs └───────────────────────┘
540
+
541
+ ▒▒▒▒╳ ╳ ╳ ▒▒▒▒
542
+ mosi ▒▒▒▒╳──CMD──╳─ADDR──╳─DATA──▒▒▒▒
543
+ ```
544
+
545
+ ## `tessella compose`
546
+
547
+ ```text
548
+ tessella compose frame [--title T] [--style S] [--pad N] [--fill P]
549
+ [--halo H] BLOCK
550
+ tessella compose beside [--gap N] [--align A] BLOCK...
551
+ tessella compose stack [--gap N] [--rule C] [--center] BLOCK...
552
+ tessella compose indent --prefix STR BLOCK
553
+ tessella compose caption --text T [--above] BLOCK
554
+ tessella compose shadow [--glyph G] BLOCK
555
+ tessella compose fill [--pattern P] [--halo H] BLOCK
556
+ tessella compose grid [--columns N] [--gap N] [--row-gap N] [--align A]
557
+ BLOCK...
558
+ ```
559
+
560
+ `--halo` is `sides`, `around` or `none`; `--no-halo` is `none`.
561
+ Each BLOCK is a file, or `-` for stdin (once). Every subcommand also
562
+ takes `--ascii`, and `--max-width N` to refuse a result wider than N
563
+ columns.
564
+
565
+ Bad input exits 1, naming the block and the line: a missing file, an
566
+ empty block, a tab, a non-ASCII byte under `--ascii`, a result wider
567
+ than `--max-width`.
568
+
569
+ ```text
570
+ $ tessella compose frame --ascii plot.txt
571
+ tessella compose: plot.txt:1: non-ASCII byte
572
+ ```
573
+
574
+ ```text
575
+ $ tessella compose frame --max-width 20 plot.txt
576
+ tessella compose: output is 34 columns on line 1 (max-width 20)
577
+ ```
578
+
579
+ Composing several files from a script, with a temp directory for the
580
+ blocks, is shown in [the command's scripts](cli.md#scripts).
581
+
582
+ A usage mistake exits 2: an unknown style or alignment, a style
583
+ other than `ascii` with `--ascii`, the wrong number of blocks.
584
+
585
+ ```text
586
+ $ tessella compose beside plot.txt
587
+ tessella compose: beside takes at least 2 BLOCKs
588
+ Usage: tessella compose frame [--title T] [--style S] [--pad N]
589
+ [--fill P] [--halo H] BLOCK
590
+ tessella compose shadow [--glyph G] BLOCK
591
+ tessella compose fill [--pattern P] [--halo H] BLOCK
592
+ tessella compose grid [--columns N] [--gap N] [--row-gap N]
593
+ [--align A] BLOCK...
594
+ tessella compose beside [--gap N] [--align A] BLOCK...
595
+ tessella compose stack [--gap N] [--rule C] [--center] BLOCK...
596
+ tessella compose indent --prefix STR [--suffix STR] BLOCK
597
+ tessella compose caption --text T [--above] BLOCK
598
+ Every subcommand also takes --[no-]ascii and --max-width N.
599
+ ```