use std::borrow::Cow;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::Widget,
};
use crate::tui::theme::solarized;
const PIRATE_EMOJIS: &[&str] = &[
"🏴☠️",
"🐙",
"🐳",
"🌊",
"🦈",
"⚓",
"🗡️",
"💀",
"🦜",
"🏝️",
"🧭",
"⛵",
"🦑",
"🐚",
"🪸",
];
const COSMIC_EMOJIS: &[&str] = &[
"🪐", "🌌", "🛰️", "🌙", "✨", "🔮", "🌟", "☄️", "🚀", "👽", "🛸", "⭐", "💫", "🌠", "🔭",
];
const UNICORN_EMOJIS: &[&str] = &[
"🦄", "🌈", "🧚", "🦋", "✨", "💖", "🌸", "🎀", "💫", "🍭", "🎠", "💜", "🩷", "🪷", "🦩",
];
const ANIMAL_EMOJIS: &[&str] = &[
"🐔",
"🐤",
"🦙",
"🐭",
"🐉",
"🦖",
"🦀",
"🦞",
"🦁",
"🐦🔥",
"🦆",
"🦦",
"🐒",
"🦊",
"🦚",
"🪽",
"🐿️",
"🦝",
"🐸",
"🦎",
];
const FOOD_EMOJIS: &[&str] = &[
"🧀", "🍕", "🍺", "🍸", "🍻", "🥂", "🍣", "🌮", "🥦", "🌵", "🌴", "🧃", "🍜", "🥖", "🥐", "🌶️",
"🍩", "🧁", "🍰", "🎂",
];
const SPECIAL_EMOJIS: &[&str] = &[
"💎", "🦾", "⚡", "👺", "🫀", "🗿", "⛩️", "🍀", "🎅", "🎨", "🔥", "💀", "👾", "🤖", "🧠", "🎯",
"🎪", "🎭", "🏆", "🎖️",
];
const TECH_EMOJIS: &[&str] = &[
"🔌", "💾", "📡", "🖥️", "⚙️", "🔧", "🛠️", "📟", "💿", "🔋", "📶", "🖨️", "⌨️", "🖱️", "💽",
];
const NATURE_EMOJIS: &[&str] = &[
"🌵", "🌴", "🥦", "🍀", "🌿", "🍃", "🌱", "🌲", "🌳", "🍂", "🍁", "🌾", "🪴", "🎋", "🎍",
];
const MATRIX_KATAKANA: &[char] = &[
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ',
'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ',
'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ン',
];
const KANJI_CHARS: &[char] = &[
'自', '由', 'ニ', 'カ', '海', '賊', '風', '雷', '火', '水', '光', '闇', '星', '月', '龍', '虎',
];
const NERD_REFS: &[&str] = &[
"42", "EOF", "NULL", "sudo", "git", "0x", "fn", "async", "OK", "ACK", "SYN", "NaN", "π", "∞", "λ", "Ω", ];
const CYCLING_WORDS: &[&str] = &[
"Nika", "Freedom", "Liberté", "自由", "LOVE", "PEACE", "CODE", "RUST", "ASYNC", "MCP", "DAG",
"YAML", "ARR", "AHOY", "MAGIC",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DecryptVerb {
#[default]
Infer,
Exec,
Fetch,
Invoke,
Agent,
Chat,
}
impl DecryptVerb {
pub fn color(&self) -> Color {
match self {
DecryptVerb::Infer => solarized::VIOLET,
DecryptVerb::Exec => solarized::GREEN,
DecryptVerb::Fetch => solarized::CYAN,
DecryptVerb::Invoke => solarized::BLUE,
DecryptVerb::Agent => solarized::YELLOW,
DecryptVerb::Chat => solarized::BASE0,
}
}
pub fn emojis(&self) -> &'static [&'static str] {
match self {
DecryptVerb::Infer => COSMIC_EMOJIS,
DecryptVerb::Exec => NATURE_EMOJIS,
DecryptVerb::Fetch => PIRATE_EMOJIS,
DecryptVerb::Invoke => TECH_EMOJIS,
DecryptVerb::Agent => ANIMAL_EMOJIS,
DecryptVerb::Chat => SPECIAL_EMOJIS,
}
}
pub fn icon(&self) -> &'static str {
match self {
DecryptVerb::Infer => "⚡",
DecryptVerb::Exec => "📟",
DecryptVerb::Fetch => "🛰️",
DecryptVerb::Invoke => "🔌",
DecryptVerb::Agent => "🐔",
DecryptVerb::Chat => "💬",
}
}
}
#[derive(Clone)]
struct CharState {
target: char,
current: DecryptGlyph,
progress: f32,
seed: u64,
}
#[derive(Clone)]
enum DecryptGlyph {
Char(char),
Emoji(&'static str),
Kanji(char),
Text(&'static str),
}
impl DecryptGlyph {
fn as_str(&self) -> Cow<'static, str> {
match self {
DecryptGlyph::Char(c) => Cow::Owned(c.to_string()),
DecryptGlyph::Emoji(e) => Cow::Borrowed(*e),
DecryptGlyph::Kanji(k) => Cow::Owned(k.to_string()),
DecryptGlyph::Text(t) => Cow::Borrowed(*t),
}
}
fn display_width(&self) -> usize {
match self {
DecryptGlyph::Char(_) => 1,
DecryptGlyph::Emoji(_) => 2,
DecryptGlyph::Kanji(_) => 2,
DecryptGlyph::Text(t) => t.chars().count(),
}
}
}
pub struct MatrixDecrypt<'a> {
text: &'a str,
progress: f32,
frame: u8,
verb: DecryptVerb,
seed: u64,
with_rare_emojis: bool,
with_kanji: bool,
speed: f32,
}
impl<'a> Default for MatrixDecrypt<'a> {
fn default() -> Self {
Self {
text: "",
progress: 0.0,
frame: 0,
verb: DecryptVerb::default(),
seed: 42,
with_rare_emojis: true,
with_kanji: true,
speed: 1.0,
}
}
}
impl<'a> MatrixDecrypt<'a> {
pub fn new(text: &'a str) -> Self {
Self {
text,
..Default::default()
}
}
pub fn text(mut self, text: &'a str) -> Self {
self.text = text;
self
}
pub fn progress(mut self, progress: f32) -> Self {
self.progress = progress.clamp(0.0, 1.0);
self
}
pub fn frame(mut self, frame: u8) -> Self {
self.frame = frame;
self
}
pub fn verb(mut self, verb: DecryptVerb) -> Self {
self.verb = verb;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn with_rare_emojis(mut self, enable: bool) -> Self {
self.with_rare_emojis = enable;
self
}
pub fn with_kanji(mut self, enable: bool) -> Self {
self.with_kanji = enable;
self
}
pub fn speed(mut self, speed: f32) -> Self {
self.speed = speed.max(0.1);
self
}
fn random_glyph(&self, rng: &mut SmallRng) -> DecryptGlyph {
let roll: f32 = rng.gen();
if roll < 0.35 {
let chars: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '_', '<', '>', '[', ']', '{', '}', '/', '*', '#', '@', '^',
];
let idx = rng.gen_range(0..chars.len());
DecryptGlyph::Char(chars[idx])
} else if roll < 0.80 {
let idx = rng.gen_range(0..MATRIX_KATAKANA.len());
DecryptGlyph::Char(MATRIX_KATAKANA[idx])
} else if roll < 0.90 && self.with_kanji {
let idx = rng.gen_range(0..KANJI_CHARS.len());
DecryptGlyph::Kanji(KANJI_CHARS[idx])
} else if roll < 0.98 {
let idx = rng.gen_range(0..NERD_REFS.len());
DecryptGlyph::Text(NERD_REFS[idx])
} else if self.with_rare_emojis {
const NIKA_MASCOTS: &[&str] = &["🐔", "⚡", "📟", "🛰️", "🔌", "🦋"];
let idx = rng.gen_range(0..NIKA_MASCOTS.len());
DecryptGlyph::Emoji(NIKA_MASCOTS[idx])
} else {
let idx = rng.gen_range(0..MATRIX_KATAKANA.len());
DecryptGlyph::Char(MATRIX_KATAKANA[idx])
}
}
fn should_reveal(&self, char_index: usize, total_chars: usize, rng: &mut SmallRng) -> bool {
if total_chars == 0 {
return true;
}
let position_factor = char_index as f32 / total_chars as f32;
let random_offset: f32 = rng.gen_range(-0.15..0.15);
let threshold = (position_factor + random_offset).clamp(0.0, 1.0);
self.progress * self.speed > threshold
}
fn build_line(&self) -> Line<'a> {
let mut rng = SmallRng::seed_from_u64(self.seed.wrapping_add(self.frame as u64));
let chars: Vec<char> = self.text.chars().collect();
let total = chars.len();
let mut spans = Vec::new();
let base_color = self.verb.color();
for (i, &ch) in chars.iter().enumerate() {
let char_seed = self.seed.wrapping_add(i as u64 * 31337);
let mut char_rng = SmallRng::seed_from_u64(char_seed.wrapping_add(self.frame as u64));
if self.should_reveal(i, total, &mut rng) {
let style = Style::default().fg(base_color);
spans.push(Span::styled(ch.to_string(), style));
} else {
let glyph = self.random_glyph(&mut char_rng);
let chaos_color = self.chaos_color(&mut char_rng);
let style = Style::default().fg(chaos_color);
spans.push(Span::styled(glyph.as_str(), style));
}
}
Line::from(spans)
}
fn chaos_color(&self, rng: &mut SmallRng) -> Color {
let colors = [
solarized::CYAN,
solarized::GREEN,
solarized::YELLOW,
solarized::ORANGE,
solarized::MAGENTA,
solarized::VIOLET,
solarized::BLUE,
solarized::RED,
];
colors[rng.gen_range(0..colors.len())]
}
}
impl Widget for MatrixDecrypt<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 || self.text.is_empty() {
return;
}
let line = self.build_line();
let x = area.x;
let y = area.y;
buf.set_line(x, y, &line, area.width);
}
}
#[derive(Debug, Clone)]
pub struct StreamingDecrypt {
text: String,
char_progress: Vec<f32>,
char_birth_frames: Vec<u8>,
frame: u8,
verb: DecryptVerb,
seed: u64,
reveal_speed: f32,
wave_factor: f32,
initial_chaos_frames: u8,
is_complete: bool,
}
impl Default for StreamingDecrypt {
fn default() -> Self {
Self {
text: String::new(),
char_progress: Vec::new(),
char_birth_frames: Vec::new(),
frame: 0,
verb: DecryptVerb::default(),
seed: 42,
reveal_speed: 0.025, wave_factor: 0.15, initial_chaos_frames: 8, is_complete: false,
}
}
}
impl StreamingDecrypt {
pub fn new() -> Self {
Self::default()
}
pub fn with_verb(mut self, verb: DecryptVerb) -> Self {
self.verb = verb;
self
}
pub fn with_reveal_speed(mut self, speed: f32) -> Self {
self.reveal_speed = speed.clamp(0.005, 1.0);
self
}
pub fn with_wave_factor(mut self, factor: f32) -> Self {
self.wave_factor = factor.clamp(0.0, 0.5);
self
}
pub fn with_initial_chaos(mut self, frames: u8) -> Self {
self.initial_chaos_frames = frames;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn push_text(&mut self, new_text: &str) {
for ch in new_text.chars() {
self.text.push(ch);
self.char_progress.push(0.0);
self.char_birth_frames.push(self.frame);
}
}
pub fn tick(&mut self) {
self.frame = self.frame.wrapping_add(1);
let total_chars = self.char_progress.len();
if total_chars == 0 {
return;
}
let mut all_revealed = true;
for (i, progress) in self.char_progress.iter_mut().enumerate() {
if *progress >= 1.0 {
continue;
}
let birth_frame = self.char_birth_frames.get(i).copied().unwrap_or(0);
let frames_alive = self.frame.wrapping_sub(birth_frame);
if frames_alive < self.initial_chaos_frames {
all_revealed = false;
continue;
}
let position_factor = i as f32 / total_chars.max(1) as f32;
let speed_multiplier = 1.0 - (position_factor * self.wave_factor);
let effective_speed = self.reveal_speed * speed_multiplier;
*progress = (*progress + effective_speed).min(1.0);
if *progress < 1.0 {
all_revealed = false;
}
}
self.is_complete = all_revealed && !self.text.is_empty();
}
pub fn reveal_all(&mut self) {
for progress in &mut self.char_progress {
*progress = 1.0;
}
self.is_complete = true;
}
pub fn is_complete(&self) -> bool {
self.is_complete
}
pub fn text(&self) -> &str {
&self.text
}
pub fn verb(&self) -> DecryptVerb {
self.verb
}
pub fn clear(&mut self) {
self.text.clear();
self.char_progress.clear();
self.char_birth_frames.clear();
self.frame = 0;
self.is_complete = false;
}
pub fn overall_progress(&self) -> f32 {
if self.char_progress.is_empty() {
return 1.0;
}
self.char_progress.iter().sum::<f32>() / self.char_progress.len() as f32
}
pub fn widget(&self) -> MatrixDecrypt<'_> {
MatrixDecrypt::new(&self.text)
.progress(self.overall_progress())
.frame(self.frame)
.verb(self.verb)
.seed(self.seed)
}
pub fn render(&self, area: Rect, buf: &mut Buffer) {
self.widget().render(area, buf);
}
pub fn build_line(&self) -> Line<'static> {
use rand::rngs::SmallRng;
use rand::SeedableRng;
let chars: Vec<char> = self.text.chars().collect();
let mut spans = Vec::new();
let base_color = self.verb.color();
for (i, &ch) in chars.iter().enumerate() {
let char_seed = self.seed.wrapping_add(i as u64 * 31337);
let mut char_rng = SmallRng::seed_from_u64(char_seed.wrapping_add(self.frame as u64));
let progress = self.char_progress.get(i).copied().unwrap_or(1.0);
if progress >= 1.0 {
let style = Style::default().fg(base_color);
spans.push(Span::styled(ch.to_string(), style));
} else {
let glyph = Self::random_glyph_static(&mut char_rng);
let colors = [
solarized::CYAN,
solarized::GREEN,
solarized::YELLOW,
solarized::ORANGE,
solarized::MAGENTA,
solarized::VIOLET,
solarized::BLUE,
solarized::RED,
];
let chaos_color = colors[char_rng.gen_range(0..colors.len())];
let style = Style::default().fg(chaos_color);
spans.push(Span::styled(glyph, style));
}
}
Line::from(spans)
}
pub fn build_lines(&self) -> Vec<Line<'static>> {
let lines_text: Vec<&str> = self.text.lines().collect();
let mut result = Vec::new();
let mut char_offset = 0;
for line_text in lines_text {
let line_len = line_text.chars().count();
let line = self.build_line_segment(line_text, char_offset);
result.push(line);
char_offset += line_len + 1; }
result
}
pub fn build_lines_wrapped(&self, max_width: usize) -> Vec<Line<'static>> {
if max_width == 0 {
return self.build_lines();
}
let mut result = Vec::new();
let mut global_char_offset = 0;
for original_line in self.text.lines() {
let original_len = original_line.chars().count();
let wrapped = Self::wrap_line(original_line, max_width);
let mut line_char_offset = global_char_offset;
for wrapped_segment in wrapped {
let segment_len = wrapped_segment.chars().count();
let line = self.build_line_segment(&wrapped_segment, line_char_offset);
result.push(line);
line_char_offset += segment_len;
}
global_char_offset += original_len + 1; }
if result.is_empty() {
result.push(Line::from(vec![]));
}
result
}
fn wrap_line(text: &str, max_width: usize) -> Vec<String> {
if max_width == 0 || text.is_empty() {
return vec![text.to_string()];
}
let mut result = Vec::new();
let mut current_line = String::new();
let mut current_width = 0;
for word in text.split_whitespace() {
let word_width = word.chars().count();
if word_width > max_width {
if !current_line.is_empty() {
result.push(current_line);
current_line = String::new();
current_width = 0;
}
let mut chars = word.chars().peekable();
while chars.peek().is_some() {
let chunk: String = chars.by_ref().take(max_width).collect();
if !chunk.is_empty() {
result.push(chunk);
}
}
} else if current_width == 0 {
current_line = word.to_string();
current_width = word_width;
} else if current_width + 1 + word_width <= max_width {
current_line.push(' ');
current_line.push_str(word);
current_width += 1 + word_width;
} else {
result.push(current_line);
current_line = word.to_string();
current_width = word_width;
}
}
if !current_line.is_empty() {
result.push(current_line);
}
if result.is_empty() {
result.push(String::new());
}
result
}
fn build_line_segment(&self, text: &str, char_offset: usize) -> Line<'static> {
use rand::rngs::SmallRng;
use rand::SeedableRng;
let chars: Vec<char> = text.chars().collect();
let mut spans = Vec::new();
let base_color = self.verb.color();
for (i, &ch) in chars.iter().enumerate() {
let global_idx = char_offset + i;
let char_seed = self.seed.wrapping_add(global_idx as u64 * 31337);
let mut char_rng = SmallRng::seed_from_u64(char_seed.wrapping_add(self.frame as u64));
let progress = self.char_progress.get(global_idx).copied().unwrap_or(1.0);
if progress >= 1.0 {
let style = Style::default().fg(base_color);
spans.push(Span::styled(ch.to_string(), style));
} else {
let glyph = Self::random_glyph_static(&mut char_rng);
let colors = [
solarized::CYAN,
solarized::GREEN,
solarized::YELLOW,
solarized::ORANGE,
solarized::MAGENTA,
solarized::VIOLET,
solarized::BLUE,
solarized::RED,
];
let chaos_color = colors[char_rng.gen_range(0..colors.len())];
let style = Style::default().fg(chaos_color);
spans.push(Span::styled(glyph, style));
}
}
Line::from(spans)
}
fn random_glyph_static(rng: &mut SmallRng) -> String {
let roll: f32 = rng.gen();
if roll < 0.35 {
let chars: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '_', '<', '>', '[', ']', '{', '}', '/', '*', '#', '@', '^',
];
let idx = rng.gen_range(0..chars.len());
chars[idx].to_string()
} else if roll < 0.80 {
let idx = rng.gen_range(0..MATRIX_KATAKANA.len());
MATRIX_KATAKANA[idx].to_string()
} else if roll < 0.90 {
let idx = rng.gen_range(0..KANJI_CHARS.len());
KANJI_CHARS[idx].to_string()
} else if roll < 0.98 {
let idx = rng.gen_range(0..NERD_REFS.len());
NERD_REFS[idx].to_string()
} else {
const NIKA_MASCOTS: &[&str] = &["🐔", "⚡", "📟", "🛰️", "🔌", "🦋"];
let idx = rng.gen_range(0..NIKA_MASCOTS.len());
NIKA_MASCOTS[idx].to_string()
}
}
}
#[derive(Clone, Default)]
pub struct MultiLineDecrypt {
lines: Vec<StreamingDecrypt>,
current_line: usize,
verb: DecryptVerb,
reveal_speed: f32,
seed: u64,
}
impl MultiLineDecrypt {
pub fn new() -> Self {
Self {
lines: vec![StreamingDecrypt::default()],
current_line: 0,
verb: DecryptVerb::default(),
reveal_speed: 0.05,
seed: 42,
}
}
pub fn with_verb(mut self, verb: DecryptVerb) -> Self {
self.verb = verb;
for line in &mut self.lines {
line.verb = verb;
}
self
}
pub fn with_reveal_speed(mut self, speed: f32) -> Self {
self.reveal_speed = speed;
for line in &mut self.lines {
line.reveal_speed = speed;
}
self
}
pub fn push_text(&mut self, text: &str) {
for ch in text.chars() {
if ch == '\n' {
self.current_line += 1;
let new_line = StreamingDecrypt::new()
.with_verb(self.verb)
.with_reveal_speed(self.reveal_speed)
.with_seed(self.seed.wrapping_add(self.current_line as u64));
self.lines.push(new_line);
} else {
if self.current_line >= self.lines.len() {
let new_line = StreamingDecrypt::new()
.with_verb(self.verb)
.with_reveal_speed(self.reveal_speed)
.with_seed(self.seed.wrapping_add(self.current_line as u64));
self.lines.push(new_line);
}
self.lines[self.current_line].push_text(&ch.to_string());
}
}
}
pub fn tick(&mut self) {
for line in &mut self.lines {
line.tick();
}
}
pub fn reveal_all(&mut self) {
for line in &mut self.lines {
line.reveal_all();
}
}
pub fn is_complete(&self) -> bool {
self.lines.iter().all(|l| l.is_complete())
}
pub fn clear(&mut self) {
self.lines.clear();
self.lines.push(
StreamingDecrypt::new()
.with_verb(self.verb)
.with_reveal_speed(self.reveal_speed)
.with_seed(self.seed),
);
self.current_line = 0;
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn render(&self, area: Rect, buf: &mut Buffer, scroll_offset: usize) {
for (i, line) in self.lines.iter().enumerate().skip(scroll_offset) {
let y = area.y + (i - scroll_offset) as u16;
if y >= area.y + area.height {
break;
}
let line_area = Rect::new(area.x, y, area.width, 1);
line.render(line_area, buf);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
#[test]
fn test_decrypt_verb_colors() {
assert_eq!(DecryptVerb::Infer.color(), solarized::VIOLET);
assert_eq!(DecryptVerb::Exec.color(), solarized::GREEN);
assert_eq!(DecryptVerb::Fetch.color(), solarized::CYAN);
assert_eq!(DecryptVerb::Invoke.color(), solarized::BLUE);
assert_eq!(DecryptVerb::Agent.color(), solarized::YELLOW);
}
#[test]
fn test_decrypt_verb_emojis() {
assert!(!DecryptVerb::Infer.emojis().is_empty());
assert!(!DecryptVerb::Agent.emojis().is_empty());
assert!(DecryptVerb::Infer.emojis().contains(&"🪐"));
assert!(DecryptVerb::Agent.emojis().contains(&"🐔"));
}
#[test]
fn test_decrypt_verb_icons() {
assert_eq!(DecryptVerb::Infer.icon(), "⚡");
assert_eq!(DecryptVerb::Agent.icon(), "🐔");
}
#[test]
fn test_matrix_decrypt_renders() {
let backend = TestBackend::new(40, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| {
let decrypt = MatrixDecrypt::new("Hello, World!")
.progress(0.5)
.frame(0)
.verb(DecryptVerb::Infer);
frame.render_widget(decrypt, frame.area());
})
.unwrap();
}
#[test]
fn test_matrix_decrypt_full_reveal() {
let backend = TestBackend::new(40, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| {
let decrypt = MatrixDecrypt::new("Fully revealed")
.progress(1.0)
.verb(DecryptVerb::Exec);
frame.render_widget(decrypt, frame.area());
})
.unwrap();
}
#[test]
fn test_matrix_decrypt_chaos() {
let backend = TestBackend::new(40, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| {
let decrypt = MatrixDecrypt::new("All chaos")
.progress(0.0)
.verb(DecryptVerb::Fetch);
frame.render_widget(decrypt, frame.area());
})
.unwrap();
}
#[test]
fn test_streaming_decrypt_push() {
let mut stream = StreamingDecrypt::new();
stream.push_text("Hello");
assert_eq!(stream.text(), "Hello");
assert_eq!(stream.char_progress.len(), 5);
stream.push_text(" World");
assert_eq!(stream.text(), "Hello World");
assert_eq!(stream.char_progress.len(), 11);
}
#[test]
fn test_streaming_decrypt_tick() {
let mut stream = StreamingDecrypt::new()
.with_reveal_speed(0.5)
.with_initial_chaos(0) .with_wave_factor(0.0); stream.push_text("Hi");
assert!(!stream.is_complete());
stream.tick();
assert_eq!(stream.char_progress[0], 0.5);
stream.tick();
assert_eq!(stream.char_progress[0], 1.0);
assert!(stream.is_complete());
}
#[test]
fn test_streaming_decrypt_initial_chaos_delay() {
let mut stream = StreamingDecrypt::new()
.with_reveal_speed(1.0) .with_initial_chaos(5) .with_wave_factor(0.0);
stream.push_text("A");
for i in 0..4 {
stream.tick();
assert_eq!(
stream.char_progress[0],
0.0,
"Should stay at 0 during chaos at tick {}",
i + 1
);
}
stream.tick();
assert_eq!(stream.char_progress[0], 1.0, "Should reveal after chaos");
assert!(stream.is_complete());
}
#[test]
fn test_streaming_decrypt_cascade_effect() {
let mut stream = StreamingDecrypt::new()
.with_reveal_speed(0.1)
.with_initial_chaos(0) .with_wave_factor(0.5); stream.push_text("ABC");
stream.tick();
let first_progress = stream.char_progress[0];
let last_progress = stream.char_progress[2];
assert!(
first_progress > last_progress,
"First char ({}) should reveal faster than last ({})",
first_progress,
last_progress
);
}
#[test]
fn test_streaming_decrypt_reveal_all() {
let mut stream = StreamingDecrypt::new();
stream.push_text("Test text");
assert!(!stream.is_complete());
stream.reveal_all();
assert!(stream.is_complete());
assert!(stream.char_progress.iter().all(|&p| p == 1.0));
}
#[test]
fn test_streaming_decrypt_clear() {
let mut stream = StreamingDecrypt::new();
stream.push_text("Some text");
stream.tick();
stream.clear();
assert!(stream.text().is_empty());
assert!(stream.char_progress.is_empty());
assert!(!stream.is_complete());
}
#[test]
fn test_streaming_decrypt_verb() {
let stream = StreamingDecrypt::new().with_verb(DecryptVerb::Agent);
assert_eq!(stream.verb, DecryptVerb::Agent);
}
#[test]
fn test_multi_line_decrypt() {
let mut multi = MultiLineDecrypt::new();
multi.push_text("Line 1\nLine 2\nLine 3");
assert_eq!(multi.line_count(), 3);
assert_eq!(multi.lines[0].text(), "Line 1");
assert_eq!(multi.lines[1].text(), "Line 2");
assert_eq!(multi.lines[2].text(), "Line 3");
}
#[test]
fn test_multi_line_tick() {
let mut multi = MultiLineDecrypt::new().with_reveal_speed(1.0); multi.push_text("A\nB");
for _ in 0..15 {
multi.tick();
}
assert!(multi.is_complete());
}
#[test]
fn test_multi_line_reveal_all() {
let mut multi = MultiLineDecrypt::new();
multi.push_text("Line 1\nLine 2");
assert!(!multi.is_complete());
multi.reveal_all();
assert!(multi.is_complete());
}
#[test]
fn test_kanji_chars_included() {
assert!(KANJI_CHARS.contains(&'自'));
assert!(KANJI_CHARS.contains(&'由'));
assert!(KANJI_CHARS.contains(&'ニ'));
assert!(KANJI_CHARS.contains(&'カ'));
assert!(KANJI_CHARS.contains(&'海'));
assert!(KANJI_CHARS.contains(&'賊'));
}
#[test]
fn test_nerd_refs_included() {
assert!(NERD_REFS.contains(&"42"));
assert!(NERD_REFS.contains(&"NULL"));
assert!(NERD_REFS.contains(&"sudo"));
}
#[test]
fn test_cycling_words_included() {
assert!(CYCLING_WORDS.contains(&"Nika"));
assert!(CYCLING_WORDS.contains(&"Freedom"));
assert!(CYCLING_WORDS.contains(&"自由"));
}
#[test]
fn test_emoji_themes_non_empty() {
assert!(!PIRATE_EMOJIS.is_empty());
assert!(!COSMIC_EMOJIS.is_empty());
assert!(!UNICORN_EMOJIS.is_empty());
assert!(!ANIMAL_EMOJIS.is_empty());
assert!(!FOOD_EMOJIS.is_empty());
assert!(!SPECIAL_EMOJIS.is_empty());
assert!(!TECH_EMOJIS.is_empty());
assert!(!NATURE_EMOJIS.is_empty());
}
#[test]
fn test_decrypt_glyph_width() {
assert_eq!(DecryptGlyph::Char('a').display_width(), 1);
assert_eq!(DecryptGlyph::Emoji("🐔").display_width(), 2);
assert_eq!(DecryptGlyph::Kanji('自').display_width(), 2);
assert_eq!(DecryptGlyph::Text("42").display_width(), 2);
}
#[test]
fn test_overall_progress() {
let mut stream = StreamingDecrypt::new();
assert_eq!(stream.overall_progress(), 1.0);
stream.push_text("AB");
assert_eq!(stream.overall_progress(), 0.0);
stream.char_progress[0] = 1.0;
assert_eq!(stream.overall_progress(), 0.5);
stream.char_progress[1] = 1.0;
assert_eq!(stream.overall_progress(), 1.0);
}
#[test]
fn test_build_line_returns_line() {
let mut stream = StreamingDecrypt::new().with_verb(DecryptVerb::Agent);
stream.push_text("Hello");
stream.reveal_all();
let line = stream.build_line();
assert_eq!(line.spans.len(), 5);
}
#[test]
fn test_build_lines_multiline() {
let mut stream = StreamingDecrypt::new();
stream.push_text("Line 1\nLine 2\nLine 3");
stream.reveal_all();
let lines = stream.build_lines();
assert_eq!(lines.len(), 3);
}
#[test]
fn test_build_line_chaos_when_not_revealed() {
let stream = StreamingDecrypt::new().with_verb(DecryptVerb::Fetch);
let line = stream.build_line();
assert!(line.spans.is_empty());
}
#[test]
fn test_build_lines_wrapped_short_text() {
let mut stream = StreamingDecrypt::new();
stream.push_text("Hello World");
stream.reveal_all();
let lines = stream.build_lines_wrapped(50);
assert_eq!(lines.len(), 1);
}
#[test]
fn test_build_lines_wrapped_wraps_long_text() {
let mut stream = StreamingDecrypt::new();
stream.push_text("Hello World Test");
stream.reveal_all();
let lines = stream.build_lines_wrapped(10);
assert!(lines.len() >= 2);
}
#[test]
fn test_build_lines_wrapped_zero_width_fallback() {
let mut stream = StreamingDecrypt::new();
stream.push_text("Hello\nWorld");
stream.reveal_all();
let lines = stream.build_lines_wrapped(0);
assert_eq!(lines.len(), 2);
}
#[test]
fn test_build_lines_wrapped_multiline_with_wrap() {
let mut stream = StreamingDecrypt::new();
stream.push_text("This is a long first line\nShort second");
stream.reveal_all();
let lines = stream.build_lines_wrapped(15);
assert!(lines.len() >= 3); }
#[test]
fn test_wrap_line_simple() {
let result = StreamingDecrypt::wrap_line("Hello World", 20);
assert_eq!(result, vec!["Hello World"]);
}
#[test]
fn test_wrap_line_breaks_at_space() {
let result = StreamingDecrypt::wrap_line("Hello World Test", 10);
assert_eq!(result.len(), 2);
assert_eq!(result[0], "Hello");
assert_eq!(result[1], "World Test");
}
#[test]
fn test_wrap_line_long_word_split() {
let result = StreamingDecrypt::wrap_line("Supercalifragilistic", 5);
assert!(result.len() >= 4); }
#[test]
fn test_wrap_line_empty() {
let result = StreamingDecrypt::wrap_line("", 10);
assert_eq!(result, vec![""]);
}
}