Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ clap = { version = "4.5.43", features = ["cargo", "derive"] }
crossterm = "0.29.0"
rand = "0.9.2"
ezemoji = "2.0.0"
toml = "0.9.5"
serde = { version = "1.0.219", features = ["derive"] }

[dev-dependencies]
pretty_assertions = "1.4.1"
Expand Down
165 changes: 157 additions & 8 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,157 @@
use super::{AUTHOR, Direction, MAXSPEED, MINSPEED};
use clap::{Parser, crate_description, crate_name, crate_version};
use ezemoji::{CharGroup, CharWidth, GroupKind, MultiRange};
use serde::{Deserialize, Deserializer, Serialize};
use std::path::PathBuf;

use std::str::FromStr;

#[derive(Debug, Deserialize)]
struct RangeDef {
start: u32,
end: u32,
}

impl From<RangeDef> for std::ops::Range<u32> {
fn from(r: RangeDef) -> Self {
r.start..r.end
}
}

fn deserialize_ranges<'de, D>(deserializer: D) -> Result<Vec<std::ops::Range<u32>>, D::Error>
where
D: Deserializer<'de>,
{
let defs: Vec<RangeDef> = Vec::deserialize(deserializer)?;
Ok(defs.into_iter().map(|r| r.into()).collect())
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Group {
#[serde(deserialize_with = "deserialize_ranges")]
pub range: Vec<std::ops::Range<u32>>,
pub width: u8,
}

impl Group {
pub fn width(&self) -> u8 {
self.width
}

pub fn len(&self) -> usize {
self.range.iter().map(|r| (r.end - r.start) as usize).sum()
}
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Config {
pub groups: std::collections::BTreeMap<String, Group>,
}

pub fn load_config() -> Option<Config> {
#[cfg(windows)]
let config_path = {
let appdata = std::env::var("APPDATA").unwrap();
PathBuf::from(appdata)
.join("rusty-rain")
.join("config.toml")
};

#[cfg(unix)]
let config_path = {
let home = std::env::var("HOME").unwrap();
PathBuf::from(home)
.join(".config")
.join("rusty-rain")
.join("config.toml")
};

if !config_path.exists() {
return None;
}

let string_config = std::fs::read_to_string(&config_path).unwrap_or_default();
let config: Config = match toml::from_str(&string_config) {
Ok(config) => config,
Err(err) => {
eprintln!("failed to parse config: {err}");
return None;
}
};

Some(config)
}

#[derive(Debug, Clone)]
pub struct CharGroupKind(pub CharGroup);
pub enum Grouping {
EzEmoji(CharGroup),
Custom(Group),
}

impl Grouping {
pub fn name(&self) -> GroupKind {
match self {
Grouping::EzEmoji(group) => group.name,
Grouping::Custom(_) => GroupKind::Custom("custom"),
}
}

pub fn width(&self) -> u8 {
match self {
Grouping::EzEmoji(group) => group.width(),
Grouping::Custom(group) => group.width(),
}
}

pub fn len(&self) -> usize {
match self {
Grouping::EzEmoji(group) => group.len,
Grouping::Custom(group) => group.len(),
}
}

impl FromStr for CharGroupKind {
pub fn nth_char(&self, index: usize) -> Option<char> {
match self {
Grouping::EzEmoji(group) => group.nth_char(index),
Grouping::Custom(group) => {
let index = index as u32;
let mut i = 0u32;
for range in group.range.iter() {
let step = range.end - range.start;
if index >= i && index < i + step {
let offset = index - i;
return char::from_u32(range.start + offset);
}
i += step;
}
None
}
}
}
}

impl From<CharGroup> for Grouping {
fn from(value: CharGroup) -> Self {
Grouping::EzEmoji(value)
}
}

impl From<Group> for Grouping {
fn from(value: Group) -> Self {
Grouping::Custom(value)
}
}

impl FromStr for Grouping {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
// Idea was brought up to use these nerd fonts icons by
// Idea was brought up to use these chars together by
// [tonogdlp](https://github.com/tonogdlp) in PR
// https://github.com/cowboy8625/ezemoji/pull/4
// Once merged into ezemoji we can remove this
"classic" => Ok(CharGroupKind(CharGroup::new(
"classic" => Ok(Grouping::from(CharGroup::new(
GroupKind::Custom("Classic"),
MultiRange::new(&[
ezemoji::JAP_RANGE,
Expand All @@ -35,7 +171,7 @@ impl FromStr for CharGroupKind {
// [hasecilu](https://github.com/hasecilu) in PR
// https://github.com/cowboy8625/ezemoji/pull/5
// Once merged into ezemoji we can remove this
"opensource" => Ok(CharGroupKind(CharGroup::new(
"opensource" => Ok(Grouping::from(CharGroup::new(
GroupKind::Custom("OpenSource"),
MultiRange::new(&[
62208..62210,
Expand All @@ -61,7 +197,7 @@ impl FromStr for CharGroupKind {
// [hasecilu](https://github.com/hasecilu) in PR
// https://github.com/cowboy8625/ezemoji/pull/5
// Once merged into ezemoji we can remove this
"pglangs" => Ok(CharGroupKind(CharGroup::new(
"pglangs" => Ok(Grouping::from(CharGroup::new(
GroupKind::Custom("ProgrammingLanguages"),
MultiRange::new(&[
// From all Nerd Fonts
Expand Down Expand Up @@ -114,7 +250,20 @@ impl FromStr for CharGroupKind {
]),
CharWidth::Double,
))),
name => Ok(CharGroupKind(CharGroup::from_str(name)?)),
name => match CharGroup::from_str(name) {
Ok(group) => Ok(Grouping::from(group)),
Err(_) => {
let Some(config) = load_config() else {
return Err("group not found".to_string());
};

if let Some(group) = config.groups.get(name) {
Ok(Grouping::from(group.clone()))
} else {
Err("group not found".to_string())
}
}
},
}
}
}
Expand Down Expand Up @@ -186,7 +335,7 @@ pub struct Cli {
#[arg(short, long, default_value_t = false)]
pub shade: bool,
#[arg(short, long, help = HELP_CHARS, default_value = "bin")]
pub group: CharGroupKind,
pub group: Grouping,
#[arg(short = 'C', long, help = HELP_COLORS, default_value_t = String::from("green"))]
pub color: String,
#[arg(short = 'H', long, help = HELP_HEAD, default_value_t = String::from("white"))]
Expand Down
65 changes: 35 additions & 30 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ use crossterm::{
style::{Color, Print, SetForegroundColor},
terminal,
};
use std::time::{Duration, Instant};
use rand::Rng;
use std::{
io::{BufWriter, Stdout, Write, stdout},
str::FromStr,
time::{Duration, Instant},
};

#[cfg(test)]
use rand::SeedableRng;

use crate::cli::Grouping;

const MAXSPEED: u64 = 0;
const MINSPEED: u64 = 200;
const POLL_INTERVAL: Duration = Duration::from_millis(50);
Expand All @@ -26,13 +32,6 @@ const AUTHOR: &str = "
Email: [email protected]
";

use rand::Rng;

#[cfg(test)]
use rand::SeedableRng;

use crate::cli::CharGroupKind;

/// rand crate wrapper for testing.
/// being able to have deterministic tests is important
#[derive(Debug)]
Expand Down Expand Up @@ -136,7 +135,7 @@ impl FromStr for Direction {
}

#[derive(Debug)]
struct Rain<'group, const LENGTH: usize> {
struct Rain<const LENGTH: usize> {
/// Random number generator wrapper for testing purposes
rng: Random,
/// Characters to use for the rain
Expand Down Expand Up @@ -165,8 +164,8 @@ struct Rain<'group, const LENGTH: usize> {
queue: Vec<usize>,
/// Speed of the rain
speed: std::ops::Range<u64>,
/// Group of characters defined by a slice &'static [u32]
group: &'group CharGroupKind,
/// Character width
char_width: usize,
/// Width of the terminal
/// NOTE: This value is not a true width of the terminal but size in visible characters
/// 🌕 is a single character but takes up 2 columns and so the width value would count this as
Expand All @@ -181,18 +180,17 @@ struct Rain<'group, const LENGTH: usize> {
previous_screen_buffer: Vec<Cell>,
}

impl<'group, const LENGTH: usize> Rain<'group, LENGTH> {
impl<const LENGTH: usize> Rain<LENGTH> {
const MIN_LENGTH_OF_RAIN: usize = 4;
const MAX_LENGTH_OFFSET_OF_RAIN: usize = 4;
fn new(mut width: usize, height: usize, settings: &'group cli::Cli) -> Self {
width /= settings.group.0.width() as usize;
fn new(mut width: usize, height: usize, settings: &cli::Cli) -> Self {
width /= settings.group.width() as usize;

let mut rng = Random::default();
let char_length = settings.group.0.len();
let char_length = settings.group.len();
let chars: [char; LENGTH] = std::array::from_fn(|_| {
settings
.group
.0
.nth_char(rng.random_range(0..char_length))
.unwrap_or('#') // fallback character
});
Expand Down Expand Up @@ -246,7 +244,7 @@ impl<'group, const LENGTH: usize> Rain<'group, LENGTH> {
body_colors,
chars,
directions: vec![settings.direction; width],
group: &settings.group,
char_width: settings.group.width() as usize,
head_colors: vec![settings.head_color().into(); width],
height,
positions: vec![0; width],
Expand Down Expand Up @@ -410,11 +408,10 @@ impl<'group, const LENGTH: usize> Rain<'group, LENGTH> {
}

if redraw_screen && matches!(self.directions[0], Direction::Left | Direction::Right) {
let char_width = self.group.0.width() as usize;
for (y, chunk) in self.screen_buffer.chunks(self.width).enumerate() {
let screen = chunk
.iter()
.map(|c| c.display(char_width))
.map(|c| c.display(self.char_width))
.collect::<String>();
queue!(w, cursor::MoveTo(0, y as u16), Print(screen))?;

Expand All @@ -424,23 +421,20 @@ impl<'group, const LENGTH: usize> Rain<'group, LENGTH> {
return Ok(());
} else if redraw_screen {
execute!(w, cursor::MoveTo(0, 0))?;
let char_width = self.group.0.width() as usize;
let screen = self
.screen_buffer
.iter()
.map(|c| c.display(char_width))
.map(|c| c.display(self.char_width))
.collect::<String>();
execute!(w, Print(screen))?;

self.queue.clear();
return Ok(());
}

let group_width = self.group.0.width() as usize;

for idx in self.queue.drain(..) {
let cell = &self.screen_buffer[idx];
let x = (idx % self.width) * group_width;
let x = (idx % self.width) * self.char_width;
let y = idx / self.width;

if cell.is_visible() {
Expand All @@ -455,7 +449,7 @@ impl<'group, const LENGTH: usize> Rain<'group, LENGTH> {
w,
cursor::MoveTo(x as u16, y as u16),
SetForegroundColor(cell.color),
Print(" ".repeat(group_width))
Print(" ".repeat(self.char_width))
)?;
}

Expand Down Expand Up @@ -572,14 +566,25 @@ fn main() -> std::io::Result<()> {
// visibly expand.
// example:  .
let extra_width = matches!(
settings.group.0.name,
settings.group.name(),
ezemoji::GroupKind::Custom("OpenSource")
| ezemoji::GroupKind::Custom("ProgrammingLanguages")
);
for char in settings.group.0.iter() {
print!("{char}");
if extra_width {
print!(" ");
match settings.group {
Grouping::EzEmoji(group) => {
for char in group.iter() {
print!("{char}");
if extra_width {
print!(" ");
}
}
}
Grouping::Custom(group) => {
for range in group.range.iter() {
for cp in range.clone() {
print!("{}", std::char::from_u32(cp).unwrap_or('🤦'));
}
}
}
}
return Ok(());
Expand Down
Loading
Loading