1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! GitHub's contribution chart, in the terminal.
//!
//! `mossaic` draws the year the way github.com draws it: [Primer]'s own
//! colours, read out of the stylesheets GitHub serves; cells at its own geometry
//! — an 11px square on a 14px pitch, rounded by 2px; and, on a terminal that
//! draws pixels, actual anti-aliased rounded squares rather than characters
//! shaped like them.
//!
//! This is the library the three binaries share:
//!
//! | binary | what it is |
//! | --- | --- |
//! | `mossaic` | the chart |
//! | `mossaic-art` | writes text into a contribution graph by dating commits |
//! | `mossaic-glyphs` | shows the fallback cells, to check what a terminal renders |
//!
//! # The chart, as pixels
//!
//! [`graphics`] rasterises a year and hands it to whichever protocol the
//! terminal speaks — [`graphics::kitty`] for RGBA over the kitty graphics
//! protocol, [`graphics::sixel`] for a palette and six pixels to a byte. The
//! same image goes to [`png`] for a terminal that draws neither.
//!
//! ```
//! use mossaic::graphics;
//! use mossaic::primer::{Appearance, Palette, Season};
//!
//! // `levels[week][weekday]`: GitHub's shade, or None for a day it does not draw.
//! let mut levels = vec![[None; 7]; 53];
//! levels[20][3] = Some(4);
//!
//! let palette = Palette::new(Appearance::Dark, Season::Default, true);
//! assert_eq!(palette.levels[4], mossaic::primer::Rgb::hex(0x56d364));
//!
//! // One character cell is 10x20 pixels here; a day gets two columns and one row.
//! let image = graphics::grid(&levels, &palette, (10, 20));
//! assert_eq!((image.width, image.height), (53 * 2 * 10, 7 * 20));
//!
//! let escape = graphics::sixel(&image, palette.canvas);
//! assert!(escape.starts_with("\x1bP0;1;0q"));
//! ```
//!
//! # Writing text into a year
//!
//! [`art`] draws characters as pixels on the calendar and maps every lit one to
//! a date. Letters are five columns wide with one between, and five rows tall on
//! Mon–Fri, so **eight characters** is what a year holds: nine need all 53
//! columns, and the first and last are partial weeks.
//!
//! ```
//! use mossaic::art::{self, Grid, Ink, Shades};
//!
//! # fn main() -> Result<(), String> {
//! let grid = Grid::new(2027).unwrap();
//! let columns = art::bitmap("VYNCINT")?;
//! assert_eq!(columns.len(), 41); // 6N - 1
//!
//! // Letters against an empty graph: the classic look.
//! let placed = art::place(&columns, &grid, 1, None, Ink { lit: 4, field: 0 })?;
//! assert_eq!(placed.skipped, 0, "centred, so nothing falls outside the year");
//! assert_eq!(placed.lit.len(), 75);
//! assert!(placed.field.is_empty());
//! # Ok(())
//! # }
//! ```
//!
//! Leaving the background empty means *not contributing* on the other three
//! hundred days. [`art::Shades`] draws it as a colour instead, so the letters
//! are the difference between two greens and the year stays busy:
//!
//! ```
//! use mossaic::art::{self, Grid, Shades};
//! use mossaic::primer::Legibility;
//!
//! # fn main() -> Result<(), String> {
//! let grid = Grid::new(2027).unwrap();
//! let columns = art::bitmap("VYNCINT")?;
//!
//! let shades = Shades { ink: 4, field: 1 };
//! shades.check()?;
//! // Level 1 under level 4 is plain in every palette GitHub ships.
//! assert_eq!(shades.worst().0, Legibility::Clear);
//!
//! // Four commits on a letter day makes four the year'"'"'s peak; one commit is
//! // then level 1, and the background costs a commit a day.
//! let ink = shades.commits(4);
//! assert_eq!((ink.lit, ink.field), (4, 1));
//!
//! let placed = art::place(&columns, &grid, 1, None, ink)?;
//! assert_eq!(placed.lit.len(), 75);
//! assert_eq!(placed.field.len(), 365 - 75, "every other day of the year");
//! # Ok(())
//! # }
//! ```
//!
//! # Asking the terminal
//!
//! [`term::probe`] asks rather than guesses: one write, five questions, one round
//! trip, and a deadline for the terminals that stay silent. What comes back
//! decides the protocol, the image scale, and whether the palette is the light
//! one or the dark one.
//!
//! [Primer]: https://primer.style
/// When to colour output.
///
/// `Auto` is what everything defaults to: colour when stdout is a terminal and
/// [`NO_COLOR`](https://no-color.org) is unset. The other two are for pipes
/// that want it anyway and terminals that do not.
/// Text with its control characters removed, for anything that reaches a
/// terminal.
///
/// A contribution calendar is data from elsewhere — the API, or a file someone
/// sent you — and a terminal executes what it is written. An `ESC` in a login
/// or an error message is a title change, a cursor-position report typed back
/// into the application, or an `OSC 52` clipboard write. The renderer drops
/// control characters when it fills a cell, but the paths that print straight
/// to stdout (`--png`, `--track`, error messages) do not, so untrusted text is
/// cleaned where it enters rather than at each of them.
/// A count with thousands separators, the way github.com writes one.
///
/// Here rather than in three modules: the chart, the tracker and the reports all
/// print counts, and they should all print them the same way.