1use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub struct Character {
29 pub nickname: String,
31 pub emoji: String,
33 pub palette: Palette,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44pub struct Palette {
45 pub primary_hex: String,
47 pub accent_hex: String,
49 pub ansi256_primary: u8,
51 pub ansi256_accent: u8,
53}
54
55impl Character {
56 pub fn from_did(did: &str) -> Self {
62 Self::from_seed(did.as_bytes())
63 }
64
65 pub fn from_card(card: &serde_json::Value) -> Self {
88 let did_opt = card.get("did").and_then(|d| d.as_str());
89 let did = match did_opt {
90 Some(d) if !d.is_empty() => d,
91 _ => return Self::unknown_peer(),
92 };
93 let display = card.get("display").and_then(|d| d.as_object());
94 let nick = display
95 .and_then(|d| d.get("nickname"))
96 .and_then(|n| n.as_str())
97 .map(sanitize_display_text)
98 .filter(|s| !s.is_empty());
99 let emoji = display
100 .and_then(|d| d.get("emoji"))
101 .and_then(|e| e.as_str())
102 .map(sanitize_display_text)
103 .filter(|s| !s.is_empty());
104 Self::from_did_with_override(did, nick.as_deref(), emoji.as_deref())
105 }
106
107 fn unknown_peer() -> Self {
112 Self {
113 nickname: "unknown-peer".to_string(),
114 emoji: "❓".to_string(),
115 palette: Palette {
116 primary_hex: "#7d7d7d".to_string(),
117 accent_hex: "#a8a8a8".to_string(),
118 ansi256_primary: 244,
119 ansi256_accent: 248,
120 },
121 }
122 }
123
124 pub fn from_did_with_override(
133 did: &str,
134 nickname_override: Option<&str>,
135 emoji_override: Option<&str>,
136 ) -> Self {
137 let auto = Self::from_did(did);
138 Self {
139 nickname: nickname_override
140 .filter(|s| !s.is_empty())
141 .map(str::to_string)
142 .unwrap_or(auto.nickname),
143 emoji: emoji_override
144 .filter(|s| !s.is_empty())
145 .map(str::to_string)
146 .unwrap_or(auto.emoji),
147 palette: auto.palette,
148 }
149 }
150
151 pub fn from_seed(seed: &[u8]) -> Self {
157 let mut h = Sha256::new();
158 h.update(seed);
159 let digest = h.finalize();
160 let adj_idx =
163 u32::from_be_bytes(digest[0..4].try_into().unwrap()) as usize % ADJECTIVES.len();
164 let noun_idx = u32::from_be_bytes(digest[4..8].try_into().unwrap()) as usize % NOUNS.len();
165 let emoji_idx =
166 u32::from_be_bytes(digest[8..12].try_into().unwrap()) as usize % EMOJIS.len();
167 let hue_raw = u32::from_be_bytes(digest[12..16].try_into().unwrap());
169 let hue_deg = (hue_raw % 3600) as f32 / 10.0; let sat = 0.55 + (digest[16] as f32 / 255.0) * 0.25; let light = 0.50 + (digest[17] as f32 / 255.0) * 0.15; let accent_hue_deg = (hue_deg + 30.0) % 360.0;
173 let accent_light = 0.65 + (digest[18] as f32 / 255.0) * 0.15; let (pr, pg, pb) = hsl_to_rgb(hue_deg, sat, light);
176 let (ar, ag, ab) = hsl_to_rgb(accent_hue_deg, sat, accent_light);
177
178 Self {
179 nickname: format!("{}-{}", ADJECTIVES[adj_idx], NOUNS[noun_idx]),
180 emoji: EMOJIS[emoji_idx].to_string(),
181 palette: Palette {
182 primary_hex: format!("#{:02x}{:02x}{:02x}", pr, pg, pb),
183 accent_hex: format!("#{:02x}{:02x}{:02x}", ar, ag, ab),
184 ansi256_primary: rgb_to_ansi256(pr, pg, pb),
185 ansi256_accent: rgb_to_ansi256(ar, ag, ab),
186 },
187 }
188 }
189
190 pub fn short(&self) -> String {
192 format!("{} {}", self.emoji, self.nickname)
193 }
194
195 pub fn colored(&self) -> String {
200 format!(
201 "\x1b[38;5;{}m{} {}\x1b[0m",
202 self.palette.ansi256_primary, self.emoji, self.nickname
203 )
204 }
205}
206
207pub const MAX_DISPLAY_CHARS: usize = 64;
221
222pub fn sanitize_display_text(s: &str) -> String {
223 s.chars()
224 .filter(|c| !c.is_control())
225 .take(MAX_DISPLAY_CHARS)
226 .collect()
227}
228
229fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
232 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
233 let h_prime = h / 60.0;
234 let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
235 let (r1, g1, b1) = match h_prime as i32 {
236 0 => (c, x, 0.0),
237 1 => (x, c, 0.0),
238 2 => (0.0, c, x),
239 3 => (0.0, x, c),
240 4 => (x, 0.0, c),
241 _ => (c, 0.0, x),
242 };
243 let m = l - c / 2.0;
244 let r = ((r1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
245 let g = ((g1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
246 let b = ((b1 + m) * 255.0).round().clamp(0.0, 255.0) as u8;
247 (r, g, b)
248}
249
250fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
252 let q = |c: u8| -> u8 { ((c as u16 * 5 + 127) / 255) as u8 }; 16 + 36 * q(r) + 6 * q(g) + q(b)
254}
255
256const ADJECTIVES: &[&str] = &[
260 "agate",
261 "alpine",
262 "amber",
263 "ancient",
264 "antique",
265 "arctic",
266 "ashen",
267 "auburn",
268 "autumn",
269 "azure",
270 "balmy",
271 "blithe",
272 "brave",
273 "breezy",
274 "briar",
275 "bright",
276 "brisk",
277 "bronze",
278 "brushed",
279 "bubbling",
280 "burnished",
281 "calm",
282 "candle",
283 "cedar",
284 "chestnut",
285 "chill",
286 "chipper",
287 "cinder",
288 "clay",
289 "clear",
290 "cliffside",
291 "cobalt",
292 "copper",
293 "coral",
294 "cordial",
295 "cosmic",
296 "crimson",
297 "crisp",
298 "crystal",
299 "curious",
300 "dapper",
301 "dappled",
302 "dawn",
303 "daydream",
304 "deep",
305 "delta",
306 "dewy",
307 "distant",
308 "drift",
309 "drowsy",
310 "dune",
311 "dusky",
312 "eager",
313 "echoing",
314 "ember",
315 "emerald",
316 "feral",
317 "ferny",
318 "festive",
319 "fjord",
320 "flaxen",
321 "fluted",
322 "fond",
323 "forest",
324 "foxtrot",
325 "fragrant",
326 "frosted",
327 "frosty",
328 "garnet",
329 "gentle",
330 "ginger",
331 "glacial",
332 "glassy",
333 "gleaming",
334 "glint",
335 "glossy",
336 "gold",
337 "graceful",
338 "granite",
339 "grove",
340 "hammered",
341 "harbor",
342 "hardy",
343 "hazel",
344 "heath",
345 "honey",
346 "humble",
347 "hush",
348 "indigo",
349 "ivory",
350 "jade",
351 "jaunty",
352 "juniper",
353 "keen",
354 "kelp",
355 "kindly",
356 "knit",
357 "lacquered",
358 "lapis",
359 "lavender",
360 "leaden",
361 "lichen",
362 "lilac",
363 "linen",
364 "lively",
365 "lonely",
366 "lucid",
367 "lunar",
368 "marble",
369 "marsh",
370 "meadow",
371 "mellow",
372 "merry",
373 "mild",
374 "minted",
375 "misted",
376 "misty",
377 "moonlit",
378 "morning",
379 "mossy",
380 "muted",
381 "neon",
382 "nimble",
383 "noble",
384 "north",
385 "ochre",
386 "olive",
387 "onyx",
388 "opal",
389 "orchid",
390 "outback",
391 "pearl",
392 "pearled",
393 "peat",
394 "petal",
395 "pewter",
396 "pine",
397 "placid",
398 "plucky",
399 "plum",
400 "polar",
401 "polished",
402 "poppy",
403 "prairie",
404 "primrose",
405 "prussian",
406 "purpled",
407 "quartz",
408 "quiet",
409 "quill",
410 "raven",
411 "reckless",
412 "redwood",
413 "restless",
414 "ribbon",
415 "river",
416 "rosemary",
417 "rosy",
418 "ruby",
419 "rusted",
420 "rustic",
421 "russet",
422 "saffron",
423 "sage",
424 "salt",
425 "sandy",
426 "satin",
427 "scarlet",
428 "sea",
429 "shaded",
430 "shadow",
431 "shimmer",
432 "shining",
433 "shore",
434 "silken",
435 "silver",
436 "skylit",
437 "slate",
438 "slatey",
439 "smoky",
440 "smolder",
441 "snowy",
442 "soft",
443 "solar",
444 "splendid",
445 "spruce",
446 "starry",
447 "steadfast",
448 "steady",
449 "sterling",
450 "stone",
451 "sturdy",
452 "sublime",
453 "summer",
454 "sunlit",
455 "sunny",
456 "supple",
457 "sweetbay",
458 "swift",
459 "tawny",
460 "teal",
461 "tender",
462 "terra",
463 "thistle",
464 "thrushlike",
465 "tidal",
466 "tinder",
467 "tinkling",
468 "topaz",
469 "torrid",
470 "tranquil",
471 "trillium",
472 "twilight",
473 "umber",
474 "valley",
475 "velvet",
476 "vernal",
477 "verdant",
478 "vesper",
479 "vibrant",
480 "violet",
481 "vivid",
482 "warm",
483 "warmer",
484 "weathered",
485 "westwind",
486 "whispered",
487 "wildflower",
488 "willow",
489 "winterly",
490 "windy",
491 "winter",
492 "wisp",
493 "withered",
494 "witty",
495 "woodland",
496 "woven",
497 "wren",
498 "wry",
499 "yarrow",
500 "yonder",
501 "zen",
502 "zephyr",
503];
504
505const NOUNS: &[&str] = &[
509 "anchor",
510 "ash",
511 "aspen",
512 "atlas",
513 "aurora",
514 "badger",
515 "bark",
516 "bay",
517 "bayou",
518 "beacon",
519 "beaver",
520 "bell",
521 "birch",
522 "bison",
523 "blossom",
524 "bough",
525 "branch",
526 "breeze",
527 "briar",
528 "brook",
529 "bud",
530 "bunting",
531 "burrow",
532 "butte",
533 "caldera",
534 "camellia",
535 "canyon",
536 "cardinal",
537 "caribou",
538 "cedar",
539 "chime",
540 "chinook",
541 "cinder",
542 "cirrus",
543 "cliff",
544 "cloudburst",
545 "comet",
546 "compass",
547 "copper",
548 "coral",
549 "cove",
550 "creek",
551 "crest",
552 "cricket",
553 "crow",
554 "cumulus",
555 "cyclone",
556 "cypress",
557 "dale",
558 "delta",
559 "dew",
560 "dewdrop",
561 "dolphin",
562 "dove",
563 "dragonfly",
564 "dune",
565 "dusk",
566 "eagle",
567 "ember",
568 "fern",
569 "field",
570 "finch",
571 "fjord",
572 "flame",
573 "flax",
574 "fleck",
575 "fog",
576 "foam",
577 "forest",
578 "fox",
579 "frost",
580 "garnet",
581 "gander",
582 "geode",
583 "geyser",
584 "glade",
585 "gleam",
586 "glen",
587 "glimmer",
588 "gorge",
589 "grove",
590 "hare",
591 "harbor",
592 "haze",
593 "headland",
594 "hearth",
595 "heath",
596 "heron",
597 "hollow",
598 "hummingbird",
599 "ibis",
600 "iris",
601 "ivy",
602 "jasmine",
603 "jasper",
604 "jay",
605 "juniper",
606 "kelp",
607 "kestrel",
608 "kettle",
609 "kingfisher",
610 "knoll",
611 "lagoon",
612 "lake",
613 "lantern",
614 "lark",
615 "laurel",
616 "leaf",
617 "leaflet",
618 "ledge",
619 "lichen",
620 "lily",
621 "linden",
622 "lion",
623 "loft",
624 "loon",
625 "lotus",
626 "lupin",
627 "lynx",
628 "magnolia",
629 "magpie",
630 "maple",
631 "marmot",
632 "marsh",
633 "meadow",
634 "meadowlark",
635 "mesa",
636 "mist",
637 "moor",
638 "moss",
639 "moth",
640 "mountain",
641 "narwhal",
642 "nettle",
643 "nightingale",
644 "nimbus",
645 "oak",
646 "ocean",
647 "ochre",
648 "opal",
649 "orchard",
650 "orchid",
651 "oriole",
652 "otter",
653 "owl",
654 "palm",
655 "pelican",
656 "petal",
657 "pine",
658 "pinion",
659 "plain",
660 "plateau",
661 "plover",
662 "pond",
663 "poppy",
664 "prairie",
665 "prism",
666 "puffin",
667 "quartz",
668 "quill",
669 "raindrop",
670 "rapid",
671 "raven",
672 "ravine",
673 "redwood",
674 "reef",
675 "ridge",
676 "river",
677 "robin",
678 "rook",
679 "rosemary",
680 "rowan",
681 "sable",
682 "saffron",
683 "sage",
684 "salmon",
685 "sand",
686 "sandbar",
687 "sandpiper",
688 "sapling",
689 "savanna",
690 "scrub",
691 "sea",
692 "shadow",
693 "shale",
694 "shard",
695 "sheaf",
696 "shore",
697 "shrub",
698 "sky",
699 "slate",
700 "snowdrop",
701 "snowfall",
702 "snowflake",
703 "sparrow",
704 "spindle",
705 "spire",
706 "spring",
707 "sprout",
708 "spruce",
709 "squall",
710 "starling",
711 "starshine",
712 "steppe",
713 "stone",
714 "stratus",
715 "summit",
716 "swallow",
717 "swift",
718 "tarn",
719 "tern",
720 "thaw",
721 "thicket",
722 "thistle",
723 "thrush",
724 "tide",
725 "tideline",
726 "tinder",
727 "topaz",
728 "tournesol",
729 "trillium",
730 "trout",
731 "tundra",
732 "twilight",
733 "twig",
734 "valley",
735 "vesper",
736 "vine",
737 "violet",
738 "wallaby",
739 "warbler",
740 "wave",
741 "weasel",
742 "whirlwind",
743 "willow",
744 "wisp",
745 "wolf",
746 "wood",
747 "wren",
748 "yarrow",
749 "yew",
750 "zephyr",
751];
752
753const EMOJIS: &[&str] = &[
761 "🦊", "🐺", "🐻", "🐅", "🐆", "🦓", "🦒", "🦌", "🦘", "🐇", "🦔", "🦣", "🦏", "🐈", "🐱", "🐶",
763 "🐰", "🦦", "🦥", "🦡", "🦨", "🦄", "🐴", "🐗", "🐘", "🦬", "🦫", "🐪", "🦙", "🐭", "🐹", "🐀",
764 "🦅", "🦉", "🦢", "🦩", "🐧", "🦃", "🦚", "🦜", "🦤", "🦆", "🐓", "🐔", "🐦", "🪶",
766 "🐊", "🦎", "🐍", "🐢", "🦕", "🦖", "🐸", "🐙", "🐬", "🐳", "🐋", "🐡", "🦈", "🦭", "🐟", "🐠", "🦀", "🦞", "🦐", "🐚",
769 "🐝", "🦋", "🐌", "🐞", "🦗", "🕷", "🦂", "🌲", "🌳", "🌴", "🌵", "🌱", "🌿", "🍃", "🍀", "🍁", "🍂", "🌷", "🌸", "🌺", "🌻", "🌼", "🌹", "🪻", "🪷", "🍄", "🍇", "🍈", "🍉", "🍊", "🍋", "🍌", "🍍", "🥭", "🍎", "🍏", "🍐", "🍑", "🍒", "🍓", "🫐", "🥝",
774 "🌊", "🌋", "🌙", "🌟", "🌈", "🔥", "❄", "💧", "⚡", "☀", "☁", "⛄",
776 "💎", "🪄", "🔮", "🧿", "🌠", "🎵", "🎶", "🎷", "🎸", "🎹", "🎺", "🎻", "🥁", "🪕", "🪈", "⚓", "🧭", "🏺", "🪴", "🗿", "🛡", "🗝", "🎲", "🎭", "🎨", "🎯", "🪐",
780];
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785 use serde_json::json;
786 use std::collections::HashSet;
787
788 #[test]
789 fn deterministic_same_did() {
790 let a = Character::from_did("did:wire:paul-a1b2c3d4");
791 let b = Character::from_did("did:wire:paul-a1b2c3d4");
792 assert_eq!(a, b);
793 }
794
795 #[test]
796 fn different_dids_differ() {
797 let a = Character::from_did("did:wire:paul-a1b2c3d4");
798 let b = Character::from_did("did:wire:paul-e5f6a7b8");
799 assert_ne!(a, b);
800 }
801
802 #[test]
803 fn nickname_is_hyphenated_pair() {
804 let c = Character::from_did("did:wire:test-deadbeef");
805 let parts: Vec<&str> = c.nickname.split('-').collect();
806 assert_eq!(parts.len(), 2);
807 assert!(parts[0].chars().all(|ch| ch.is_ascii_lowercase()));
808 assert!(parts[1].chars().all(|ch| ch.is_ascii_lowercase()));
809 }
810
811 #[test]
812 fn emoji_is_in_curated_set() {
813 let c = Character::from_did("did:wire:test-cafebabe");
814 assert!(EMOJIS.contains(&c.emoji.as_str()));
815 }
816
817 #[test]
818 fn palette_hex_is_well_formed() {
819 let c = Character::from_did("did:wire:test-12345678");
820 assert!(c.palette.primary_hex.starts_with('#'));
821 assert_eq!(c.palette.primary_hex.len(), 7);
822 assert!(c.palette.accent_hex.starts_with('#'));
823 assert_eq!(c.palette.accent_hex.len(), 7);
824 }
825
826 #[test]
827 fn ansi256_in_cube_range() {
828 let c = Character::from_did("did:wire:test-87654321");
829 assert!((16..=231).contains(&c.palette.ansi256_primary));
830 assert!((16..=231).contains(&c.palette.ansi256_accent));
831 }
832
833 #[test]
834 fn short_format() {
835 let c = Character::from_did("did:wire:fixed-seed-here");
836 let short = c.short();
837 assert!(short.contains(&c.emoji));
838 assert!(short.contains(&c.nickname));
839 assert_eq!(short, format!("{} {}", c.emoji, c.nickname));
840 }
841
842 #[test]
843 fn colored_includes_ansi_escape() {
844 let c = Character::from_did("did:wire:colored-test");
845 let colored = c.colored();
846 assert!(colored.starts_with("\x1b[38;5;"));
847 assert!(colored.ends_with("\x1b[0m"));
848 assert!(colored.contains(&c.nickname));
849 }
850
851 #[test]
852 fn no_nickname_collisions_10k_samples() {
853 let mut chars: HashSet<(String, String, String)> = HashSet::new();
858 let mut collisions = 0;
859 for i in 0..10_000 {
860 let did = format!("did:wire:test-{:08x}", i);
861 let c = Character::from_did(&did);
862 let key = (
863 c.nickname.clone(),
864 c.emoji.clone(),
865 c.palette.primary_hex.clone(),
866 );
867 if !chars.insert(key) {
868 collisions += 1;
869 }
870 }
871 assert!(
872 collisions < 100,
873 "saw {collisions} character-triple collisions in 10k samples (>1%)"
874 );
875 }
876
877 #[test]
878 fn word_lists_have_expected_size() {
879 assert!(ADJECTIVES.len() >= 100, "adjective list too small");
880 assert!(NOUNS.len() >= 100, "noun list too small");
881 assert!(EMOJIS.len() >= 50, "emoji list too small");
882 }
883
884 #[test]
885 fn no_duplicate_words() {
886 let adj_set: HashSet<&&str> = ADJECTIVES.iter().collect();
887 assert_eq!(adj_set.len(), ADJECTIVES.len(), "duplicate adjective");
888 let noun_set: HashSet<&&str> = NOUNS.iter().collect();
889 assert_eq!(noun_set.len(), NOUNS.len(), "duplicate noun");
890 let emoji_set: HashSet<&&str> = EMOJIS.iter().collect();
891 assert_eq!(emoji_set.len(), EMOJIS.len(), "duplicate emoji");
892 }
893
894 #[test]
895 fn hsl_to_rgb_known_values() {
896 let (r, g, b) = hsl_to_rgb(0.0, 1.0, 0.5);
898 assert_eq!(r, 255);
899 assert_eq!(g, 0);
900 assert_eq!(b, 0);
901 let (r, g, b) = hsl_to_rgb(120.0, 1.0, 0.5);
903 assert_eq!(r, 0);
904 assert_eq!(g, 255);
905 assert_eq!(b, 0);
906 let (r, g, b) = hsl_to_rgb(240.0, 1.0, 0.5);
908 assert_eq!(r, 0);
909 assert_eq!(g, 0);
910 assert_eq!(b, 255);
911 }
912
913 #[test]
914 fn sanitize_strips_ansi_escape() {
915 let out = sanitize_display_text("\x1b]0;owned\x07");
920 assert!(!out.contains('\x1b'), "ESC must be stripped: {out:?}");
921 assert!(!out.contains('\x07'), "BEL must be stripped: {out:?}");
922 assert_eq!(out, "]0;owned");
924 let out2 = sanitize_display_text("\x1b[2J\x1b[H");
926 assert!(!out2.contains('\x1b'));
927 assert_eq!(out2, "[2J[H");
928 assert_eq!(sanitize_display_text("hello\nworld"), "helloworld");
930 assert_eq!(sanitize_display_text("a\tb\x7fc"), "abc");
931 }
932
933 #[test]
934 fn sanitize_preserves_unicode_emoji_and_text() {
935 assert_eq!(
936 sanitize_display_text("🦊 foxtrot-meadow"),
937 "🦊 foxtrot-meadow"
938 );
939 assert_eq!(sanitize_display_text("café résumé"), "café résumé");
940 }
941
942 #[test]
943 fn sanitize_caps_length() {
944 let long = "a".repeat(200);
945 let out = sanitize_display_text(&long);
946 assert_eq!(out.chars().count(), MAX_DISPLAY_CHARS);
947 }
948
949 #[test]
950 fn from_card_with_empty_did_returns_unknown_sentinel() {
951 let card = json!({"handle": "broken"});
955 let c = Character::from_card(&card);
956 assert_eq!(c.nickname, "unknown-peer");
957 assert_eq!(c.emoji, "❓");
958 }
959
960 #[test]
961 fn from_card_with_null_did_returns_unknown_sentinel() {
962 let card = json!({"did": null, "handle": "broken"});
963 let c = Character::from_card(&card);
964 assert_eq!(c.nickname, "unknown-peer");
965 }
966
967 #[test]
968 fn from_card_strips_escape_from_published_nickname() {
969 let card = json!({
974 "did": "did:wire:malicious-deadbeef",
975 "display": {"nickname": "\x1b]0;OWNED\x07evil", "emoji": "🦊"},
976 });
977 let c = Character::from_card(&card);
978 assert!(!c.nickname.contains('\x1b'));
980 assert!(!c.nickname.contains('\x07'));
981 assert!(c.nickname.contains("OWNED")); assert_eq!(c.emoji, "🦊");
983 }
984
985 #[test]
986 fn from_card_with_published_override_uses_it() {
987 let card = json!({
988 "did": "did:wire:friend-12345678",
989 "display": {"nickname": "the-forge", "emoji": "🔨"},
990 });
991 let c = Character::from_card(&card);
992 assert_eq!(c.nickname, "the-forge");
993 assert_eq!(c.emoji, "🔨");
994 }
995
996 #[test]
997 fn from_card_without_display_falls_back_to_did() {
998 let card = json!({"did": "did:wire:friend-12345678"});
999 let c = Character::from_card(&card);
1000 let auto = Character::from_did("did:wire:friend-12345678");
1001 assert_eq!(c, auto);
1002 }
1003
1004 #[test]
1005 fn rgb_to_ansi256_matches_cube() {
1006 assert_eq!(rgb_to_ansi256(0, 0, 0), 16);
1008 assert_eq!(rgb_to_ansi256(255, 255, 255), 231);
1009 assert_eq!(rgb_to_ansi256(255, 0, 0), 196);
1011 assert_eq!(rgb_to_ansi256(0, 255, 0), 46);
1013 }
1014}