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
data/doc/check.rb ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Keeps the documents in doc/ true to the code and to examples/.
5
+ #
6
+ # Two kinds of fence are checked, in every doc/*.md:
7
+ #
8
+ # - A render: a ```text fence that opens with "$ COMMAND". COMMAND
9
+ # may run over several lines, each but its last ending in a
10
+ # backslash, as sh reads it. The rest of the fence is what COMMAND
11
+ # prints, stdout and stderr merged. COMMAND runs under sh, in
12
+ # examples/, with this checkout's exe/ first on PATH and its lib/
13
+ # on RUBYLIB, so `tessella` and `require "tessella"` are the ones
14
+ # beside these documents.
15
+ # - A listing: a ```ruby or ```sh fence holding a line
16
+ # "# examples/NAME" in its first three. Its body is the file
17
+ # examples/NAME, whole.
18
+ #
19
+ # ruby doc/check.rb report each fence that differs
20
+ # ruby doc/check.rb --update rewrite the fences from what runs now
21
+ #
22
+ # Exit 0 when every fence agrees (or after --update), 1 otherwise.
23
+
24
+ require "open3"
25
+
26
+ ROOT = File.expand_path("..", __dir__)
27
+ EXAMPLES = File.join(ROOT, "examples")
28
+ RUN_ENV = {
29
+ "PATH" => "#{File.join(ROOT, "exe")}:#{ENV.fetch("PATH")}",
30
+ "RUBYLIB" => File.join(ROOT, "lib")
31
+ }.freeze
32
+ RENDER = /^```text\n\$ (?<key>(?:[^\n]*\\\n)*[^\n]*)\n(?<body>.*?)^```$/m
33
+ LISTING = %r{
34
+ ^```(?<lang>ruby|sh)\n
35
+ (?<body>(?:(?!```)[^\n]*\n){0,2}?\#\ examples/(?<key>[\w.-]+)[^\n]*\n.*?)
36
+ ^```$
37
+ }mx
38
+
39
+ def render(cmd)
40
+ Open3.capture2e(RUN_ENV, "sh", "-c", cmd, chdir: EXAMPLES).first
41
+ end
42
+
43
+ def listing(name) = File.read(File.join(EXAMPLES, name))
44
+
45
+ def fence(kind, match, body)
46
+ return "```text\n$ #{match[:key]}\n#{body}```" if kind == :render
47
+
48
+ "```#{match[:lang]}\n#{body}```"
49
+ end
50
+
51
+ # The text with every fence rewritten from what it should hold; each
52
+ # fence that differs is noted in stale.
53
+ def refresh(text, doc, stale)
54
+ [[RENDER, :render], [LISTING, :listing]].reduce(text) do |out, (re, kind)|
55
+ out.gsub(re) do
56
+ m = Regexp.last_match
57
+ got = send(kind, m[:key])
58
+ stale << "#{doc}: #{m[:key]}" unless got == m[:body]
59
+ fence(kind, m, got)
60
+ end
61
+ end
62
+ end
63
+
64
+ update = ARGV.delete("--update")
65
+ stale = []
66
+ Dir[File.join(__dir__, "*.md")].each do |path|
67
+ text = File.read(path)
68
+ out = refresh(text, File.basename(path), stale)
69
+ File.write(path, out) if update && out != text
70
+ end
71
+ exit 0 if update || stale.empty?
72
+
73
+ warn "out of date:"
74
+ stale.each { warn " #{it}" }
75
+ exit 1
data/doc/cli.md ADDED
@@ -0,0 +1,483 @@
1
+ # The `tessella` command
2
+
3
+ ```text
4
+ tessella [--target T] COMMAND [options] [file]
5
+ tessella --help
6
+ tessella --version
7
+ ```
8
+
9
+ The first argument names the command. Each command reads one file,
10
+ or stdin when none is given (`compose` reads several), and prints one
11
+ figure on stdout. Each has its own `--help`. `--target term|cjk|web`,
12
+ before the command, or `TESSELLA_TARGET` in the environment, draws
13
+ for a [width target](targets.md). Every drawing command takes
14
+ `--ascii` and `--no-ascii`; the second matters only under `cjk`,
15
+ where ASCII is the default.
16
+
17
+ | Command | Draws | Page |
18
+ |------------|--------------------------------------------|--------------------------------|
19
+ | `chart` | line, scatter, stairs, density, bar, histogram, box, stem, heatmap, candlestick, pie | [charts](charts.md) |
20
+ | `compose` | frame, beside, stack, grid, indent, caption, shadow, fill | [composition](composition.md) |
21
+ | `tree` | an indented outline as a tree | [tree](tree.md) |
22
+ | `table` | CSV or TSV rows as a table | [table](table.md) |
23
+ | `bitfield` | an RFC-style packet or register layout | [bitfield](bitfield.md) |
24
+ | `wave` | a timing diagram | [wave](wave.md) |
25
+ | `seq` | a sequence diagram | [seq](seq.md) |
26
+ | `image` | a picture as text | [images](images.md) |
27
+ | `lint` | checks a text figure, drawn or hand-made | [lint](lint.md) |
28
+ | `sparkline` | a one-line chart, one cell a value | [sparkline](sparkline.md) |
29
+ | `meter` | a one-line progress bar or gauge | [meter](meter.md) |
30
+ | `rule` | a divider, with an optional ornament | [rule](rule.md) |
31
+ | `text` | letters in a Unicode style: bold, script… | [text](text.md) |
32
+ | `glyphs` | lists glyph sets and their width classes | [glyphs](glyphs.md) |
33
+
34
+ ## Exit status
35
+
36
+ Every command keeps one contract:
37
+
38
+ | Status | Meaning |
39
+ |--------|---------------------------------------------------------------|
40
+ | 0 | the figure was drawn (or `--help`, `--version` answered) |
41
+ | 1 | bad input: a file that cannot be read, a malformed line, a value out of range, a figure wider than `--max-width` |
42
+ | 2 | a usage mistake: an unknown command or option, a bad option value, an option the command or kind does not take |
43
+
44
+ The message goes to stderr as `tessella COMMAND: message`, naming the
45
+ line, row or block it is about; a usage mistake adds the usage line.
46
+
47
+ A file that cannot be read is reported the same way by every
48
+ command, naming it (`stdin` when it was stdin):
49
+
50
+ ```text
51
+ $ tessella table missing.csv
52
+ tessella table: cannot read missing.csv: No such file or directory
53
+ ```
54
+
55
+ ```text
56
+ $ tessella chart --kind radar langs.csv
57
+ tessella chart: invalid argument: --kind radar
58
+ Usage: tessella chart [options] [file]
59
+ ```
60
+
61
+ ```text
62
+ $ tessella 2>&1 | head -2
63
+ tessella: no command
64
+ Usage: tessella [--target T] COMMAND [options] [file]
65
+ ```
66
+
67
+ ## `--max-width N`
68
+
69
+ Every diagram command (`compose`, `tree`, `table`, `bitfield`,
70
+ `wave`, `seq`, `image`) takes `--max-width N`, and refuses with exit 1
71
+ a figure wider than N columns rather than print one that would wrap.
72
+ A build that writes figures into a document with an 80-column rule
73
+ can let tessella enforce it:
74
+
75
+ ```text
76
+ $ tessella seq --max-width 30 login.txt
77
+ tessella seq: output is 41 columns, over --max-width 30
78
+ ```
79
+
80
+ ## `tessella chart`
81
+
82
+ ```text
83
+ tessella chart [--kind K] [--width N] [--height N] [--title S]
84
+ [--xlabel S] [--ylabel S] [--xlim A,B] [--ylim A,B]
85
+ [--canvas C] [--xscale F] [--yscale F] [--xdate [FMT]]
86
+ [--yerr] [--threshold N] [--above C] [--below C]
87
+ [--ascii] [--color]
88
+ [--style post|pre] [--stacked]
89
+ [--orientation horizontal|vertical]
90
+ [--area] [--gradient C,C,...] [--blocks B]
91
+ [--bins N] [--closed left|right]
92
+ [--scale N] [--fill G] [--no-eighths] [--track G|none]
93
+ [--cell-width N] [--colormap C,C,...] [--min A] [--max B]
94
+ [--donut] [--rim cells|smooth|basic]
95
+ [file]
96
+ ```
97
+
98
+ | Option | Takes | Default |
99
+ |------------------------|----------------------------------------|-----------------|
100
+ | `--kind K` | `line scatter stairs density bar histogram box stem heatmap candlestick pie` | `line` |
101
+ | `--width N` | plot area width, cells | 40 |
102
+ | `--height N` | plot area height, cells | 15 |
103
+ | `--title S` | a title above the frame | |
104
+ | `--xlabel S` | a label under the x axis | |
105
+ | `--ylabel S` | a label left of the y axis | |
106
+ | `--xlim A,B` | x limits | from the data |
107
+ | `--ylim A,B` | y limits | from the data |
108
+ | `--canvas C` | `braille block quadrant half eighths dot ascii density` | `braille` (`block` under `--target web`) |
109
+ | `--xscale F` | `ln log2 log10` | |
110
+ | `--yscale F` | plotted kinds: `ln log2 log10` | |
111
+ | `--xdate [FMT]` | plotted kinds: x as ISO 8601 dates | format by span |
112
+ | `--yerr` | line, scatter: third column is the error | off |
113
+ | `--threshold N` | line, scatter, stairs: a dashed rule at N | none |
114
+ | `--above C`, `--below C` | with --color: colours beyond and short of N | red; series |
115
+ | `--ascii` | ASCII only | off |
116
+ | `--color` | ANSI colour in the plot area | off |
117
+ | `--style S` | stairs: `post pre` | `post` |
118
+ | `--orientation O` | all but stem: value axis direction | by kind |
119
+ | `--cell-width N` | heatmap: columns a cell | 2 |
120
+ | `--colormap C,...` | heatmap, with --color: colour stops | viridis-like |
121
+ | `--min A`, `--max B` | heatmap: fix the range | the data's |
122
+ | `--donut` | pie: a ring, not a disc | off |
123
+ | `--rim R` | pie: `cells smooth basic` edge | `cells` |
124
+ | `--stacked` | bar: series end to end, not grouped | grouped |
125
+ | `--blocks B` | bar, histogram: `eighths half block quadrant braille` | `eighths` |
126
+ | `--area` | line, stairs: fill under each series | off |
127
+ | `--gradient C,...` | area: colours by height, or `none` | green,yellow,red |
128
+ | `--bins N` | histogram: number of bins | Sturges' rule |
129
+ | `--closed C` | histogram: `left right` | `left` |
130
+ | `--scale N` | stem: value of one stem step | 10 |
131
+ | `--fill G` | bar, histogram: bar glyph | `█` (`#`) |
132
+ | `--no-eighths` | bar, histogram: whole cells only | |
133
+ | `--track G` | bar, histogram: track glyph, or `none` | `·` (`.`) |
134
+
135
+ `tessella chart --help` lists the kinds with the columns each reads,
136
+ and describes every option. The input format, column by kind, is in
137
+ [charts](charts.md#the-input-format).
138
+
139
+ ## `tessella compose`
140
+
141
+ ```text
142
+ tessella compose frame [--title T] [--style S] [--pad N] [--fill P]
143
+ [--halo H] BLOCK
144
+ tessella compose beside [--gap N] [--align top|middle|bottom] BLOCK...
145
+ tessella compose stack [--gap N] [--rule C] [--center] BLOCK...
146
+ tessella compose indent --prefix STR [--suffix STR] BLOCK
147
+ tessella compose caption --text T [--above] BLOCK
148
+ tessella compose shadow [--glyph G] BLOCK
149
+ tessella compose fill [--pattern P] [--halo H] BLOCK
150
+ tessella compose grid [--columns N] [--gap N] [--row-gap N] [--align A]
151
+ BLOCK...
152
+ ```
153
+
154
+ All take `--ascii` and `--max-width N`. A BLOCK is a file, or `-` for
155
+ stdin, once. See [composition](composition.md).
156
+
157
+ ## The diagram commands
158
+
159
+ ```text
160
+ tessella tree [--style light|heavy|rounded] [--ascii] [--root NAME] [file]
161
+ tessella table [--tsv] [--no-header] [--style S] [--ascii]
162
+ [--align SPEC] [--title T] [file]
163
+ tessella bitfield [--word 8|16|32|64] [--octets] [--no-ruler] [--abbrev]
164
+ [file]
165
+ tessella wave [--period N] [--ascii] [--ruler] [file]
166
+ tessella seq [--ascii] [--numbered] [--footer] [file]
167
+ tessella image [--style S] [--width N] [--dither M] [--ramp STR]
168
+ [--gamma F] [--threshold F] [--edge-threshold F]
169
+ [--invert] [--ascii] [file]
170
+ ```
171
+
172
+ Each also takes `--max-width N`. A frame's `--style` also takes six
173
+ glyphs (`tl tr bl br h v`), a table's eleven (those six, then `top_t
174
+ bot_t sep_l sep_t sep_r`): see
175
+ [custom borders](composition.md#custom-borders). Table styles are `light heavy double
176
+ rounded dashed dashed_fine heavy_dashed heavy_dashed_fine double_h
177
+ double_v ascii minimal markdown`; image styles `ramp blocks half
178
+ quadrant sextant braille octant edges`, dithers `fs atkinson bayer none`.
179
+
180
+ ## `tessella lint`
181
+
182
+ ```text
183
+ tessella lint [--width N] [--prefix STR] [--suffix STR] [--ruler]
184
+ [--forbid SEQ]...
185
+ [--web] [--no-wide] [--ascii] [--allow-mixed]
186
+ [--fences] [--geometry] [--border GLYPHS]... [file ...]
187
+ tessella lint --alphabet [file ...]
188
+ ```
189
+
190
+ Unlike the drawing commands, `lint` takes several files and prints
191
+ its findings on stderr, as `FILE:LINE: message`; `--ruler` and
192
+ `--alphabet` print on stdout. It exits 1 on any finding. See
193
+ [lint](lint.md).
194
+
195
+ ## Pipelines
196
+
197
+ Every command reads stdin and writes stdout, so they chain; `compose`
198
+ reads `-` as stdin:
199
+
200
+ ```text
201
+ $ tessella chart --kind bar --width 20 langs.csv | \
202
+ tessella compose caption --text 'Lines of code' -
203
+ ┌ ┐
204
+ ruby ┤██████▍········· 12
205
+ c ┤████████████████ 30
206
+ rust ┤████············ 7.5
207
+ └ ┘
208
+
209
+ Lines of code
210
+ ```
211
+
212
+ ## Scripts
213
+
214
+ The commands are built to be scripted: each reads a file or stdin,
215
+ writes the figure on stdout and nothing else, and says what went wrong
216
+ by its exit status. The scripts below are in
217
+ [examples/](../examples/), POSIX sh for FreeBSD and GNU/Linux alike,
218
+ and each runs from any directory.
219
+
220
+ ### Data from another program
221
+
222
+ Anything that prints columns can feed a chart; a header row names
223
+ the series.
224
+
225
+ ```sh
226
+ #!/bin/sh
227
+ # examples/trig.sh -- data made on the spot by awk, piped straight
228
+ # into a chart: a header row names the two series.
229
+ # usage: sh trig.sh
230
+ set -eu
231
+
232
+ awk 'BEGIN {
233
+ print "x sin cos"
234
+ for (i = 0; i <= 100; i++)
235
+ printf "%g %g %g\n", i / 10, sin(i / 10), cos(i / 10)
236
+ }' |
237
+ tessella chart --title trig --xlabel x --width 30 --height 6 \
238
+ --canvas block
239
+ ```
240
+
241
+ ```text
242
+ $ sh trig.sh
243
+ trig
244
+ ┌──────────────────────────────┐
245
+ 1 │▀▜▄▀▀▀▚▖ ▗▀▀▀▚▟▀▀▀▚ │ sin
246
+ │ ▟▀▙ ▝▖ ▟▘ ▞▀▄ ▀▖ │ cos
247
+ │▞ ▝▚ ▝▖ ▞▘ ▗▞ ▝▖ ▜▖ │
248
+ │▀▀▀▀▀█▀▀▀▀▛▀▀█▀▀▀▀▛▀▀▀▀▀▛▀▀▀▜▛│
249
+ │ ▙ ▝▖▞ ▗▛ ▝▄ ▀│
250
+ -1 │ ▜▄▄▄▛▚▄▄▟▀ ▝▚▄▄▄│
251
+ └──────────────────────────────┘
252
+ 0 10
253
+ x
254
+ ```
255
+
256
+ ### Several figures, one result
257
+
258
+ `compose` reads only one block from stdin, so the other blocks go
259
+ through files; a temp directory, removed on exit, keeps them.
260
+
261
+ ```sh
262
+ #!/bin/sh
263
+ # examples/report.sh -- a chart and a table drawn from one CSV, set
264
+ # side by side, framed and captioned: compose reads one block from
265
+ # stdin, so the others go through a temp directory.
266
+ # usage: sh report.sh [file.csv] (default langs.csv)
267
+ set -eu
268
+
269
+ cd -- "$(dirname -- "$0")" || exit 1
270
+ csv=${1:-langs.csv}
271
+
272
+ tmpdir=
273
+ cleanup() { [ -z "$tmpdir" ] || rm -rf -- "$tmpdir"; }
274
+ trap cleanup EXIT INT HUP TERM
275
+ tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/report.XXXXXX") || exit 1
276
+
277
+ tessella chart --kind bar --width 20 -- "$csv" > "$tmpdir/chart"
278
+ tessella table --style rounded -- "$csv" > "$tmpdir/table"
279
+ tessella compose beside --gap 2 --align middle \
280
+ "$tmpdir/chart" "$tmpdir/table" |
281
+ tessella compose frame --title Load - |
282
+ tessella compose caption --text "Source: $csv" -
283
+ ```
284
+
285
+ ```text
286
+ $ sh report.sh
287
+ ┌───────────────────── Load ─────────────────────┐
288
+ │ ╭──────┬─────╮ │
289
+ │ ┌ ┐ │ lang │ loc │ │
290
+ │ ruby ┤██████▍········· 12 ├──────┼─────┤ │
291
+ │ c ┤████████████████ 30 │ ruby │ 12 │ │
292
+ │ rust ┤████············ 7.5 │ c │ 30 │ │
293
+ │ └ ┘ │ rust │ 7.5 │ │
294
+ │ ╰──────┴─────╯ │
295
+ └────────────────────────────────────────────────┘
296
+
297
+ Source: langs.csv
298
+ ```
299
+
300
+ The same in a loop: one picture in four styles.
301
+
302
+ ```sh
303
+ #!/bin/sh
304
+ # examples/gallery.sh -- one picture in several styles, each captioned
305
+ # with its name, then set side by side.
306
+ # usage: sh gallery.sh [image] (default disc.pgm)
307
+ set -eu
308
+
309
+ cd -- "$(dirname -- "$0")" || exit 1
310
+ image=${1:-disc.pgm}
311
+
312
+ tmpdir=
313
+ cleanup() { [ -z "$tmpdir" ] || rm -rf -- "$tmpdir"; }
314
+ trap cleanup EXIT INT HUP TERM
315
+ tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/gallery.XXXXXX") || exit 1
316
+
317
+ for style in ramp half quadrant edges; do
318
+ tessella image --style "$style" --width 14 -- "$image" |
319
+ tessella compose caption --text "$style" - > "$tmpdir/$style"
320
+ done
321
+ tessella compose beside --gap 3 --align bottom \
322
+ "$tmpdir/ramp" "$tmpdir/half" "$tmpdir/quadrant" "$tmpdir/edges"
323
+ ```
324
+
325
+ <!-- tessella: allow-mixed -->
326
+ ```text
327
+ $ sh gallery.sh
328
+ .. ────
329
+ .--===-:. ▄ █ ▀▄ ▖▞▞▞▖▚ ╱──────╲╲
330
+ :=+*##*+=-. █ █▀█▀▄▀▄ ▗▚▚▛▟▙▛▌▌▚ ╱╱╱╱───╲╲╲╲
331
+ .-+*#%%#*+-: ▄▀█▀███▀▄▄ ▘▌▛▟█▙█▜▐▗ │││ ││
332
+ .:=+*##*+=-. ▀▄▀█▀█▀▄ ▘▚▀▛▟▜▞▌▌▚ ╲╲╲╲──╱╱╱╱╱
333
+ :-====-:. ▀ ▀▄▀ ▀ ▘▘▚▘▌▘▚▝ ╲╲──────╱╱
334
+ .... ▀ ▘ ──────
335
+
336
+ ramp half quadrant edges
337
+ ```
338
+
339
+ ### Figures for source files and plain-text documents
340
+
341
+ A tree as a C comment, refused rather than wrapped if it outgrows the
342
+ column budget:
343
+
344
+ ```sh
345
+ #!/bin/sh
346
+ # examples/comment.sh -- a tree as a C block comment, for a header
347
+ # kept to ASCII and 72 columns: --max-width fails the build (exit 1)
348
+ # rather than let the figure wrap.
349
+ # usage: sh comment.sh
350
+ set -eu
351
+
352
+ cd -- "$(dirname -- "$0")" || exit 1
353
+
354
+ printf '/*\n'
355
+ tessella tree --ascii --max-width 69 outline.txt |
356
+ tessella compose indent --ascii --prefix ' * ' -
357
+ printf ' */\n'
358
+ ```
359
+
360
+ ```text
361
+ $ sh comment.sh
362
+ /*
363
+ * tessella
364
+ * |-- lib # the library
365
+ * | |-- tessella.rb # the entry
366
+ * | `-- tessella
367
+ * | `-- cli
368
+ * |-- exe
369
+ * | `-- tessella # the command
370
+ * `-- test
371
+ */
372
+ ```
373
+
374
+ An RFC-style figure: everything ASCII, stacked, captioned, indented
375
+ three columns, and held to 72 columns.
376
+
377
+ ```sh
378
+ #!/bin/sh
379
+ # examples/rfc.sh -- an RFC-style section: a message exchange and the
380
+ # header it carries, both ASCII, stacked under a rule and indented
381
+ # three columns, as an RFC sets its figures.
382
+ # usage: sh rfc.sh
383
+ set -eu
384
+
385
+ cd -- "$(dirname -- "$0")" || exit 1
386
+
387
+ tmpdir=
388
+ cleanup() { [ -z "$tmpdir" ] || rm -rf -- "$tmpdir"; }
389
+ trap cleanup EXIT INT HUP TERM
390
+ tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/rfc.XXXXXX") || exit 1
391
+
392
+ printf 'Client -> Server: Echo Request\nServer --> Client: Echo Reply\n' |
393
+ tessella seq --ascii > "$tmpdir/seq"
394
+ tessella bitfield icmp.txt |
395
+ tessella compose caption --ascii --text 'Figure 1: ICMP header' - \
396
+ > "$tmpdir/header"
397
+ tessella compose stack --ascii --gap 1 --rule '-' --center \
398
+ "$tmpdir/seq" "$tmpdir/header" |
399
+ tessella compose indent --ascii --prefix ' ' --max-width 72 -
400
+ ```
401
+
402
+ ```text
403
+ $ sh rfc.sh
404
+ +---------+ +---------+
405
+ | Client | | Server |
406
+ +----+----+ +----+----+
407
+ | |
408
+ | Echo Request |
409
+ |------------->|
410
+ | |
411
+ | Echo Reply |
412
+ |<- - - - - - -|
413
+ | |
414
+
415
+ -----------------------------------------------------------------
416
+
417
+ 0 1 2 3
418
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
419
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
420
+ | Type | Code | Checksum |
421
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
422
+
423
+ Figure 1: ICMP header
424
+ ```
425
+
426
+ ### Exit status in a script
427
+
428
+ Exit 1 is the data's fault, exit 2 the caller's. A script that draws
429
+ many inputs can skip a bad file and still stop on a bad call:
430
+
431
+ ```sh
432
+ #!/bin/sh
433
+ # examples/status.sh -- telling bad data (exit 1) from a bad call
434
+ # (exit 2) in a script: draw what can be drawn, report the rest.
435
+ # usage: sh status.sh
436
+ set -eu
437
+
438
+ cd -- "$(dirname -- "$0")" || exit 1
439
+
440
+ draw() {
441
+ # shellcheck disable=SC3043 # local: every target sh has it
442
+ local status
443
+ status=0
444
+ tessella chart --kind bar --width 20 "$@" 2>/dev/null || status=$?
445
+ case $status in
446
+ 0) ;;
447
+ 1) printf '%s: bad data, skipped\n' "$*" ;;
448
+ *) printf '%s: bad call (exit %s)\n' "$*" "$status" ;;
449
+ esac
450
+ }
451
+
452
+ draw langs.csv
453
+ draw bad.csv
454
+ draw --bins 3 langs.csv
455
+ ```
456
+
457
+ ```text
458
+ $ sh status.sh
459
+ ┌ ┐
460
+ ruby ┤██████▍········· 12
461
+ c ┤████████████████ 30
462
+ rust ┤████············ 7.5
463
+ └ ┘
464
+ bad.csv: bad data, skipped
465
+ --bins 3 langs.csv: bad call (exit 2)
466
+ ```
467
+
468
+ ## From Ruby
469
+
470
+ The command line is a library too, loaded apart from the drawing
471
+ library:
472
+
473
+ ```ruby
474
+ require "tessella/cli" # loads tessella, optparse, the commands
475
+
476
+ status = Tessella::CLI.run(%w[chart --kind bar langs.csv],
477
+ stdin: $stdin, stdout: $stdout,
478
+ stderr: $stderr)
479
+ ```
480
+
481
+ `CLI.run` returns the exit status instead of exiting, and writes
482
+ only to the streams it is given, so a test can pass `StringIO`s.
483
+ `require "tessella"` alone loads neither the commands nor `optparse`.