Skip to content

Commit 2a861d8

Browse files
committed
Extracts fish art from original Perl script
Adds a new binary `extract_fish` that extracts fish ASCII art from the original Perl Asciiquarium script. This allows for a larger and more diverse set of fish assets. Generates a Rust module (`generated_fish_assets.rs`) containing the extracted art as `&'static str` constants and a helper function `get_generated_fish_assets()` to build a `Vec`. Updates the `get_all_fish_assets` function to include both manually defined fish and the extracted fish.
1 parent 461991f commit 2a861d8

File tree

4 files changed

+404
-2
lines changed

4 files changed

+404
-2
lines changed

src/bin/extract_fish.rs

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
use std::env;
2+
use std::fs::{self, File};
3+
use std::io::{self, Write};
4+
use std::path::PathBuf;
5+
6+
/// Extracts fish ASCII art blocks from the original Perl Asciiquarium script and
7+
/// generates a Rust module that exposes them as &'static str constants plus a
8+
/// helper to build Vec<FishArt>.
9+
///
10+
/// Usage:
11+
/// cargo run --bin extract_fish [input_perl_path] [output_rust_path]
12+
///
13+
/// Defaults:
14+
/// input_perl_path = archive/original/asciiquarium
15+
/// output_rust_path = src/widgets/generated_fish_assets.rs
16+
///
17+
/// Strategy:
18+
/// - Locate `sub add_new_fish` and `sub add_old_fish` in the Perl script.
19+
/// - Within each, find `my @fish_image = (` and parse quoted blocks (q{...} or q#...#).
20+
/// - The fish arrays alternate [art, color_mask, art, color_mask, ...].
21+
/// We extract only the art blocks at even indices (0, 2, 4, ...).
22+
/// - Generate a Rust module with:
23+
/// - const FISH_N: &str = "...";
24+
/// - pub fn get_generated_fish_assets() -> Vec<FishArt> { ... }
25+
/// - a local measure function to compute width/height
26+
fn main() -> io::Result<()> {
27+
let (input_path, output_path) = parse_args();
28+
29+
let input = fs::read_to_string(&input_path)?;
30+
let mut all_art: Vec<String> = Vec::new();
31+
32+
for func in ["add_new_fish", "add_old_fish"] {
33+
let blocks = extract_fish_blocks_from_function(&input, func);
34+
all_art.extend(blocks);
35+
}
36+
37+
if all_art.is_empty() {
38+
eprintln!("No fish art blocks found. Check the input file path and format.");
39+
}
40+
41+
let generated = generate_rust_module(&all_art);
42+
43+
// Ensure parent dir exists
44+
if let Some(parent) = output_path.parent() {
45+
if !parent.exists() {
46+
fs::create_dir_all(parent)?;
47+
}
48+
}
49+
50+
let mut file = File::create(&output_path)?;
51+
file.write_all(generated.as_bytes())?;
52+
53+
println!(
54+
"Extracted {} fish art blocks into: {}",
55+
all_art.len(),
56+
output_path.display()
57+
);
58+
59+
Ok(())
60+
}
61+
62+
fn parse_args() -> (PathBuf, PathBuf) {
63+
let mut args = env::args().skip(1);
64+
let input = args
65+
.next()
66+
.map(PathBuf::from)
67+
.unwrap_or_else(|| PathBuf::from("archive/original/asciiquarium"));
68+
let output = args
69+
.next()
70+
.map(PathBuf::from)
71+
.unwrap_or_else(|| PathBuf::from("src/widgets/generated_fish_assets.rs"));
72+
(input, output)
73+
}
74+
75+
/// Extract fish art blocks from a specific Perl function:
76+
/// - Locates `sub <func_name>`
77+
/// - Finds `my @fish_image = (`
78+
/// - Collects q{...} or q#...# blocks until `);`
79+
/// - Returns only the even-indexed blocks (0, 2, 4, ...)
80+
fn extract_fish_blocks_from_function(input: &str, func_name: &str) -> Vec<String> {
81+
let mut results = Vec::new();
82+
83+
// Locate function start
84+
let func_marker = format!("sub {}", func_name);
85+
let Some(func_start_idx) = input.find(&func_marker) else {
86+
return results;
87+
};
88+
89+
// Search from function start for the fish_image array declaration
90+
let array_marker = "my @fish_image";
91+
let array_start_rel = input[func_start_idx..].find(array_marker);
92+
let Some(array_start_rel) = array_start_rel else {
93+
return results;
94+
};
95+
let array_start = func_start_idx + array_start_rel;
96+
97+
// From array start, find the opening '(' and then parse until the matching ');'
98+
let after_array = &input[array_start..];
99+
let Some(paren_idx_rel) = after_array.find('(') else {
100+
return results;
101+
};
102+
let cursor = array_start + paren_idx_rel + 1; // position after '('
103+
104+
// We will scan line-by-line for q-blocks, until we reach a line containing ");"
105+
let mut in_block = false;
106+
let mut block_delim = BlockDelim::Brace; // default; will set per block
107+
let mut current_block: Vec<String> = Vec::new();
108+
109+
for line in input[cursor..].lines() {
110+
let trimmed = line.trim_start();
111+
112+
// End of array
113+
if !in_block && trimmed.contains(");") {
114+
break;
115+
}
116+
117+
if !in_block {
118+
// Start of a q-block?
119+
// We support q{...} and q#...# patterns commonly used in the original file.
120+
if trimmed.starts_with("q{") {
121+
in_block = true;
122+
block_delim = BlockDelim::Brace;
123+
// If there is content after q{ on the same line, capture it (rare in file)
124+
if let Some(rest) = trimmed.strip_prefix("q{") {
125+
// If same-line close occurs (unlikely in fish arrays), handle it
126+
if rest.contains("},") {
127+
let before_close = rest.split("},").next().unwrap_or("");
128+
if !before_close.is_empty() {
129+
current_block.push(before_close.to_string());
130+
}
131+
// End block immediately
132+
results.push(current_block.join("\n"));
133+
current_block.clear();
134+
in_block = false;
135+
} else {
136+
// Typically, content starts on the next line; we still store remainder if any
137+
if !rest.is_empty() {
138+
current_block.push(rest.to_string());
139+
}
140+
}
141+
}
142+
} else if trimmed.starts_with("q#") {
143+
in_block = true;
144+
block_delim = BlockDelim::Hash;
145+
if let Some(rest) = trimmed.strip_prefix("q#") {
146+
if rest.contains("#,") {
147+
let before_close = rest.split("#,").next().unwrap_or("");
148+
if !before_close.is_empty() {
149+
current_block.push(before_close.to_string());
150+
}
151+
results.push(current_block.join("\n"));
152+
current_block.clear();
153+
in_block = false;
154+
} else if !rest.is_empty() {
155+
current_block.push(rest.to_string());
156+
}
157+
}
158+
}
159+
// otherwise ignore non-q lines
160+
} else {
161+
// Inside a q-block: collect lines until delimiter close
162+
let close_hit = match block_delim {
163+
BlockDelim::Brace => trimmed == "}," || trimmed.ends_with("},"),
164+
BlockDelim::Hash => trimmed == "#," || trimmed.ends_with("#,"),
165+
};
166+
167+
if close_hit {
168+
// End current block. We don't include the closing line.
169+
let art = current_block.join("\n");
170+
results.push(art);
171+
current_block.clear();
172+
in_block = false;
173+
} else {
174+
current_block.push(line.to_string());
175+
}
176+
}
177+
}
178+
179+
// results contains [art, mask, art, mask, ...]; keep only even indices
180+
results
181+
.into_iter()
182+
.enumerate()
183+
.filter_map(|(i, s)| {
184+
if i % 2 == 0 {
185+
Some(trim_trailing_newlines(&s))
186+
} else {
187+
None
188+
}
189+
})
190+
.collect()
191+
}
192+
193+
#[derive(Clone, Copy)]
194+
enum BlockDelim {
195+
Brace,
196+
Hash,
197+
}
198+
199+
/// Trim up to one trailing newline for a cleaner const block, but preserve interior newlines.
200+
fn trim_trailing_newlines(s: &str) -> String {
201+
let mut out = s.to_string();
202+
while out.ends_with('\n') {
203+
out.pop();
204+
}
205+
out
206+
}
207+
208+
/// Generate the Rust module source containing constants and a helper function.
209+
///
210+
/// The generated module is self-contained and does not depend on the existing
211+
/// `asciiquarium_assets` module. It exposes:
212+
/// - const FISH_N: &str = "...";
213+
/// - pub fn get_generated_fish_assets() -> Vec<FishArt>
214+
/// - a local `measure_art` function.
215+
fn generate_rust_module(art_blocks: &[String]) -> String {
216+
let mut out = String::new();
217+
218+
out.push_str("//! AUTO-GENERATED FILE: Do not edit by hand.\n");
219+
out.push_str("//!\n");
220+
out.push_str(
221+
"//! Generated by `src/bin/extract_fish.rs` from the original Perl Asciiquarium script.\n",
222+
);
223+
out.push_str(
224+
"//! This module provides fish ASCII assets as &'static str constants and a helper\n",
225+
);
226+
out.push_str("//! to build Vec<FishArt> for use with the Asciiquarium widget.\n\n");
227+
228+
out.push_str("use super::asciiquarium::FishArt;\n\n");
229+
230+
for (i, art) in art_blocks.iter().enumerate() {
231+
let const_name = format!("FISH_{:04}", i + 1);
232+
let escaped = escape_for_rust_string(art);
233+
out.push_str(&format!(
234+
"pub const {}: &str = \"{}\";\n",
235+
const_name, escaped
236+
));
237+
}
238+
239+
out.push_str("\nfn measure_art(art: &str) -> (usize, usize) {\n");
240+
out.push_str(" let mut max_w = 0usize;\n");
241+
out.push_str(" let mut h = 0usize;\n");
242+
out.push_str(" for line in art.lines() {\n");
243+
out.push_str(" let w = line.chars().count();\n");
244+
out.push_str(" if w > max_w { max_w = w; }\n");
245+
out.push_str(" h += 1;\n");
246+
out.push_str(" }\n");
247+
out.push_str(" (max_w.max(1), h.max(1))\n");
248+
out.push_str("}\n\n");
249+
250+
out.push_str("pub fn get_generated_fish_assets() -> Vec<FishArt> {\n");
251+
out.push_str(" let mut out = Vec::new();\n");
252+
if !art_blocks.is_empty() {
253+
out.push_str(" let arts: &[&str] = &[\n");
254+
for i in 0..art_blocks.len() {
255+
out.push_str(&format!(" FISH_{:04},\n", i + 1));
256+
}
257+
out.push_str(" ];\n");
258+
out.push_str(" for art in arts {\n");
259+
out.push_str(" let (w, h) = measure_art(art);\n");
260+
out.push_str(" out.push(FishArt { art, width: w, height: h });\n");
261+
out.push_str(" }\n");
262+
}
263+
out.push_str(" out\n");
264+
out.push_str("}\n");
265+
266+
out
267+
}
268+
269+
/// Escape a string for inclusion in a standard Rust string literal.
270+
/// - Escapes backslashes and double quotes
271+
/// - Converts CRLF to LF
272+
/// - Preserves newlines as \\n
273+
fn escape_for_rust_string(s: &str) -> String {
274+
let mut out = String::with_capacity(s.len() * 2);
275+
for ch in s.replace("\r\n", "\n").chars() {
276+
match ch {
277+
'\\' => out.push_str("\\\\"),
278+
'\"' => out.push_str("\\\""),
279+
'\n' => out.push_str("\\n"),
280+
_ => out.push(ch),
281+
}
282+
}
283+
out
284+
}
285+
286+
#[cfg(test)]
287+
mod tests {
288+
use super::*;
289+
290+
#[test]
291+
fn test_trim_trailing_newlines() {
292+
assert_eq!(trim_trailing_newlines("abc\n"), "abc");
293+
assert_eq!(trim_trailing_newlines("abc\n\n"), "abc");
294+
assert_eq!(trim_trailing_newlines("abc"), "abc");
295+
}
296+
297+
#[test]
298+
fn test_escape_for_rust_string() {
299+
let s = "a\\b\"c\nd";
300+
let e = escape_for_rust_string(s);
301+
assert_eq!(e, "a\\\\b\\\"c\\nd");
302+
}
303+
304+
#[test]
305+
fn test_generate_empty_module() {
306+
let m = generate_rust_module(&[]);
307+
assert!(m.contains("get_generated_fish_assets"));
308+
// Should not include arts array if empty
309+
assert!(!m.contains("let arts: &[&str] = &["));
310+
}
311+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ pub use widgets::asciiquarium::{
2222
AsciiquariumWidget, FishArt, FishInstance,
2323
};
2424
pub use widgets::asciiquarium_assets::{get_fish_assets, measure_art};
25+
pub use widgets::get_all_fish_assets;

src/widgets/generated_fish_assets.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! AUTO-GENERATED FILE: Do not edit by hand.
2+
//!
3+
//! Generated by `src/bin/extract_fish.rs` from the original Perl Asciiquarium script.
4+
//! This module provides fish ASCII assets as &'static str constants and a helper
5+
//! to build Vec<FishArt> for use with the Asciiquarium widget.
6+
7+
use super::asciiquarium::FishArt;
8+
9+
pub const FISH_0001: &str = " \\\\\n / \\\\\n>=_('>\n \\\\_/\n /";
10+
pub const FISH_0002: &str = " /\n / \\\\\n<')_=<\n \\\\_/\n \\\\";
11+
pub const FISH_0003: &str = " ,\n \\}\\\\\n\\\\ .' `\\\\\n\\}\\}< ( 6>\n/ `, .'\n \\}/\n '";
12+
pub const FISH_0004: &str = " ,\n /\\{\n /' `. /\n<6 ) >\\{\\{\n `. ,' \\\\\n \\\\\\{\n `";
13+
pub const FISH_0005: &str = " \\\\'`.\n ) \\\\\n(`.??????_.-`' ' '`-.\n \\\\ `.??.` (o) \\\\_\n > >< ((( (\n / .`??`._ /_| /'\n(.`???????`-. _ _.-`\n /__/'";
14+
pub const FISH_0006: &str = " .'`/\n / (\n .-'` ` `'-._??????.')\n_/ (o) '.??.' /\n) ))) >< <\n`\\\\ |_\\\\ _.'??'. \\\\\n '-._ _ .-'???????'.)\n `\\\\__\\\\";
15+
pub const FISH_0007: &str = " ,--,_\n__ _\\\\.---'-.\n\\\\ '.-\" // o\\\\\n/_.'-._ \\\\\\\\ /\n `\"--(/\"`";
16+
pub const FISH_0008: &str = " _,--,\n .-'---./_ __\n/o \\\\\\\\ \"-.' /\n\\\\ // _.-'._\\\\\n `\"\\\\)--\"`";
17+
pub const FISH_0009: &str = " \\\n ...\\..,\n\\ /' \\\n >= ( ' >\n/ \\ / /\n `\"'\"'/''";
18+
pub const FISH_0010: &str = " /\n ,../...\n / '\\ /\n< ' ) =<\n \\ \\ / \\\n `'\\'\"'\"'";
19+
pub const FISH_0011: &str = " \\\n\\ /--\\\n>= (o>\n/ \\__/\n /";
20+
pub const FISH_0012: &str = " /\n /--\\ /\n<o) =<\n \\__/ \\\n \\";
21+
pub const FISH_0013: &str = " \\:.\n\\;, ,;\\\\\\\\\\,,\n \\\\\\\\\\;;:::::::o\n ///;;::::::::<\n /;` ``/////``";
22+
pub const FISH_0014: &str = " .:/\n ,,///;, ,;/\n o:::::::;;///\n>::::::::;;\\\\\\\\\\\n ''\\\\\\\\\\\\\\\\\\'' ';\\";
23+
pub const FISH_0015: &str = " __\n><_'>\n '";
24+
pub const FISH_0016: &str = " __\n<'_><\n `";
25+
pub const FISH_0017: &str = " ..\\,\n>=' ('>\n '''/''";
26+
pub const FISH_0018: &str = " ,/..\n<') `=<\n ``\\```";
27+
pub const FISH_0019: &str = " \\\n / \\\n>=_('>\n \\_/\n /";
28+
pub const FISH_0020: &str = " /\n / \\\n<')_=<\n \\_/\n \\";
29+
pub const FISH_0021: &str = " ,\\\n>=('>\n '/";
30+
pub const FISH_0022: &str = " /,\n<')=<\n \\`";
31+
pub const FISH_0023: &str = " __\n\\/ o\\\n/\\__/";
32+
pub const FISH_0024: &str = " __\n/o \\/\n\\__/\\";
33+
34+
fn measure_art(art: &str) -> (usize, usize) {
35+
let mut max_w = 0usize;
36+
let mut h = 0usize;
37+
for line in art.lines() {
38+
let w = line.chars().count();
39+
if w > max_w { max_w = w; }
40+
h += 1;
41+
}
42+
(max_w.max(1), h.max(1))
43+
}
44+
45+
pub fn get_generated_fish_assets() -> Vec<FishArt> {
46+
let mut out = Vec::new();
47+
let arts: &[&str] = &[
48+
FISH_0001,
49+
FISH_0002,
50+
FISH_0003,
51+
FISH_0004,
52+
FISH_0005,
53+
FISH_0006,
54+
FISH_0007,
55+
FISH_0008,
56+
FISH_0009,
57+
FISH_0010,
58+
FISH_0011,
59+
FISH_0012,
60+
FISH_0013,
61+
FISH_0014,
62+
FISH_0015,
63+
FISH_0016,
64+
FISH_0017,
65+
FISH_0018,
66+
FISH_0019,
67+
FISH_0020,
68+
FISH_0021,
69+
FISH_0022,
70+
FISH_0023,
71+
FISH_0024,
72+
];
73+
for art in arts {
74+
let (w, h) = measure_art(art);
75+
out.push(FishArt { art, width: w, height: h });
76+
}
77+
out
78+
}

0 commit comments

Comments
 (0)