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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
// devela::media::visual::image::sixel::char
//
//! Defines [`SixelChar`].
//
use crate::{Display, FmtResult, FmtWriter, Formatter, StringU8, TextLut, format_buf, is, unwrap};
#[doc = crate::_tags!(image term)]
/// A sixel character.
#[doc = crate::_doc_location!("media/visual/image")]
#[must_use]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct SixelChar(u8);
impl SixelChar {
const MASK: u8 = 0b111_111; // == 63
/// Create a sixel character from the first 6 bits of the given `mask`.
///
/// Bit 1 is the bottom pixel and bit 6 (== 32) is the top pixel.
/// Bits 7 and 8 are ignored.
#[inline(always)] #[rustfmt::skip]
pub const fn from_bitmask(mask: u8) -> Self { Self(mask & Self::MASK) }
/// Get the 6-bit bitmask representation.
#[must_use] #[inline(always)] #[rustfmt::skip]
pub const fn as_bitmask(self) -> u8 { self.0 }
/// Get the sixel byte value.
#[must_use] #[inline(always)] #[rustfmt::skip]
pub const fn as_byte(self) -> u8 { self.0 + Self::MASK }
/// Create a sixel character from a valid Unicode scalar, from '@' to '~'.
pub const fn from_char(c: char) -> Option<Self> {
let byte = c as u32;
if byte >= Self::MASK as u32 && byte <= (Self::MASK + Self::MASK) as u32 {
Some(Self((byte - Self::MASK as u32) as u8))
} else {
None
}
}
/// Returns the corresponding Unicode scalar.
#[must_use] #[inline(always)] #[rustfmt::skip]
pub const fn as_char(self) -> char { self.as_byte() as char }
/// Checks the equality of two chars.
#[must_use] #[inline(always)] #[rustfmt::skip]
pub const fn eq(self, other: Self) -> bool { self.0 == other.0 }
/// Returns `true` if all the pixels are unset.
#[must_use] #[inline(always)] #[rustfmt::skip]
pub const fn is_empty(self) -> bool { self.eq(Self::EMPTY) }
/// Returns `true` if all the pixels are set.
#[must_use] #[inline(always)] #[rustfmt::skip]
pub const fn is_full(self) -> bool { self.eq(Self::FULL) }
/// Returns the next sixel character in sequence, wrapping around from 63 to 0.
pub const fn next(self) -> Self {
is![self.eq(Self::FULL), Self::EMPTY, Self(self.0 - 1)]
}
/// Returns the previous sixel character in sequence, wrapping around from 0 to 63.
pub const fn prev(self) -> Self {
is![self.eq(Self::EMPTY), Self::FULL, Self(self.0 - 1)]
}
/// Returns the next sixel character in sequence, returning None if at maximum.
#[must_use]
pub const fn next_checked(self) -> Option<Self> {
is![self.eq(Self::FULL), None, Some(Self(self.0 + 1))]
}
/// Returns the previous sixel character in sequence, returning None if at minimum.
#[must_use]
pub const fn prev_checked(self) -> Option<Self> {
is![self.eq(Self::EMPTY), None, Some(Self(self.0 - 1))]
}
}
/// # Other conversions
impl SixelChar {
/* bools */
/// Create a sixel character from 6 pixels [top, 2, 3, 4, 5].
#[allow(clippy::identity_op)]
pub const fn from_bools(bools: [bool; 6]) -> Self {
let value = (bools[0] as u8) * 32 // Top pixel = MSB
+ (bools[1] as u8) * 16
+ (bools[2] as u8) * 8
+ (bools[3] as u8) * 4
+ (bools[4] as u8) * 2
+ (bools[5] as u8) * 1; // Bottom pixel = LSB
Self(value)
}
/// Extract the sixel bits as [top, 2, 3, 4, 5, bottom] bools.
#[must_use]
pub const fn to_bools(self) -> [bool; 6] {
let value = self.as_bitmask();
[
value & 32 != 0, // Top pixel (MSB)
value & 16 != 0,
value & 8 != 0,
value & 4 != 0,
value & 2 != 0,
value & 1 != 0, // Bottom pixel (LSB)
]
}
/* braille */
/// Attempts to create a `SixelChar` from a Braille pattern Unicode character.
///
/// Returns `Some(SixelChar)` if the character is in the Braille block (U+2800..=U+28FF), where
/// dots 1-6 map to the sixel bits (dots 7-8 are ignored). Returns `None` for other characters.
pub const fn from_braille(braille: char) -> Option<Self> {
let b = braille as u32;
if b >= 0x2800 && b <= 0x28FF {
let bits = (b & Self::MASK as u32) as u8;
Some(Self::from_bitmask(bits))
} else {
None
}
}
/// Creates a `SixelChar` from any character, using the lower 6 bits of its codepoint.
///
/// For Braille patterns (U+2800..=U+28FF), this preserves dots 1-6 and ignores dots 7-8.
/// For other characters, the result may not be visually meaningful.
pub const fn from_braille_unchecked(braille: char) -> Self {
Self::from_bitmask((braille as u32 & Self::MASK as u32) as u8)
}
/// Converts this sixel to a Braille pattern character.
///
/// The sixel bits map directly to Braille dots 1-6:
/// - Bit 0 (top) → Dot 1
/// - Bit 5 (bottom) → Dot 6
#[must_use]
pub const fn to_braille(self) -> char {
unwrap![some char::from_u32(0x2800 + self.as_bitmask() as u32)]
}
/* hexagram */
/// Creates a sixel from an I Ching hexagram character.
///
/// - Returns `None` if the character is not in the hexagram range (U+4DC0..=U+4DFF).
/// - Standard mapping: filled pixels represent broken lines.
pub const fn from_hexagram(hexagram: char) -> Option<Self> {
let h = hexagram as usize;
if h >= 0x4DC0 && h <= 0x4DFF {
Some(Self::from_bitmask(TextLut::CHAR_HEXAGRAM_TO_SIXEL[h - 0x4DC0]))
} else {
None
}
}
/// Converts to an I Ching hexagram character using standard mapping.
///
/// - Standard mapping: filled pixels represent broken lines.
pub const fn to_hexagram(self) -> char {
TextLut::SIXEL_TO_CHAR_HEXAGRAM[self.as_bitmask() as usize]
}
/// Creates a sixel from an I Ching hexagram character.
///
/// - Returns `None` if the character is not in the hexagram range (U+4DC0..=U+4DFF).
/// - Inverted mapping: filled pixels represent unbroken lines.
pub const fn from_hexagram_inverted(hexagram: char) -> Option<Self> {
let h = hexagram as usize;
if h >= 0x4DC0 && h <= 0x4DFF {
Some(Self::from_bitmask(!TextLut::CHAR_HEXAGRAM_TO_SIXEL[h - 0x4DC0]))
} else {
None
}
}
/// Converts to an I Ching hexagram character using inverted mapping.
///
/// - Inverted mapping: filled pixels represent unbroken lines.
pub const fn to_hexagram_inverted(self) -> char {
TextLut::SIXEL_TO_CHAR_HEXAGRAM[(!self.as_bitmask() & Self::MASK) as usize]
}
/* string */
/// Converts this sixel to an ANSI-colored string showing multiple representations.
///
/// The output displays:
/// - The raw character (red)
/// - Braille pattern equivalent (green)
/// - Box representation (blue)
/// - Hexagram character (cyan)
/// - Binary bitmask representation
///
/// Uses ANSI escape codes for coloring and resets formatting at the end.
/// # Example
/// ```ignore
/// # use devela::{Ansi, SixelChar, write_at};
/// let (mut offset, mut result) = (0, [0; 65]);
/// write_at![result, +=offset, @Ansi::BLACK_BG, @Ansi::RED, b'@', @'⠁'];
/// assert_eq![
/// SixelChar::TOP.to_string_ansi(), // == "@⠁■□□□□□|000001"
/// "\u{1b}[40m\u{1b}[31m@\u{1b}[32m⠁\u{1b}[34m■□□□□□\u{1b}[0m\u{1b}[36m\u{1b}[0m|000001"
/// ];
/// ```
#[allow(non_snake_case)]
// TODO: make const to_string_ansi
pub fn to_string_ansi(&self) -> StringU8<65> {
use crate::{Ansi, lets};
let mut buf = [0; 65];
let c = self.as_char();
let m = self.as_bitmask();
let b = self.to_braille();
// let h = self.to_hexagram(); // double-width
let bx = self.to_string_box();
lets![res = "\x1b[0m", @Ansi::{R=RED, B=BLUE, G=GREEN, W=CYAN, KB=BLACK_BG}];
let args = format_args!["{KB}{R}{c}{G}{b}{B}{bx}{res}{W}{res}|{m:06b}"];
let len = FmtWriter::format_len_unchecked(&mut buf, args);
StringU8::<65>::_from_array_len_trusted(buf, len as u8)
}
/// Converts this sixel to a box representation showing pixel states.
///
/// Each of the six pixels is represented as either:
/// - `■` for filled/true pixels
/// - `□` for empty/false pixels
///
/// The output shows pixels from top to bottom.
/// # Example
/// ```
/// # use devela::SixelChar;
/// assert_eq![SixelChar::from_bitmask(0b111001).to_string_box(), "■□□■■■"];
/// ```
pub const fn to_string_box(&self) -> StringU8<20> {
let mut string = StringU8::<20>::new();
let mask = self.as_bitmask();
let mut i = 0;
while i < 6 {
is![mask & (1 << i) != 0, string.push('■'), string.push('□')];
i += 1;
}
assert![string.len() == 18];
string
}
}
/// # Constants
impl SixelChar {
/// No pixels set. (?)
pub const EMPTY: Self = Self::from_bitmask(0b000_000);
/// All the six pixels set ().
pub const FULL: Self = Self::from_bitmask(0b111_111);
/// The bottom pixel (_).
pub const BOT: Self = Self::from_bitmask(0b100_000); // MSB
/// The 2 bottom pixels (o).
pub const BOT2: Self = Self::from_bitmask(0b110_000);
/// The 3 bottom pixels (w).
pub const BOT3: Self = Self::from_bitmask(0b111_000);
/// The 3 bottom pixels ({).
pub const BOT4: Self = Self::from_bitmask(0b111_100);
/// The 5 bottom pixels (}).
pub const BOT5: Self = Self::from_bitmask(0b111_110);
/// The top pixel (@).
pub const TOP: Self = Self::from_bitmask(0b000_001); // LSB
/// The 2 top pixels (B).
pub const TOP2: Self = Self::from_bitmask(0b000_011);
/// The 3 top pixels (F).
pub const TOP3: Self = Self::from_bitmask(0b000_111);
/// The 4 top pixels (N).
pub const TOP4: Self = Self::from_bitmask(0b001_111);
/// The 5 top pixels (^).
pub const TOP5: Self = Self::from_bitmask(0b011_111);
}
impl Display for SixelChar {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult<()> {
let mut buf = [0u8; 16];
let c = self.as_char();
let m = self.as_bitmask();
let b = self.to_braille();
f.write_str(format_buf!(&mut buf, "[{c}{b}0b_{m:06b}]").unwrap())
}
}
impl TextLut {
/// Lookup table for fast conversion from sixel bitmask to I-Ching hexagram Unicode scalar.
///
/// - Standard mapping: filled pixels represent broken lines.
#[rustfmt::skip]
// size: 256 bytes
const SIXEL_TO_CHAR_HEXAGRAM: [char; 64] = [
'\u{4DC0}','\u{4DEA}','\u{4DCD}','\u{4DE1}','\u{4DC8}','\u{4DC4}','\u{4DD9}','\u{4DCA}',
'\u{4DC9}','\u{4DF9}','\u{4DE5}','\u{4DF5}','\u{4DFC}','\u{4DFA}','\u{4DE8}','\u{4DD2}',
'\u{4DCC}','\u{4DF0}','\u{4DDD}','\u{4DF6}','\u{4DE4}','\u{4DFE}','\u{4DD5}','\u{4DE3}',
'\u{4DD8}','\u{4DD0}','\u{4DD4}','\u{4DF2}','\u{4DE9}','\u{4DC2}','\u{4DDA}','\u{4DD7}',
'\u{4DEB}','\u{4DDB}','\u{4DF1}','\u{4DDF}','\u{4DF8}','\u{4DEF}','\u{4DD1}','\u{4DED}',
'\u{4DC5}','\u{4DEE}','\u{4DFF}','\u{4DE7}','\u{4DFB}','\u{4DDC}','\u{4DC3}','\u{4DC6}',
'\u{4DE0}','\u{4DDE}','\u{4DF7}','\u{4DFD}','\u{4DF4}','\u{4DE6}','\u{4DF3}','\u{4DCE}',
'\u{4DCB}','\u{4DEC}','\u{4DE2}','\u{4DCF}','\u{4DD3}','\u{4DC7}','\u{4DD6}','\u{4DC1}',
];
/// Lookup table for fast conversion from I-Ching hexagram Unicode scalar to sixel bitmask.
///
/// - Standard mapping: filled pixels represent broken lines.
///
/// You have to subtract 0x4dc0 from char to get the index array.
#[rustfmt::skip]
// size: 64 bytes
const CHAR_HEXAGRAM_TO_SIXEL: [u8; 64] = [
// NEW ORDER, REVERSING THE BITS to have the correct top/down order
0b_000000, 0b_111111, 0b_011101, 0b_101110, 0b_000101, 0b_101000, 0b_101111, 0b_111101,
0b_000100, 0b_001000, 0b_000111, 0b_111000, 0b_010000, 0b_000010, 0b_110111, 0b_111011,
0b_011001, 0b_100110, 0b_001111, 0b_111100, 0b_011010, 0b_010110, 0b_111110, 0b_011111,
0b_011000, 0b_000110, 0b_011110, 0b_100001, 0b_101101, 0b_010010, 0b_110001, 0b_100011,
0b_110000, 0b_000011, 0b_111010, 0b_010111, 0b_010100, 0b_001010, 0b_110101, 0b_101011,
0b_001110, 0b_011100, 0b_000001, 0b_100000, 0b_111001, 0b_100111, 0b_101001, 0b_100101,
0b_010001, 0b_100010, 0b_011011, 0b_110110, 0b_110100, 0b_001011, 0b_010011, 0b_110010,
0b_100100, 0b_001001, 0b_101100, 0b_001101, 0b_001100, 0b_110011, 0b_010101, 0b_101010,
];
#[cfg(test)]
const _SIXEL_HEXAGRAM_CORRESPONDANCE: () = {
let mut index = 0;
while index < 64 {
let bitmask = TextLut::CHAR_HEXAGRAM_TO_SIXEL[index];
let char_from_bitmask = TextLut::SIXEL_TO_CHAR_HEXAGRAM[bitmask as usize];
let char_code = char_from_bitmask as u32;
let index_from_char = (char_code - 0x4DC0) as usize;
// Verify that SIXEL_TO_CHAR_HEXAGRAM[bitmask] gives a character
// whose code point corresponds to the original index
assert!(index_from_char == index);
// Verify that CHAR_HEXAGRAM_TO_SIXEL[index_from_char] gives back the original bitmask
let roundtrip_bitmask = TextLut::CHAR_HEXAGRAM_TO_SIXEL[index_from_char];
assert!(roundtrip_bitmask == bitmask);
index += 1;
}
};
}
#[cfg(test)]
mod tests {
// use super::*;
//
// #[test]
// fn sixel_char() {
// }
}