Small library for generating claude-code like unicde block mascots, and providing animations when they do stuff
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: save/load named characters from ~/.config/clood.json

- clood --save <name>: saves current character to disk
- clood <name>: recalls a saved character
- clood <name> --anim mood:-1:3: recall and animate
- Unknown names list all saved characters
- JSON file is human-readable and mergeable

+323 -29
+200 -3
Cargo.lock
··· 38 38 source = "registry+https://github.com/rust-lang/crates.io-index" 39 39 checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 40 40 dependencies = [ 41 - "windows-sys", 41 + "windows-sys 0.61.2", 42 42 ] 43 43 44 44 [[package]] ··· 49 49 dependencies = [ 50 50 "anstyle", 51 51 "once_cell_polyfill", 52 - "windows-sys", 52 + "windows-sys 0.61.2", 53 53 ] 54 54 55 55 [[package]] ··· 125 125 dependencies = [ 126 126 "clap", 127 127 "ctrlc", 128 + "dirs", 128 129 "rand", 130 + "serde", 131 + "serde_json", 129 132 ] 130 133 131 134 [[package]] ··· 142 145 dependencies = [ 143 146 "dispatch2", 144 147 "nix", 145 - "windows-sys", 148 + "windows-sys 0.61.2", 149 + ] 150 + 151 + [[package]] 152 + name = "dirs" 153 + version = "5.0.1" 154 + source = "registry+https://github.com/rust-lang/crates.io-index" 155 + checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 156 + dependencies = [ 157 + "dirs-sys", 158 + ] 159 + 160 + [[package]] 161 + name = "dirs-sys" 162 + version = "0.4.1" 163 + source = "registry+https://github.com/rust-lang/crates.io-index" 164 + checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 165 + dependencies = [ 166 + "libc", 167 + "option-ext", 168 + "redox_users", 169 + "windows-sys 0.48.0", 146 170 ] 147 171 148 172 [[package]] ··· 181 205 checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 182 206 183 207 [[package]] 208 + name = "itoa" 209 + version = "1.0.18" 210 + source = "registry+https://github.com/rust-lang/crates.io-index" 211 + checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" 212 + 213 + [[package]] 184 214 name = "libc" 185 215 version = "0.2.183" 186 216 source = "registry+https://github.com/rust-lang/crates.io-index" 187 217 checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" 218 + 219 + [[package]] 220 + name = "libredox" 221 + version = "0.1.14" 222 + source = "registry+https://github.com/rust-lang/crates.io-index" 223 + checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" 224 + dependencies = [ 225 + "libc", 226 + ] 227 + 228 + [[package]] 229 + name = "memchr" 230 + version = "2.8.0" 231 + source = "registry+https://github.com/rust-lang/crates.io-index" 232 + checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 188 233 189 234 [[package]] 190 235 name = "nix" ··· 220 265 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 221 266 222 267 [[package]] 268 + name = "option-ext" 269 + version = "0.2.0" 270 + source = "registry+https://github.com/rust-lang/crates.io-index" 271 + checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 272 + 273 + [[package]] 223 274 name = "ppv-lite86" 224 275 version = "0.2.21" 225 276 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 277 328 ] 278 329 279 330 [[package]] 331 + name = "redox_users" 332 + version = "0.4.6" 333 + source = "registry+https://github.com/rust-lang/crates.io-index" 334 + checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 335 + dependencies = [ 336 + "getrandom", 337 + "libredox", 338 + "thiserror", 339 + ] 340 + 341 + [[package]] 342 + name = "serde" 343 + version = "1.0.228" 344 + source = "registry+https://github.com/rust-lang/crates.io-index" 345 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 346 + dependencies = [ 347 + "serde_core", 348 + "serde_derive", 349 + ] 350 + 351 + [[package]] 352 + name = "serde_core" 353 + version = "1.0.228" 354 + source = "registry+https://github.com/rust-lang/crates.io-index" 355 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 356 + dependencies = [ 357 + "serde_derive", 358 + ] 359 + 360 + [[package]] 361 + name = "serde_derive" 362 + version = "1.0.228" 363 + source = "registry+https://github.com/rust-lang/crates.io-index" 364 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 365 + dependencies = [ 366 + "proc-macro2", 367 + "quote", 368 + "syn", 369 + ] 370 + 371 + [[package]] 372 + name = "serde_json" 373 + version = "1.0.149" 374 + source = "registry+https://github.com/rust-lang/crates.io-index" 375 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 376 + dependencies = [ 377 + "itoa", 378 + "memchr", 379 + "serde", 380 + "serde_core", 381 + "zmij", 382 + ] 383 + 384 + [[package]] 280 385 name = "strsim" 281 386 version = "0.11.1" 282 387 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 294 399 ] 295 400 296 401 [[package]] 402 + name = "thiserror" 403 + version = "1.0.69" 404 + source = "registry+https://github.com/rust-lang/crates.io-index" 405 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 406 + dependencies = [ 407 + "thiserror-impl", 408 + ] 409 + 410 + [[package]] 411 + name = "thiserror-impl" 412 + version = "1.0.69" 413 + source = "registry+https://github.com/rust-lang/crates.io-index" 414 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 415 + dependencies = [ 416 + "proc-macro2", 417 + "quote", 418 + "syn", 419 + ] 420 + 421 + [[package]] 297 422 name = "unicode-ident" 298 423 version = "1.0.24" 299 424 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 319 444 320 445 [[package]] 321 446 name = "windows-sys" 447 + version = "0.48.0" 448 + source = "registry+https://github.com/rust-lang/crates.io-index" 449 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 450 + dependencies = [ 451 + "windows-targets", 452 + ] 453 + 454 + [[package]] 455 + name = "windows-sys" 322 456 version = "0.61.2" 323 457 source = "registry+https://github.com/rust-lang/crates.io-index" 324 458 checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" ··· 327 461 ] 328 462 329 463 [[package]] 464 + name = "windows-targets" 465 + version = "0.48.5" 466 + source = "registry+https://github.com/rust-lang/crates.io-index" 467 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 468 + dependencies = [ 469 + "windows_aarch64_gnullvm", 470 + "windows_aarch64_msvc", 471 + "windows_i686_gnu", 472 + "windows_i686_msvc", 473 + "windows_x86_64_gnu", 474 + "windows_x86_64_gnullvm", 475 + "windows_x86_64_msvc", 476 + ] 477 + 478 + [[package]] 479 + name = "windows_aarch64_gnullvm" 480 + version = "0.48.5" 481 + source = "registry+https://github.com/rust-lang/crates.io-index" 482 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 483 + 484 + [[package]] 485 + name = "windows_aarch64_msvc" 486 + version = "0.48.5" 487 + source = "registry+https://github.com/rust-lang/crates.io-index" 488 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 489 + 490 + [[package]] 491 + name = "windows_i686_gnu" 492 + version = "0.48.5" 493 + source = "registry+https://github.com/rust-lang/crates.io-index" 494 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 495 + 496 + [[package]] 497 + name = "windows_i686_msvc" 498 + version = "0.48.5" 499 + source = "registry+https://github.com/rust-lang/crates.io-index" 500 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 501 + 502 + [[package]] 503 + name = "windows_x86_64_gnu" 504 + version = "0.48.5" 505 + source = "registry+https://github.com/rust-lang/crates.io-index" 506 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 507 + 508 + [[package]] 509 + name = "windows_x86_64_gnullvm" 510 + version = "0.48.5" 511 + source = "registry+https://github.com/rust-lang/crates.io-index" 512 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 513 + 514 + [[package]] 515 + name = "windows_x86_64_msvc" 516 + version = "0.48.5" 517 + source = "registry+https://github.com/rust-lang/crates.io-index" 518 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 519 + 520 + [[package]] 330 521 name = "zerocopy" 331 522 version = "0.8.47" 332 523 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 345 536 "quote", 346 537 "syn", 347 538 ] 539 + 540 + [[package]] 541 + name = "zmij" 542 + version = "1.0.21" 543 + source = "registry+https://github.com/rust-lang/crates.io-index" 544 + checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+3
Cargo.toml
··· 7 7 [dependencies] 8 8 clap = { version = "4", features = ["derive"] } 9 9 ctrlc = "3" 10 + dirs = "5" 10 11 rand = "0.8" 12 + serde = { version = "1", features = ["derive"] } 13 + serde_json = "1"
+4 -2
src/clood.rs
··· 1 1 //! Core clood type, rendering, and leg layout. 2 2 3 + use serde::{Deserialize, Serialize}; 4 + 3 5 use crate::color::Color; 4 6 5 7 // ── Types ─────────────────────────────────────────────────────────────────── 6 8 7 9 /// Configuration for one eye: vertical offset and open/closed state. 8 - #[derive(Debug, Clone, Copy)] 10 + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 9 11 pub struct EyeState { 10 12 /// Vertical offset from the mood baseline. Positive = higher. 11 13 pub offset: i32, ··· 14 16 } 15 17 16 18 /// All resolved parameters for rendering a single frame. 17 - #[derive(Debug, Clone)] 19 + #[derive(Debug, Clone, Serialize, Deserialize)] 18 20 pub struct Clood { 19 21 /// Body width in columns (minimum 4). 20 22 pub width: usize,
+2 -1
src/color.rs
··· 1 1 //! RGB color type with hex parsing, random generation, and ANSI block rendering. 2 2 3 3 use rand::Rng; 4 + use serde::{Deserialize, Serialize}; 4 5 5 6 /// An RGB color, used for body and eye rendering. 6 - #[derive(Debug, Clone, Copy)] 7 + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 7 8 pub struct Color { 8 9 pub r: u8, 9 10 pub g: u8,
+114 -23
src/main.rs
··· 1 1 //! CLI entry point: parse arguments, build a Clood, render or animate. 2 + //! Supports saving/loading named characters from ~/.config/clood.json. 2 3 3 4 mod animation; 4 5 mod clood; 5 6 mod color; 7 + 8 + use std::collections::HashMap; 9 + use std::fs; 10 + use std::path::PathBuf; 6 11 7 12 use clap::Parser; 8 13 use rand::Rng; ··· 11 16 use clood::{Clood, EyeState}; 12 17 use color::Color; 13 18 19 + // ── CLI ───────────────────────────────────────────────────────────────────── 20 + 14 21 /// Generate a cute unicode block character (clood) in your terminal. 15 22 /// 16 23 /// All parameters are randomized by default. Specify a flag to pin that value. 17 24 /// Animate parameters with --anim <param>:<min>:<max>, e.g. --anim mood:-1:3 25 + /// 26 + /// Save a character with --save <name>, recall it with: clood <name> 18 27 #[derive(Parser, Debug)] 19 28 #[command(name = "clood", version, about)] 20 29 struct Args { 30 + /// Load a saved character by name 31 + #[arg(value_name = "NAME")] 32 + name: Option<String>, 33 + 34 + /// Save the current character under this name 35 + #[arg(long)] 36 + save: Option<String>, 37 + 21 38 /// Body height in rows [default: random 4..8] 22 39 #[arg(long)] 23 40 height: Option<usize>, ··· 87 104 fps: u32, 88 105 } 89 106 107 + // ── Save/load ─────────────────────────────────────────────────────────────── 108 + 109 + /// Path to the saved characters file. 110 + fn config_path() -> PathBuf { 111 + dirs::home_dir() 112 + .unwrap_or_else(|| PathBuf::from(".")) 113 + .join(".config") 114 + .join("clood.json") 115 + } 116 + 117 + /// Load all saved characters from disk. 118 + fn load_saved() -> HashMap<String, Clood> { 119 + let path = config_path(); 120 + match fs::read_to_string(&path) { 121 + Ok(contents) => serde_json::from_str(&contents).unwrap_or_else(|e| { 122 + eprintln!("Warning: couldn't parse {}: {}", path.display(), e); 123 + HashMap::new() 124 + }), 125 + Err(_) => HashMap::new(), 126 + } 127 + } 128 + 129 + /// Save a character to disk, merging with any existing entries. 130 + fn save_character(name: &str, clood: &Clood) { 131 + let path = config_path(); 132 + let mut saved = load_saved(); 133 + saved.insert(name.to_string(), clood.clone()); 134 + 135 + if let Some(parent) = path.parent() { 136 + let _ = fs::create_dir_all(parent); 137 + } 138 + 139 + match serde_json::to_string_pretty(&saved) { 140 + Ok(json) => { 141 + if let Err(e) = fs::write(&path, json) { 142 + eprintln!("Error: couldn't write {}: {}", path.display(), e); 143 + } else { 144 + eprintln!("Saved '{}' to {}", name, path.display()); 145 + } 146 + } 147 + Err(e) => eprintln!("Error: couldn't serialize: {}", e), 148 + } 149 + } 150 + 151 + // ── Main ──────────────────────────────────────────────────────────────────── 152 + 90 153 fn main() { 91 154 let args = Args::parse(); 92 155 let mut rng = rand::thread_rng(); 93 156 94 - // Build the base clood from CLI args (unspecified values are randomized). 95 - let base = Clood { 157 + // If a name is given, try to load it from the save file. 158 + let base = if let Some(ref name) = args.name { 159 + let saved = load_saved(); 160 + match saved.get(name) { 161 + Some(clood) => clood.clone(), 162 + None => { 163 + eprintln!("Unknown character '{}'. Saved characters:", name); 164 + if saved.is_empty() { 165 + eprintln!(" (none — use --save <name> to create one)"); 166 + } else { 167 + for key in saved.keys() { 168 + eprintln!(" {}", key); 169 + } 170 + } 171 + std::process::exit(1); 172 + } 173 + } 174 + } else { 175 + build_clood_from_args(&args, &mut rng) 176 + }; 177 + 178 + // Save if requested. 179 + if let Some(ref save_name) = args.save { 180 + save_character(save_name, &base); 181 + } 182 + 183 + // Parse animation specs. 184 + let animations: Vec<Animation> = args 185 + .anims 186 + .iter() 187 + .filter_map(|s| match Animation::parse(s) { 188 + Ok(anim) => Some(anim), 189 + Err(msg) => { 190 + eprintln!("Warning: {}", msg); 191 + None 192 + } 193 + }) 194 + .collect(); 195 + 196 + if animations.is_empty() { 197 + print!("{}", clood::render(&base)); 198 + } else { 199 + animation::run_loop(&base, &animations, args.fps); 200 + } 201 + } 202 + 203 + /// Build a Clood from CLI arguments, randomizing any unspecified values. 204 + fn build_clood_from_args(args: &Args, rng: &mut impl Rng) -> Clood { 205 + Clood { 96 206 width: args.width.unwrap_or_else(|| rng.gen_range(8..=16)), 97 207 height: args.height.unwrap_or_else(|| rng.gen_range(4..=8)), 98 208 round: args.round, ··· 118 228 if args.color.is_some() { 119 229 eprintln!("Warning: invalid --color, using random"); 120 230 } 121 - Color::random(&mut rng) 231 + Color::random(rng) 122 232 }), 123 233 eye_color: args 124 234 .eyecolor ··· 128 238 if args.eyecolor.is_some() { 129 239 eprintln!("Warning: invalid --eyecolor, using random"); 130 240 } 131 - Color::random(&mut rng) 241 + Color::random(rng) 132 242 }), 133 - }; 134 - 135 - // Parse animation specs. 136 - let animations: Vec<Animation> = args 137 - .anims 138 - .iter() 139 - .filter_map(|s| match Animation::parse(s) { 140 - Ok(anim) => Some(anim), 141 - Err(msg) => { 142 - eprintln!("Warning: {}", msg); 143 - None 144 - } 145 - }) 146 - .collect(); 147 - 148 - if animations.is_empty() { 149 - print!("{}", clood::render(&base)); 150 - } else { 151 - animation::run_loop(&base, &animations, args.fps); 152 243 } 153 244 }