Repository: CookieCoder15/pipes-rs Branch: master Commit: 0183e472181e Files: 23 Total size: 46.7 KB Directory structure: gitextract_nbeg4cbc/ ├── .github/ │ └── workflows/ │ ├── cd.yaml │ └── ci.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md └── crates/ ├── model/ │ ├── Cargo.toml │ └── src/ │ ├── direction.rs │ ├── lib.rs │ ├── pipe/ │ │ ├── color.rs │ │ └── kind.rs │ ├── pipe.rs │ └── position.rs ├── pipes-rs/ │ ├── Cargo.toml │ └── src/ │ ├── config.rs │ ├── lib.rs │ ├── main.rs │ └── usage ├── rng/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs └── terminal/ ├── Cargo.toml └── src/ ├── lib.rs └── screen.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/cd.yaml ================================================ name: CD on: push: tags: - "v*" env: RELEASE_BIN: pipes-rs jobs: create_release: name: Create release runs-on: ubuntu-latest outputs: upload_url: ${{ steps.step.outputs.upload_url }} steps: - uses: softprops/action-gh-release@v1 id: step env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} build_release: name: Build release strategy: matrix: include: - os: ubuntu-latest targets: [x86_64-unknown-linux-gnu] suffix: linux-x86_64.tar.gz - os: macos-latest targets: [x86_64-apple-darwin, aarch64-apple-darwin] suffix: mac-universal.tar.gz - os: windows-latest targets: [x86_64-pc-windows-msvc] suffix: windows-x86_64.zip runs-on: ${{ matrix.os }} needs: create_release steps: - uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ join(matrix.targets, ',') }} - name: Build shell: bash run: | for target in ${{ join(matrix.targets, ' ') }}; do cargo build --release --target $target done - name: Create Linux archive run: tar -czvf ./${{ env.RELEASE_BIN }}-linux-x86_64.tar.gz ./target/x86_64-unknown-linux-gnu/release/${{ env.RELEASE_BIN }} if: matrix.os == 'ubuntu-latest' - name: Create Windows archive run: 7z a -tzip ./${{ env.RELEASE_BIN }}-windows-x86_64.zip ./target/x86_64-pc-windows-msvc/release/${{ env.RELEASE_BIN }}.exe if: matrix.os == 'windows-latest' - name: Create macOS archive run: | lipo -create -output ./${{ env.RELEASE_BIN }}-mac-universal ./target/x86_64-apple-darwin/release/${{ env.RELEASE_BIN }} ./target/aarch64-apple-darwin/release/${{ env.RELEASE_BIN }} codesign --sign - --options runtime ./${{ env.RELEASE_BIN }}-mac-universal tar -czvf ./${{ env.RELEASE_BIN }}-mac-universal.tar.gz ./${{ env.RELEASE_BIN }}-mac-universal if: matrix.os == 'macos-latest' - name: Upload archive uses: shogo82148/actions-upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create_release.outputs.upload_url }} asset_path: ./${{ env.RELEASE_BIN }}-${{ matrix.suffix }} asset_name: ${{ env.RELEASE_BIN }}-${{ matrix.suffix }} - name: Get version id: get-version run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\//} if: matrix.os == 'macos-latest' - name: Bump Homebrew formula uses: mislav/bump-homebrew-formula-action@v3 with: formula-name: pipes-rs homebrew-tap: lhvy/homebrew-tap download-url: https://github.com/lhvy/pipes-rs/releases/download/${{ steps.get-version.outputs.version }}/pipes-rs-mac-universal.tar.gz env: COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }} if: matrix.os == 'macos-latest' ================================================ FILE: .github/workflows/ci.yaml ================================================ name: CI on: pull_request: push: branches: master paths: - "**.rs" - "**.toml" - "**.lock" - "**.yaml" env: RUSTFLAGS: "--deny warnings --warn unreachable-pub" jobs: rust: name: Rust strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: components: clippy - name: Load Rust/Cargo cache uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --all-targets --all-features --locked - name: Clippy run: cargo clippy --all-targets --all-features - name: Test run: cargo test --all-targets --all-features --locked fmt: name: Formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: components: rustfmt - name: Check formatting run: cargo fmt -- --check ================================================ FILE: .gitignore ================================================ /target ================================================ FILE: Cargo.toml ================================================ [workspace] members = ["crates/*"] resolver = "2" [profile.dev] panic = "abort" [profile.release] panic = "abort" codegen-units = 1 lto = "fat" strip = true ================================================ FILE: LICENSE.md ================================================ # Blue Oak Model License Version 1.0.0 ## Purpose This license gives everyone as much permission to work with this software as possible, while protecting contributors from liability. ## Acceptance In order to receive this license, you must agree to its rules. The rules of this license are both obligations under that agreement and conditions to your license. You must not do anything with this software that triggers a rule that you cannot or will not follow. ## Copyright Each contributor licenses you to do everything with this software that would otherwise infringe that contributor's copyright in it. ## Notices You must ensure that everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license or a link to . ## Excuse If anyone notifies you in writing that you have not complied with [Notices](#notices), you can keep your license by taking all practical steps to comply within 30 days after the notice. If you do not do so, your license ends immediately. ## Patent Each contributor licenses you to do everything with this software that would otherwise infringe any patent claims they can license or become able to license. ## Reliability No contributor can revoke this license. ## No Liability **_As far as the law allows, this software comes as is, without any warranty or condition, and no contributor will be liable to anyone for any damages related to this software or this license, under any kind of legal claim._** ================================================ FILE: README.md ================================================ # pipes-rs ![GitHub Actions CI status](https://github.com/lhvy/pipes-rs/actions/workflows/ci.yaml/badge.svg) > An over-engineered rewrite of pipes.sh in Rust ![pipes-rs preview](https://github.com/lhvy/i/raw/master/pipes-rs-preview.gif) ## Installation Install the latest version release via git on any platform using Cargo: ```sh cargo install --git https://github.com/lhvy/pipes-rs ``` Alternatively for macOS, install via Homebrew: ```sh brew install lhvy/tap/pipes-rs ``` Alternatively for Windows, install via Scoop: ```sh scoop bucket add extras scoop install pipes-rs ``` ### Manual Download Download compiled binaries from [releases](https://github.com/lhvy/pipes-rs/releases/latest). ## Windows Font Issues There have been reports that some characters pipes-rs uses are missing on Windows, which causes them to appear as [tofu](https://en.wikipedia.org/wiki/Noto_fonts#Etymology). If you experience this issue, try using a font with a large character set, such as [Noto Mono](https://www.google.com/get/noto/). ## Keybindings - r: reset the screen - q or ^C: exit the program ## Configuration pipes-rs can be configured using TOML located at `~/.config/pipes-rs/config.toml`. The following is an example file with the default settings: ```toml bold = true color_mode = "ansi" # ansi, rgb or none palette = "default" # default, darker, pastel or matrix rainbow = 0 # 0-255 delay_ms = 20 inherit_style = false kinds = ["heavy"] # heavy, light, curved, knobby, emoji, outline, dots, blocks, sus num_pipes = 1 reset_threshold = 0.5 # 0.0–1.0 turn_chance = 0.15 # 0.0–1.0 ``` ### Color Modes | Mode | Description | | :----- | :-------------------------------------------------------------------------------- | | `ansi` | pipe colors are randomly selected from the terminal color profile, default option | | `rgb` | pipe colors are randomly generated rgb values, unsupported in some terminals | | `none` | pipe colors will not be set and use the current terminal text color | ### Palettes | Palette | Description | | :-------- | :--------------------------------------------------------------- | | `default` | bright colors – good on dark backgrounds, default option | | `darker` | darker colors – good on light backgrounds | | `pastel` | pastel colors – good on dark backgrounds | | `matrix` | colors based on [Matrix digital rain] – good on dark backgrounds | ### Pipe Kinds | Kind | Preview | | :-------- | :------------------------ | | `heavy` | `┃ ┃ ━ ━ ┏ ┓ ┗ ┛` | | `light` | `│ │ ─ ─ ┌ ┐ └ ┘` | | `curved` | `│ │ ─ ─ ╭ ╮ ╰ ╯` | | `knobby` | `╽ ╿ ╼ ╾ ┎ ┒ ┖ ┚` | | `emoji` | `👆 👇 👈 👉 👌 👌 👌 👌` | | `outline` | `║ ║ ═ ═ ╔ ╗ ╚ ╝` | | `dots` | `• • • • • • • •` | | `blocks` | `█ █ ▀ ▀ █ █ ▀ ▀` | | `sus` | `ඞ ඞ ඞ ඞ ඞ ඞ ඞ ඞ` | _Due to emojis having a different character width, using the emoji pipe kind along side another pipe kind can cause spacing issues._ ## Options There are also command line options that can be used to override parts of the configuration file: | Option | Usage | Example | | :---------- | :-------------------------------------------------------------------------------- | :----------------- | | `-b` | toggles bold text | `-b true` | | `-c` | sets the color mode | `-c rgb` | | `-d` | sets the delay in ms | `-d 15` | | `-i` | toggles if pipes inherit style when hitting the edge | `-i false` | | `-k` | sets the kinds of pipes, each kind separated by commas | `-k heavy,curved` | | `-p` | sets the number of pipes on screen | `-p 5` | | `-r` | sets the percentage of the screen to be filled before resetting | `-r 0.75` | | `-t` | chance of a pipe turning each frame | `-t 0.15` | | `--palette` | sets the color palette, RGB mode only | `--palette pastel` | | `--rainbow` | sets the number of degrees per frame to shift the hue of each pipe, RGB mode only | `--rainbow 5` | ## Credits ### Contributors pipes-rs is maintained by [lhvy](https://github.com/lhvy) and [lunacookies](https://github.com/lunacookies); any other contributions via PRs are welcome! Forks and modifications are implicitly licensed under the Blue Oak Model License 1.0.0. Please credit the above contributors and pipes.sh when making forks or other derivative works. ### Inspiration This project is based off of [pipes.sh](https://github.com/pipeseroni/pipes.sh). [matrix digital rain]: https://en.wikipedia.org/wiki/Matrix_digital_rain ================================================ FILE: crates/model/Cargo.toml ================================================ [package] edition = "2021" license = "BlueOak-1.0.0" name = "model" version = "0.0.0" [dependencies] anyhow = "1.0.70" rng = {path = "../rng"} serde = {version = "1.0.159", features = ["derive"]} terminal = {path = "../terminal"} tincture = "1.0.0" ================================================ FILE: crates/model/src/direction.rs ================================================ #[derive(Clone, Copy, PartialEq)] pub(crate) enum Direction { Up, Down, Left, Right, } impl Direction { pub(crate) fn maybe_turn(self, turn_chance: f32) -> Direction { if !rng::gen_bool(turn_chance) { return self; } if rng::gen_bool(0.5) { // turn left match self { Direction::Up => Direction::Left, Direction::Down => Direction::Right, Direction::Left => Direction::Up, Direction::Right => Direction::Down, } } else { // turn right match self { Direction::Up => Direction::Right, Direction::Down => Direction::Left, Direction::Left => Direction::Down, Direction::Right => Direction::Up, } } } } ================================================ FILE: crates/model/src/lib.rs ================================================ mod direction; pub mod pipe; pub mod position; ================================================ FILE: crates/model/src/pipe/color.rs ================================================ use std::ops::Range; #[derive(Clone, Copy)] pub struct Color { pub terminal: terminal::Color, pub(crate) oklch: Option, } impl Color { pub(crate) fn update(&mut self, hue_shift: f32) { if let Some(oklch) = &mut self.oklch { oklch.h += hue_shift.to_radians(); let oklab = tincture::oklch_to_oklab(*oklch); let lrgb = tincture::oklab_to_linear_srgb(oklab); let srgb = tincture::linear_srgb_to_srgb(lrgb); self.terminal = terminal::Color::Rgb { r: (srgb.r * 255.0) as u8, g: (srgb.g * 255.0) as u8, b: (srgb.b * 255.0) as u8, }; } } } pub(super) fn gen_random_color(color_mode: ColorMode, palette: Palette) -> Option { match color_mode { ColorMode::Ansi => Some(gen_random_ansi_color()), ColorMode::Rgb => Some(gen_random_rgb_color(palette)), ColorMode::None => None, } } fn gen_random_ansi_color() -> Color { let num = rng::gen_range(0..12); Color { terminal: match num { 0 => terminal::Color::Red, 1 => terminal::Color::DarkRed, 2 => terminal::Color::Green, 3 => terminal::Color::DarkGreen, 4 => terminal::Color::Yellow, 5 => terminal::Color::DarkYellow, 6 => terminal::Color::Blue, 7 => terminal::Color::DarkBlue, 8 => terminal::Color::Magenta, 9 => terminal::Color::DarkMagenta, 10 => terminal::Color::Cyan, 11 => terminal::Color::DarkCyan, _ => unreachable!(), }, oklch: None, } } fn gen_random_rgb_color(palette: Palette) -> Color { let hue = rng::gen_range_float(palette.get_hue_range()); let lightness = rng::gen_range_float(palette.get_lightness_range()); let oklch = tincture::Oklch { l: lightness, c: palette.get_chroma(), h: hue.to_radians(), }; let oklab = tincture::oklch_to_oklab(oklch); let lrgb = tincture::oklab_to_linear_srgb(oklab); let srgb = tincture::linear_srgb_to_srgb(lrgb); debug_assert!( (0.0..=1.0).contains(&srgb.r) && (0.0..=1.0).contains(&srgb.g) && (0.0..=1.0).contains(&srgb.b) ); Color { terminal: terminal::Color::Rgb { r: (srgb.r * 255.0) as u8, g: (srgb.g * 255.0) as u8, b: (srgb.b * 255.0) as u8, }, oklch: Some(oklch), } } #[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum ColorMode { Ansi, Rgb, None, } #[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum Palette { Default, Darker, Pastel, Matrix, } impl Palette { pub(super) fn get_hue_range(self) -> Range { match self { Self::Matrix => 145.0..145.0, _ => 0.0..360.0, } } pub(super) fn get_lightness_range(self) -> Range { match self { Self::Default => 0.75..0.75, Self::Darker => 0.65..0.65, Self::Pastel => 0.8..0.8, Self::Matrix => 0.5..0.9, } } pub(super) fn get_chroma(self) -> f32 { match self { Self::Default => 0.125, Self::Darker => 0.11, Self::Pastel => 0.085, Self::Matrix => 0.11, } } } ================================================ FILE: crates/model/src/pipe/kind.rs ================================================ use std::num::NonZeroUsize; use std::str::FromStr; #[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum Kind { Heavy, Light, Curved, Knobby, Emoji, Outline, Dots, Blocks, Sus, } impl Kind { pub fn up(self) -> char { self.chars()[0] } pub fn down(self) -> char { self.chars()[1] } pub fn left(self) -> char { self.chars()[2] } pub fn right(self) -> char { self.chars()[3] } pub fn top_left(self) -> char { self.chars()[4] } pub fn top_right(self) -> char { self.chars()[5] } pub fn bottom_left(self) -> char { self.chars()[6] } pub fn bottom_right(self) -> char { self.chars()[7] } fn chars(self) -> [char; 8] { match self { Self::Heavy => Self::HEAVY, Self::Light => Self::LIGHT, Self::Curved => Self::CURVED, Self::Knobby => Self::KNOBBY, Self::Emoji => Self::EMOJI, Self::Outline => Self::OUTLINE, Self::Dots => Self::DOTS, Self::Blocks => Self::BLOCKS, Self::Sus => Self::SUS, } } fn width(self) -> KindWidth { match self { Self::Dots | Self::Sus => KindWidth::Custom(NonZeroUsize::new(2).unwrap()), _ => KindWidth::Auto, } } const HEAVY: [char; 8] = ['┃', '┃', '━', '━', '┏', '┓', '┗', '┛']; const LIGHT: [char; 8] = ['│', '│', '─', '─', '┌', '┐', '└', '┘']; const CURVED: [char; 8] = ['│', '│', '─', '─', '╭', '╮', '╰', '╯']; const KNOBBY: [char; 8] = ['╽', '╿', '╼', '╾', '┎', '┒', '┖', '┚']; const EMOJI: [char; 8] = ['👆', '👇', '👈', '👉', '👌', '👌', '👌', '👌']; const OUTLINE: [char; 8] = ['║', '║', '═', '═', '╔', '╗', '╚', '╝']; const DOTS: [char; 8] = ['•', '•', '•', '•', '•', '•', '•', '•']; const BLOCKS: [char; 8] = ['█', '█', '▀', '▀', '█', '█', '▀', '▀']; const SUS: [char; 8] = ['ඞ', 'ඞ', 'ඞ', 'ඞ', 'ඞ', 'ඞ', 'ඞ', 'ඞ']; } #[derive(Clone, Copy)] enum KindWidth { Auto, Custom(NonZeroUsize), } #[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct KindSet(Vec); impl FromStr for KindSet { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let mut set = Vec::new(); for kind in s.split(',') { let kind = match kind { "heavy" => Kind::Heavy, "light" => Kind::Light, "curved" => Kind::Curved, "knobby" => Kind::Knobby, "emoji" => Kind::Emoji, "outline" => Kind::Outline, "dots" => Kind::Dots, "blocks" => Kind::Blocks, "sus" => Kind::Sus, _ => anyhow::bail!( r#"unknown pipe kind (expected “heavy”, “light”, “curved”, “knobby”, “emoji”, “outline”, “dots”, “blocks”, or “sus”)"#, ), }; if !set.contains(&kind) { set.push(kind); } } Ok(Self(set)) } } impl KindSet { pub fn from_one(kind: Kind) -> Self { Self(vec![kind]) } pub fn choose_random(&self) -> Kind { let idx = rng::gen_range(0..self.0.len() as u32); self.0[idx as usize] } pub fn chars(&self) -> impl Iterator + '_ { self.0.iter().flat_map(|kind| kind.chars()) } pub fn custom_widths(&self) -> impl Iterator + '_ { self.0.iter().filter_map(|kind| match kind.width() { KindWidth::Custom(n) => Some(n), KindWidth::Auto => None, }) } } ================================================ FILE: crates/model/src/pipe.rs ================================================ mod color; mod kind; pub use color::{ColorMode, Palette}; pub use kind::{Kind, KindSet}; use self::color::Color; use crate::direction::Direction; use crate::position::{InScreenBounds, Position}; pub struct Pipe { current_direction: Direction, previous_direction: Direction, pub position: Position, pub color: Option, kind: Kind, } impl Pipe { pub fn new(size: (u16, u16), color_mode: ColorMode, palette: Palette, kind: Kind) -> Self { let color = color::gen_random_color(color_mode, palette); let (direction, position) = gen_random_direction_and_position(size); Self { current_direction: direction, previous_direction: direction, position, color, kind, } } pub fn dup(&self, size: (u16, u16)) -> Self { let (direction, position) = gen_random_direction_and_position(size); Self { current_direction: direction, previous_direction: direction, position, color: self.color, kind: self.kind, } } pub fn tick(&mut self, size: (u16, u16), turn_chance: f32, hue_shift: u8) -> InScreenBounds { let InScreenBounds(in_screen_bounds) = self.position.move_in(self.current_direction, size); if let Some(color) = &mut self.color { color.update(hue_shift.into()); } if !in_screen_bounds { return InScreenBounds(false); } self.previous_direction = self.current_direction; self.current_direction = self.current_direction.maybe_turn(turn_chance); InScreenBounds(true) } pub fn to_char(&self) -> char { match (self.previous_direction, self.current_direction) { (Direction::Up, Direction::Left) | (Direction::Right, Direction::Down) => { self.kind.top_right() } (Direction::Up, Direction::Right) | (Direction::Left, Direction::Down) => { self.kind.top_left() } (Direction::Down, Direction::Left) | (Direction::Right, Direction::Up) => { self.kind.bottom_right() } (Direction::Down, Direction::Right) | (Direction::Left, Direction::Up) => { self.kind.bottom_left() } (Direction::Up, Direction::Up) => self.kind.up(), (Direction::Down, Direction::Down) => self.kind.down(), (Direction::Left, Direction::Left) => self.kind.left(), (Direction::Right, Direction::Right) => self.kind.right(), _ => unreachable!(), } } } fn gen_random_direction_and_position((columns, rows): (u16, u16)) -> (Direction, Position) { let direction = match rng::gen_range(0..4) { 0 => Direction::Up, 1 => Direction::Down, 2 => Direction::Left, 3 => Direction::Right, _ => unreachable!(), }; let position = match direction { Direction::Up => Position { x: rng::gen_range_16(0..columns), y: rows - 1, }, Direction::Down => Position { x: rng::gen_range_16(0..columns), y: 0, }, Direction::Left => Position { x: columns - 1, y: rng::gen_range_16(0..rows), }, Direction::Right => Position { x: 0, y: rng::gen_range_16(0..rows), }, }; (direction, position) } ================================================ FILE: crates/model/src/position.rs ================================================ use crate::direction::Direction; pub struct Position { pub x: u16, pub y: u16, } impl Position { pub(crate) fn move_in(&mut self, dir: Direction, size: (u16, u16)) -> InScreenBounds { match dir { Direction::Up => { if self.y == 0 { return InScreenBounds(false); } self.y -= 1; } Direction::Down => self.y += 1, Direction::Left => { if self.x == 0 { return InScreenBounds(false); } self.x -= 1; } Direction::Right => self.x += 1, } InScreenBounds(self.in_screen_bounds(size)) } fn in_screen_bounds(&self, (columns, rows): (u16, u16)) -> bool { self.x < columns && self.y < rows } } #[derive(PartialEq, Eq)] pub struct InScreenBounds(pub bool); ================================================ FILE: crates/pipes-rs/Cargo.toml ================================================ [package] description = "An over-engineered rewrite of pipes.sh in Rust" edition = "2021" license = "BlueOak-1.0.0" name = "pipes-rs" version = "1.6.4" [dependencies] anyhow = "1.0.70" home = "0.5.5" mimalloc = { version = "0.1.36", default-features = false } model = { path = "../model" } rng = { path = "../rng" } serde = "1.0.159" terminal = { path = "../terminal" } toml = "0.8.2" ================================================ FILE: crates/pipes-rs/src/config.rs ================================================ use anyhow::Context; use model::pipe::{ColorMode, Kind, KindSet, Palette}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::time::Duration; use std::{env, fs}; #[derive(Serialize, Deserialize, Default)] pub struct Config { pub color_mode: Option, pub palette: Option, pub rainbow: Option, pub delay_ms: Option, pub fps: Option, pub reset_threshold: Option, pub kinds: Option, pub bold: Option, pub inherit_style: Option, pub num_pipes: Option, pub turn_chance: Option, } impl Config { pub fn read() -> anyhow::Result { let config = Self::read_from_disk_with_default()?; config.validate()?; Ok(config) } fn read_from_disk_with_default() -> anyhow::Result { let path = Self::path()?; if !path.exists() { return Ok(Config::default()); } Self::read_from_disk(path) } fn path() -> anyhow::Result { let config_dir = 'config_dir: { if let Ok(d) = env::var("XDG_CONFIG_HOME") { let d = PathBuf::from(d); if d.is_absolute() { break 'config_dir d; } } match home::home_dir() { Some(d) => d.join(".config/"), None => anyhow::bail!("could not determine home directory"), } }; Ok(config_dir.join("pipes-rs/config.toml")) } fn read_from_disk(path: PathBuf) -> anyhow::Result { let contents = fs::read_to_string(path)?; toml::from_str(&contents).context("failed to read config") } pub fn validate(&self) -> anyhow::Result<()> { if let Some(reset_threshold) = self.reset_threshold() { if !(0.0..=1.0).contains(&reset_threshold) { anyhow::bail!("reset threshold should be within 0 and 1") } } if !(0.0..=1.0).contains(&self.turn_chance()) { anyhow::bail!("turn chance should be within 0 and 1") } if self.delay_ms.is_some() && self.fps.is_some() { anyhow::bail!("both delay and FPS can’t be set simultaneously"); } Ok(()) } pub fn color_mode(&self) -> ColorMode { self.color_mode.unwrap_or(ColorMode::Ansi) } pub fn palette(&self) -> Palette { self.palette.unwrap_or(Palette::Default) } pub fn rainbow(&self) -> u8 { self.rainbow.unwrap_or(0) } pub fn tick_length(&self) -> Duration { if let Some(fps) = self.fps { if fps == 0.0 { return Duration::ZERO; } return Duration::from_secs_f32(1.0 / fps); } if let Some(delay_ms) = self.delay_ms { return Duration::from_millis(delay_ms); // assume rendering a frame takes no time } Duration::from_secs_f32(1.0 / 50.0) // default to 50 FPS } pub fn reset_threshold(&self) -> Option { match self.reset_threshold { Some(0.0) => None, Some(n) => Some(n), None => Some(0.5), } } pub fn kinds(&self) -> KindSet { self.kinds .clone() .unwrap_or_else(|| KindSet::from_one(Kind::Heavy)) } pub fn bold(&self) -> bool { self.bold.unwrap_or(true) } pub fn inherit_style(&self) -> bool { self.inherit_style.unwrap_or(false) } pub fn num_pipes(&self) -> u32 { self.num_pipes.unwrap_or(1) } pub fn turn_chance(&self) -> f32 { self.turn_chance.unwrap_or(0.15) } } ================================================ FILE: crates/pipes-rs/src/lib.rs ================================================ mod config; pub use config::Config; use model::pipe::{KindSet, Pipe}; use model::position::InScreenBounds; use std::{io, thread, time}; use terminal::{Event, Terminal}; pub struct App { terminal: Terminal, config: Config, kinds: KindSet, } impl App { pub fn new(config: Config) -> anyhow::Result { let kinds = config.kinds(); let stdout = io::stdout().lock(); let largest_custom_width = kinds.custom_widths().max(); let terminal = Terminal::new(stdout, kinds.chars(), largest_custom_width)?; Ok(Self { terminal, config, kinds, }) } pub fn run(mut self) -> anyhow::Result<()> { self.terminal.enter_alternate_screen()?; self.terminal.set_raw_mode(true)?; self.terminal.set_cursor_visibility(false)?; if self.config.bold() { self.terminal.enable_bold()?; } let mut pipes = self.create_pipes(); loop { if let ControlFlow::Break = self.reset_loop(&mut pipes)? { break; } } self.terminal.set_raw_mode(false)?; self.terminal.set_cursor_visibility(true)?; self.terminal.leave_alternate_screen()?; Ok(()) } pub fn reset_loop(&mut self, pipes: &mut Vec) -> anyhow::Result { self.terminal.clear()?; for pipe in &mut *pipes { *pipe = self.create_pipe(); } while self.under_threshold() { let control_flow = self.tick_loop(pipes)?; match control_flow { ControlFlow::Break | ControlFlow::Reset => return Ok(control_flow), ControlFlow::Continue => {} } } Ok(ControlFlow::Continue) } pub fn tick_loop(&mut self, pipes: &mut Vec) -> anyhow::Result { let start_time = time::Instant::now(); match self.terminal.get_event()? { Some(Event::Exit) => return Ok(ControlFlow::Break), Some(Event::Reset) => return Ok(ControlFlow::Reset), None => {} } for pipe in pipes { self.render_pipe(pipe)?; self.tick_pipe(pipe); } self.terminal.flush()?; let tick_length_so_far = start_time.elapsed(); // If we’ve taken more time than we have a budget for, // then just don’t sleep. let took_too_long = tick_length_so_far >= self.config.tick_length(); if !took_too_long { thread::sleep(self.config.tick_length() - tick_length_so_far); } Ok(ControlFlow::Continue) } fn tick_pipe(&mut self, pipe: &mut Pipe) { let InScreenBounds(stayed_onscreen) = pipe.tick( self.terminal.size(), self.config.turn_chance(), self.config.rainbow(), ); if !stayed_onscreen { *pipe = if self.config.inherit_style() { pipe.dup(self.terminal.size()) } else { self.create_pipe() }; } } fn render_pipe(&mut self, pipe: &Pipe) -> anyhow::Result<()> { self.terminal .move_cursor_to(pipe.position.x, pipe.position.y)?; if let Some(color) = pipe.color { self.terminal.set_text_color(color.terminal)?; } self.terminal.print(if rng::gen_bool(0.99999) { pipe.to_char() } else { '🦀' })?; Ok(()) } pub fn create_pipes(&mut self) -> Vec { (0..self.config.num_pipes()) .map(|_| self.create_pipe()) .collect() } fn create_pipe(&mut self) -> Pipe { let kind = self.kinds.choose_random(); Pipe::new( self.terminal.size(), self.config.color_mode(), self.config.palette(), kind, ) } fn under_threshold(&self) -> bool { match self.config.reset_threshold() { Some(reset_threshold) => self.terminal.portion_covered() < reset_threshold, None => true, } } } #[must_use] pub enum ControlFlow { Continue, Break, Reset, } ================================================ FILE: crates/pipes-rs/src/main.rs ================================================ use mimalloc::MiMalloc; use model::pipe::{ColorMode, Palette}; use pipes_rs::{App, Config}; use std::{env, process}; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; fn main() -> anyhow::Result<()> { let mut config = Config::read()?; parse_args(&mut config); config.validate()?; let app = App::new(config)?; app.run()?; Ok(()) } fn parse_args(config: &mut Config) { let args: Vec<_> = env::args().skip(1).collect(); let mut args_i = args.iter(); while let Some(arg) = args_i.next() { match arg.as_str() { "--license" => { if args.len() != 1 { eprintln!("error: provided arguments other than --license"); process::exit(1); } println!("pipes-rs is licensed under the Blue Oak Model License 1.0.0,"); println!("the text of which you will find below."); println!("\n{}", include_str!("../../../LICENSE.md")); process::exit(0); } "--version" | "-V" => { if args.len() != 1 { eprintln!("error: provided arguments other than --version"); process::exit(1); } println!("pipes-rs {}", env!("CARGO_PKG_VERSION")); process::exit(0); } "--help" | "-h" => { println!("{}", include_str!("usage")); process::exit(0); } _ => {} } if !arg.starts_with('-') { eprintln!("error: unexpected argument “{arg}” found"); eprintln!("see --help"); process::exit(1); } let (option, value) = arg.split_once('=').unwrap_or_else(|| match args_i.next() { Some(value) => (arg, value), None => required_value(arg), }); match option { "--color-mode" | "-c" => { config.color_mode = match value { "ansi" => Some(ColorMode::Ansi), "rgb" => Some(ColorMode::Rgb), "none" => Some(ColorMode::None), _ => invalid_value(option, value, "“ansi”, “rgb” or “none”"), } } "--palette" => { config.palette = match value { "default" => Some(Palette::Default), "darker" => Some(Palette::Darker), "pastel" => Some(Palette::Pastel), "matrix" => Some(Palette::Matrix), _ => invalid_value(option, value, "“default”, “darker”, “pastel” or “matrix”"), } } "--rainbow" => { config.rainbow = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "an integer between 0 and 255"), } } "--delay" | "-d" => { config.delay_ms = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "a positive integer"), } } "--fps" | "-f" => { config.fps = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "a number"), } } "--reset-threshold" | "-r" => { config.reset_threshold = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "a number"), } } "--kinds" | "-k" => { config.kinds = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "kinds of pipes separated by commas"), } } "--bold" | "-b" => { config.bold = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "“true” or “false”"), } } "--inherit-style" | "-i" => { config.inherit_style = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "“true” or “false”"), } } "--pipe-num" | "-p" => { config.num_pipes = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "a positive integer"), } } "--turn-chance" | "-t" => { config.turn_chance = match value.parse() { Ok(v) => Some(v), Err(_) => invalid_value(option, value, "a number"), } } _ => { eprintln!("error: unrecognized option {option}"); eprintln!("see --help"); process::exit(1); } } } } fn required_value(option: &str) -> ! { eprintln!("error: a value is required for {option} but none was supplied"); eprintln!("see --help"); process::exit(1); } fn invalid_value(option: &str, actual: &str, expected: &str) -> ! { eprintln!("error: invalid value “{actual}” for {option}"); eprintln!(" expected {expected}"); eprintln!("see --help"); process::exit(1); } ================================================ FILE: crates/pipes-rs/src/usage ================================================ An over-engineered rewrite of pipes.sh in Rust. Usage: pipes-rs [OPTIONS] Options: -c, --color-mode what kind of terminal coloring to use --palette the color palette used assign colors to pipes --rainbow cycle hue of pipes -d, --delay delay between frames in milliseconds -f, --fps number of frames of animation that are displayed in a second; use 0 for unlimited -r, --reset-threshold portion of screen covered before resetting (0.0–1.0) -k, --kinds kinds of pipes separated by commas, e.g. heavy,curved -b, --bold whether to use bold [possible values: true, false] -i, --inherit-style whether pipes should retain style after hitting the edge [possible values: true, false] -p, --pipe-num number of pipes -t, --turn-chance chance of a pipe turning (0.0–1.0) --license Print license -h, --help Print help -V, --version Print version ================================================ FILE: crates/rng/Cargo.toml ================================================ [package] edition = "2021" license = "BlueOak-1.0.0" name = "rng" version = "0.0.0" [dependencies] once_cell = "1.17.1" oorandom = "11.1.3" parking_lot = "0.12.1" ================================================ FILE: crates/rng/src/lib.rs ================================================ use once_cell::sync::Lazy; use parking_lot::Mutex; use std::ops::Range; static RNG: Lazy> = Lazy::new(|| { let seed = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("system time cannot be before unix epoch") .as_millis() as u64; Mutex::new(Rng { rand_32: oorandom::Rand32::new(seed), }) }); struct Rng { rand_32: oorandom::Rand32, } pub fn gen_range(range: Range) -> u32 { RNG.lock().rand_32.rand_range(range) } pub fn gen_range_float(range: Range) -> f32 { RNG.lock().rand_32.rand_float() * (range.end - range.start) + range.start } pub fn gen_range_16(range: Range) -> u16 { RNG.lock() .rand_32 .rand_range(range.start as u32..range.end as u32) as u16 } pub fn gen_bool(probability: f32) -> bool { assert!(probability >= 0.0); assert!(probability <= 1.0); RNG.lock().rand_32.rand_float() < probability } ================================================ FILE: crates/terminal/Cargo.toml ================================================ [package] edition = "2021" license = "BlueOak-1.0.0" name = "terminal" version = "0.0.0" [dependencies] anyhow = "1.0.70" crossterm = "0.27.0" unicode-width = "0.1.10" ================================================ FILE: crates/terminal/src/lib.rs ================================================ mod screen; use crossterm::event::{ self, Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, }; use crossterm::{cursor, queue, style, terminal}; use screen::Screen; use std::io::{self, Write}; use std::num::NonZeroUsize; use std::time::Duration; use unicode_width::UnicodeWidthChar; pub struct Terminal { screen: Screen, stdout: io::StdoutLock<'static>, max_char_width: u16, size: (u16, u16), } impl Terminal { pub fn new( stdout: io::StdoutLock<'static>, chars: impl Iterator, custom_width: Option, ) -> anyhow::Result { let max_char_width = Self::determine_max_char_width(chars, custom_width); let size = { let (width, height) = terminal::size()?; (width / max_char_width, height) }; let screen = Screen::new(size.0 as usize, size.1 as usize); Ok(Self { screen, stdout, max_char_width, size, }) } fn determine_max_char_width( chars: impl Iterator, custom_width: Option, ) -> u16 { let max_char_width = chars.map(|c| c.width().unwrap() as u16).max().unwrap(); match custom_width { Some(custom_width) => max_char_width.max(custom_width.get() as u16), None => max_char_width, } } pub fn enable_bold(&mut self) -> anyhow::Result<()> { queue!(self.stdout, style::SetAttribute(style::Attribute::Bold))?; Ok(()) } pub fn reset_style(&mut self) -> anyhow::Result<()> { queue!(self.stdout, style::SetAttribute(style::Attribute::Reset))?; Ok(()) } pub fn set_cursor_visibility(&mut self, visible: bool) -> anyhow::Result<()> { if visible { queue!(self.stdout, cursor::Show)?; } else { queue!(self.stdout, cursor::Hide)?; } Ok(()) } pub fn clear(&mut self) -> anyhow::Result<()> { queue!(self.stdout, terminal::Clear(terminal::ClearType::All))?; self.screen.clear(); Ok(()) } pub fn set_raw_mode(&self, enabled: bool) -> anyhow::Result<()> { if enabled { terminal::enable_raw_mode()?; } else { terminal::disable_raw_mode()?; } Ok(()) } pub fn enter_alternate_screen(&mut self) -> anyhow::Result<()> { queue!(self.stdout, terminal::EnterAlternateScreen)?; Ok(()) } pub fn leave_alternate_screen(&mut self) -> anyhow::Result<()> { queue!(self.stdout, terminal::LeaveAlternateScreen)?; Ok(()) } pub fn set_text_color(&mut self, color: Color) -> anyhow::Result<()> { let color = style::Color::from(color); queue!(self.stdout, style::SetForegroundColor(color))?; Ok(()) } pub fn move_cursor_to(&mut self, x: u16, y: u16) -> anyhow::Result<()> { queue!(self.stdout, cursor::MoveTo(x * self.max_char_width, y))?; self.screen.move_cursor_to(x as usize, y as usize); Ok(()) } pub fn portion_covered(&self) -> f32 { self.screen.portion_covered() } pub fn size(&self) -> (u16, u16) { self.size } pub fn print(&mut self, c: char) -> anyhow::Result<()> { self.screen.print(); self.stdout.write_all(c.to_string().as_bytes())?; Ok(()) } pub fn flush(&mut self) -> anyhow::Result<()> { self.stdout.flush()?; Ok(()) } pub fn get_event(&mut self) -> anyhow::Result> { if !event::poll(Duration::ZERO)? { return Ok(None); } match event::read()? { CrosstermEvent::Resize(width, height) => { self.resize(width, height); Ok(Some(Event::Reset)) } CrosstermEvent::Key( KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. } | KeyEvent { code: KeyCode::Char('q'), kind: KeyEventKind::Press, .. }, ) => Ok(Some(Event::Exit)), CrosstermEvent::Key(KeyEvent { code: KeyCode::Char('r'), .. }) => Ok(Some(Event::Reset)), _ => Ok(None), } } fn resize(&mut self, width: u16, height: u16) { self.size = (width, height); self.screen.resize(width as usize, height as usize); } } #[derive(Clone, Copy)] pub enum Color { Red, DarkRed, Green, DarkGreen, Yellow, DarkYellow, Blue, DarkBlue, Magenta, DarkMagenta, Cyan, DarkCyan, Rgb { r: u8, g: u8, b: u8 }, } impl From for style::Color { fn from(color: Color) -> Self { match color { Color::Red => Self::Red, Color::DarkRed => Self::DarkRed, Color::Green => Self::Green, Color::DarkGreen => Self::DarkGreen, Color::Yellow => Self::Yellow, Color::DarkYellow => Self::DarkYellow, Color::Blue => Self::Blue, Color::DarkBlue => Self::DarkBlue, Color::Magenta => Self::Magenta, Color::DarkMagenta => Self::DarkMagenta, Color::Cyan => Self::Cyan, Color::DarkCyan => Self::DarkCyan, Color::Rgb { r, g, b } => Self::Rgb { r, g, b }, } } } pub enum Event { Exit, Reset, } ================================================ FILE: crates/terminal/src/screen.rs ================================================ pub(crate) struct Screen { cells: Vec, cursor: (usize, usize), width: usize, height: usize, num_covered: usize, } impl Screen { pub(crate) fn new(width: usize, height: usize) -> Self { Self { cells: vec![Cell { is_covered: false }; width * height], cursor: (0, 0), width, height, num_covered: 0, } } pub(crate) fn resize(&mut self, width: usize, height: usize) { self.cells .resize(width * height, Cell { is_covered: false }); self.cursor = (0, 0); self.width = width; self.height = height; self.clear(); } pub(crate) fn move_cursor_to(&mut self, x: usize, y: usize) { assert!(x < self.width); assert!(y < self.height); self.cursor = (x, y); } pub(crate) fn print(&mut self) { let current_cell = self.current_cell(); if !current_cell.is_covered { current_cell.is_covered = true; self.num_covered += 1; } } pub(crate) fn clear(&mut self) { for cell in &mut self.cells { cell.is_covered = false; } self.num_covered = 0; } pub(crate) fn portion_covered(&self) -> f32 { debug_assert_eq!( self.num_covered, self.cells.iter().filter(|c| c.is_covered).count() ); self.num_covered as f32 / self.cells.len() as f32 } fn current_cell(&mut self) -> &mut Cell { &mut self.cells[self.cursor.1 * self.width + self.cursor.0] } } #[derive(Clone, Copy)] struct Cell { is_covered: bool, }