diff --git a/Cargo.lock b/Cargo.lock index 9f24b6a..1610e96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cgol-tui" -version = "0.5.3" +version = "0.6.1" dependencies = [ "fastrand", "ratatui", diff --git a/Cargo.toml b/Cargo.toml index b190c75..b60c5be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cgol-tui" -version = "0.5.3" +version = "0.6.1" authors = ["Jeromos Kovacs "] edition = "2021" description = "Conway's Game of Life implementation with a TUI" diff --git a/README.md b/README.md index cb52315..fef2fb4 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,19 @@ eg.: - `curl https://conwaylife.com/patterns/fx153.cells | cgol-tui -` the `-` stands for `stdin` - `cgol-tui my_own_pattern.cells fx153.cells` +### Script + +there is a fish script provided under [scripts](./scripts/pattern.fish) for viewing patterns from [conwaylife.com](https://conwaylife.com/patterns)
+usage: `pattern.fish [OPTIONS]`
+PATTERN_NAME: either a name of a pattern, or nothing to list all patterns
+OPTIONS: -d, --download download to /tmp + +#### needed tools + +- [fish](https://fishshell.com/): shell +- [curl](https://curl.se/): download +- [ripgrep](https://github.com/BurntSushi/ripgrep): only for listing all patterns + ## Sample ![Sample][1] @@ -29,7 +42,7 @@ eg.: - [x] publishing to crates.io - [x] changing to `Canvas` for rendering viewer block - [x] the ability to parse `.cells` files, from [conwaylife.com](https://conwaylife.com/patterns) -- [ ] display the names of patterns +- [x] display the names of patterns ## Acknowledgements diff --git a/scripts/pattern.fish b/scripts/pattern.fish new file mode 100755 index 0000000..3aea622 --- /dev/null +++ b/scripts/pattern.fish @@ -0,0 +1,24 @@ +#!/usr/bin/env fish + +set cmd $argv[1] +if test "$cmd" = all; or test "$cmd" = any; or test "$cmd" = "" + for line in "$(curl https://conwaylife.com/patterns/ | rg .cells)" + echo $line | string split '"' -f 2 + end + return 0 +end +set pattern "$(string replace '.cells' '' $cmd)" +echo "pattern: '$pattern'" +set url "https://conwaylife.com/patterns/$pattern.cells" +echo "url: '$url'" +if test "$argv[2]" = --download; or test "$argv[2]" = -d + set p "/tmp/$pattern.cells" + if test -e $p + echo "already saved to '$p'" + else + echo "saving to '$p'" + curl $url -o $p + end +else + curl $url | cgol-tui - +end diff --git a/src/app.rs b/src/app.rs index b8cbbdf..dae226c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ pub use area::Area; pub use cell::Cell; -use ratatui::crossterm::event::{self, Event, KeyEventKind}; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::{backend::Backend, Terminal}; pub use shapes::HandleError; pub use std::str::FromStr; @@ -12,8 +12,6 @@ const DEF_DUR: Duration = Duration::from_millis(400); mod area; mod cell; -/// Keymaps to handle input events -mod kmaps; /// Starting shapes pub mod shapes; /// ui @@ -95,7 +93,7 @@ impl App { } pub fn restart(&mut self) { let univ = self.get(); - self.universe = Universe::from_figur(self.area, univ).unwrap(); + self.universe = Universe::from_figur(self.area, &univ).unwrap(); } pub fn tick(&mut self) { @@ -150,14 +148,14 @@ impl App { continue; } match key.code { - kmaps::QUIT => break, - kmaps::SLOWER => self.slower(false), - kmaps::FASTER => self.faster(false), - kmaps::PLAY_PAUSE => self.play_pause(&mut prev_poll_t), - kmaps::RESTART => self.restart(), - kmaps::NEXT => self.next(), - kmaps::PREV => self.prev(), - kmaps::RESET => *self = Self::default(), + KeyCode::Char('q') | KeyCode::Esc => break, + KeyCode::Char('j') | KeyCode::Down => self.slower(false), + KeyCode::Char('k') | KeyCode::Up => self.faster(false), + KeyCode::Char(' ') | KeyCode::Enter => self.play_pause(&mut prev_poll_t), + KeyCode::Char('r') => self.restart(), + KeyCode::Char('n') | KeyCode::Char('l') | KeyCode::Right => self.next(), + KeyCode::Char('p') | KeyCode::Char('h') | KeyCode::Left => self.prev(), + KeyCode::Char('R') | KeyCode::Backspace => *self = Self::default(), _ => {} } } else { diff --git a/src/app/kmaps.rs b/src/app/kmaps.rs deleted file mode 100644 index c12f3b2..0000000 --- a/src/app/kmaps.rs +++ /dev/null @@ -1,20 +0,0 @@ -use ratatui::crossterm::event::KeyCode; - -pub const PLAY_PAUSE: KeyCode = KeyCode::Char(' '); - -pub const SLOWER: KeyCode = KeyCode::Char('j'); -// pub const SLOWER_BIG: KeyCode = KeyCode::Char('J'); - -pub const FASTER: KeyCode = KeyCode::Char('k'); -// pub const FASTER_BIG: KeyCode = KeyCode::Char('K'); - -pub const QUIT: KeyCode = KeyCode::Char('q'); - -pub const RESTART: KeyCode = KeyCode::Char('r'); - -pub const RESET: KeyCode = KeyCode::Char('R'); - -pub const NEXT: KeyCode = KeyCode::Char('n'); -pub const PREV: KeyCode = KeyCode::Char('p'); - -// pub const HELP: KeyCode = KeyCode::Char('?'); diff --git a/src/app/shapes.rs b/src/app/shapes.rs index 2cf3c1e..60571a8 100644 --- a/src/app/shapes.rs +++ b/src/app/shapes.rs @@ -14,7 +14,7 @@ pub enum HandleError { pub fn all() -> Vec { vec![ - Universe::from_str(FEATHERWEIGTH_SPACESHIP).unwrap(), + Universe::from_str(GLIDER).unwrap(), Universe::from_str(GOSPER_GLIDER_GUN).unwrap(), Universe::from_str(COPPERHEAD).unwrap(), Universe::from_str(RABBITS).unwrap(), @@ -35,7 +35,7 @@ pub fn get_special(i: usize, area: Area) -> Universe { pub fn rand(area: Area) -> Universe { let cells = (0..area.len()).map(|_i| fastrand::bool().into()).collect(); - Universe { area, cells } + Universe::new(area, cells, "random") } pub fn stripes(area: Area) -> Universe { @@ -48,17 +48,17 @@ pub fn stripes(area: Area) -> Universe { } }) .collect(); - Universe { area, cells } + Universe::new(area, cells, "stripes") } pub fn empty(area: Area) -> Universe { let cells = vec![Cell::Dead; area.len()]; - Universe::new(area, cells) + Universe::new(area, cells, "empty") } pub fn full(area: Area) -> Universe { let cells = vec![Cell::Alive; area.len()]; - Universe { area, cells } + Universe::new(area, cells, "full") } /// height: 5 @@ -71,7 +71,7 @@ pub fn full(area: Area) -> Universe { /// 4.....4 /// 01234 pub fn frame(area: Area) -> Universe { - let mut univ = empty(area); + let mut univ = empty(area).with_name("frame"); if area.height < 3 || area.width < 3 { return univ; } @@ -92,6 +92,7 @@ pub fn frame(area: Area) -> Universe { } pub const COPPERHEAD: &str = "\ +!Name: Copperhead .....O.OO... ....O......O ...OO...O..O @@ -118,13 +119,15 @@ OO........O...O.OO....O.O "; /// 3x3 -pub const FEATHERWEIGTH_SPACESHIP: &str = "\ +pub const GLIDER: &str = "\ +!Name: Glider ..O O.O .OO"; /// 8x4 pub const RABBITS: &str = "\ +!Name: Rabbits O.....O. ..O...O. ..O..O.O @@ -132,6 +135,7 @@ O.....O. /// 3×5 pub const BONK_TIE: &str = "\ +!Name: Bonk tie OO OO ..O @@ -140,6 +144,7 @@ OO /// 7×3 pub const ACORN: &str = "\ +!Name: Acorn .O ...O OO..OOO"; diff --git a/src/app/shapes/tests.rs b/src/app/shapes/tests.rs index 3c41ce7..0df5e97 100644 --- a/src/app/shapes/tests.rs +++ b/src/app/shapes/tests.rs @@ -3,18 +3,20 @@ use super::*; #[test] fn frame_test00() { let area = Area::new(3, 2); - let univ = Universe::from_str("...\n..."); + let univ = Universe::from_str("...\n...").unwrap().with_name("frame"); let frame = frame(area); print!("{frame}"); - assert_eq!(univ, Ok(frame)); + assert_eq!(univ, frame); } #[test] fn frame_test0() { let area = Area::new(3, 3); - let univ = Universe::from_str("...\n.O.\n..."); + let univ = Universe::from_str("...\n.O.\n...") + .unwrap() + .with_name("frame"); let frame = frame(area); print!("{frame}"); - assert_eq!(univ, Ok(frame)); + assert_eq!(univ, frame); } #[test] fn frame_test1() { @@ -25,10 +27,12 @@ fn frame_test1() { .OO. .OO. ....", - ); + ) + .unwrap() + .with_name("frame"); let frame = frame(area); print!("{frame}"); - assert_eq!(univ, Ok(frame)); + assert_eq!(univ, frame); } #[test] fn frame_test2() { @@ -40,10 +44,12 @@ fn frame_test2() { .O.O. .OOO. .....", - ); + ) + .unwrap() + .with_name("frame"); let frame = frame(area); print!("{frame}"); - assert_eq!(univ, Ok(frame)); + assert_eq!(univ, frame); } #[test] fn frame_test3() { @@ -56,16 +62,18 @@ fn frame_test3() { .O..O. .OOOO. ......", - ); + ) + .unwrap() + .with_name("frame"); let frame = frame(area); print!("{frame}"); - assert_eq!(univ, Ok(frame)); + assert_eq!(univ, frame); } #[test] -fn featherweight_spaceship_test() { +fn glider_test() { let area = Area::new(3, 3); - let m = Universe::from_str(FEATHERWEIGTH_SPACESHIP).unwrap(); + let m = Universe::from_str(GLIDER).unwrap(); assert_eq!(m.area, area); dbg!(&m); let alive = [(0u8, 2u8), (1u8, 0u8), (1u8, 2u8), (2u8, 1u8), (2u8, 2u8)]; diff --git a/src/app/tests.rs b/src/app/tests.rs index 2289e6d..0679798 100644 --- a/src/app/tests.rs +++ b/src/app/tests.rs @@ -2,7 +2,7 @@ use super::*; fn gen_uni(area: Area, cells: &[bool]) -> Universe { let cells = cells.iter().map(|c| (*c).into()).collect::>(); - Universe { area, cells } + Universe::new(area, cells, "test") } #[test] @@ -13,7 +13,7 @@ OO..O ...O ..O"; - let univ = Universe::from_str(figur).unwrap(); + let univ = Universe::from_str(figur).unwrap().with_name("test"); let cells = [ /* 1st row */ false, false, true, false, false, /* 2nd row */ true, true, false, false, true, /* 3rd row */ false, false, false, false, false, @@ -30,7 +30,7 @@ fn full() { let full = shapes::full(area); let cells = vec![true; area.len()]; let uni = gen_uni(area, &cells); - assert_eq!(full, uni); + assert_eq!(full, uni.with_name("full")); } #[test] fn halp() { @@ -62,11 +62,8 @@ fn halp() { #[test] fn bigass_tickler() { let area = Area::new(8, 8); - let mut univ = Universe::from_figur( - area, - Universe::from_str(shapes::FEATHERWEIGTH_SPACESHIP).unwrap(), - ) - .unwrap(); + let mut univ = + Universe::from_figur(area, &Universe::from_str(shapes::GLIDER).unwrap()).unwrap(); let exp_unis = [ "\ @@ -369,7 +366,7 @@ O.O..... ]; for exp_uni in exp_unis.map(Universe::from_str) { - let exp_uni = exp_uni.unwrap(); + let exp_uni = exp_uni.unwrap().with_name("Glider"); println!("exp univ:\n{exp_uni}"); println!("univ:\n{univ}"); assert_eq!(univ, exp_uni); diff --git a/src/app/ui.rs b/src/app/ui.rs index 98c7d78..fae5423 100644 --- a/src/app/ui.rs +++ b/src/app/ui.rs @@ -28,7 +28,7 @@ pub fn ui(f: &mut Frame, app: &mut App) { let cgol = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .title("Conway's Game of Life"); + .title(format!("Conway's Game of Life - {} ", app.universe.name)); // 2 blocks less: border let new_area = Area::new( (chunks[0].width - 2) * BRAILLE.width, @@ -52,8 +52,7 @@ pub fn ui(f: &mut Frame, app: &mut App) { .constraints([Constraint::Percentage(100)]) .split(chunks[1]); - let current_keys_hint = - "[q]uit, [r]estart, [R]eset, [n]ext, [p]rev, play[ ]pause, speed: 'k' ↑, 'j' ↓".yellow(); + let current_keys_hint = "[q]uit, [r]estart, pause: [ ], nav: vim/arrows".yellow(); let poll_t = { if let std::time::Duration::MAX = app.poll_t { diff --git a/src/app/universe.rs b/src/app/universe.rs index ffb6cd7..e4d3b96 100644 --- a/src/app/universe.rs +++ b/src/app/universe.rs @@ -8,6 +8,7 @@ use super::shapes; pub struct Universe { pub area: Area, pub cells: Vec, + pub name: String, } impl, U2: Into> std::ops::Index<(U1, U2)> for Universe { type Output = Cell; @@ -33,9 +34,23 @@ impl, U2: Into> std::ops::IndexMut<(U1, U2)> for Universe } impl Universe { - pub fn new(area: Area, cells: Vec) -> Self { - Self { area, cells } + pub fn new(area: Area, cells: Vec, name: impl AsRef) -> Self { + Self { + area, + cells, + name: name.as_ref().into(), + } + } + pub fn with_name(self, name: impl AsRef) -> Self { + Self { + name: name.as_ref().into(), + ..self + } } + pub fn name(&self) -> String { + self.name.clone() + } + fn get_idx(&self, row: impl Into, col: impl Into) -> usize { let row = row.into(); let col = col.into(); @@ -79,15 +94,29 @@ impl Universe { /// Convert properly formatted Vec of Strings to Universe fn from_vec_str(s: &[String]) -> Result { - let width = s.iter().map(|ln| ln.chars().count()).max().unwrap_or(0) as u16; - let height = s.len() as u16; + let s = s.iter(); + let metadata = s.clone().filter(|l| l.starts_with('!')).collect::>(); + let pattern = s.filter(|l| !l.starts_with('!')).collect::>(); + + let width = pattern + .iter() + .map(|ln| ln.chars().count()) + .max() + .unwrap_or(0) as u16; + let height = pattern.len() as u16; let area = Area::new(width, height); let mut univ = shapes::empty(area); - for (i, line) in s.iter().enumerate() { - if line.starts_with('!') { - continue; // ignore comment - } + if let Some(name) = metadata.first() { + let name = name + .replace(".cells", "") + .replace('!', "") + .replace("Name:", ""); + let name = name.trim(); + univ.name = name.to_string(); + }; + + for (i, line) in pattern.iter().enumerate() { for (j, ch) in line.chars().enumerate() { univ[(i, j)] = ch.try_into()?; } @@ -101,7 +130,7 @@ impl Universe { /// # Errors /// /// if shape can't fit universe - pub fn from_figur(area: Area, figur: Universe) -> Result { + pub fn from_figur(area: Area, figur: &Universe) -> Result { let count_alive = |univ: &Universe| -> usize { univ.cells .iter() @@ -109,13 +138,13 @@ impl Universe { .count() }; - let figur_alive = count_alive(&figur); + let figur_alive = count_alive(figur); if area < figur.area { return Err(HandleError::TooBig); } - let mut univ = shapes::empty(area); + let mut univ = shapes::empty(area).with_name(figur.name()); let (start_row, start_col) = ( (area.height - figur.height()) / 2, diff --git a/src/main.rs b/src/main.rs index 1b32d6e..c846562 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,8 +30,8 @@ where is either a .cells file, or - for stdin" let universes = args .iter() .flat_map(std::fs::read_to_string) - .map(|s| Universe::from_str(&s)) - .collect::, _>>()?; + .flat_map(|s| Universe::from_str(&s)) + .collect::>(); let mut app = App::default().with_universes([universes, piped_universe].concat());