Full Code of CookieCoder15/pipes-rs for AI

master 0183e472181e cached
23 files
46.7 KB
12.7k tokens
116 symbols
1 requests
Download .txt
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
<https://blueoakcouncil.org/license/1.0.0>.

## 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

- <kbd>r</kbd>: reset the screen
- <kbd>q</kbd> or <kbd>^C</kbd>: 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<tincture::Oklch>,
}

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<Color> {
    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<f32> {
        match self {
            Self::Matrix => 145.0..145.0,
            _ => 0.0..360.0,
        }
    }

    pub(super) fn get_lightness_range(self) -> Range<f32> {
        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<Kind>);

impl FromStr for KindSet {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        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<Item = char> + '_ {
        self.0.iter().flat_map(|kind| kind.chars())
    }

    pub fn custom_widths(&self) -> impl Iterator<Item = NonZeroUsize> + '_ {
        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<Color>,
    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<ColorMode>,
    pub palette: Option<Palette>,
    pub rainbow: Option<u8>,
    pub delay_ms: Option<u64>,
    pub fps: Option<f32>,
    pub reset_threshold: Option<f32>,
    pub kinds: Option<KindSet>,
    pub bold: Option<bool>,
    pub inherit_style: Option<bool>,
    pub num_pipes: Option<u32>,
    pub turn_chance: Option<f32>,
}

impl Config {
    pub fn read() -> anyhow::Result<Self> {
        let config = Self::read_from_disk_with_default()?;
        config.validate()?;

        Ok(config)
    }

    fn read_from_disk_with_default() -> anyhow::Result<Self> {
        let path = Self::path()?;

        if !path.exists() {
            return Ok(Config::default());
        }

        Self::read_from_disk(path)
    }

    fn path() -> anyhow::Result<PathBuf> {
        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<Self> {
        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<f32> {
        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<Self> {
        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<Pipe>) -> anyhow::Result<ControlFlow> {
        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<Pipe>) -> anyhow::Result<ControlFlow> {
        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<Pipe> {
        (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 <COLOR_MODE>            what kind of terminal coloring to use
      --palette <PALETTE>                  the color palette used assign colors to pipes
      --rainbow <DEGREES>                  cycle hue of pipes
  -d, --delay <DELAY_MS>                   delay between frames in milliseconds
  -f, --fps <FPS>                          number of frames of animation that are displayed in a second; use 0 for unlimited
  -r, --reset-threshold <RESET_THRESHOLD>  portion of screen covered before resetting (0.0–1.0)
  -k, --kinds <KINDS>                      kinds of pipes separated by commas, e.g. heavy,curved
  -b, --bold <BOOL>                        whether to use bold [possible values: true, false]
  -i, --inherit-style <BOOL>               whether pipes should retain style after hitting the edge [possible values: true, false]
  -p, --pipe-num <NUM>                     number of pipes
  -t, --turn-chance <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<Mutex<Rng>> = 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>) -> u32 {
    RNG.lock().rand_32.rand_range(range)
}

pub fn gen_range_float(range: Range<f32>) -> f32 {
    RNG.lock().rand_32.rand_float() * (range.end - range.start) + range.start
}

pub fn gen_range_16(range: Range<u16>) -> 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<Item = char>,
        custom_width: Option<NonZeroUsize>,
    ) -> anyhow::Result<Self> {
        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<Item = char>,
        custom_width: Option<NonZeroUsize>,
    ) -> 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<Option<Event>> {
        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<Color> 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<Cell>,
    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,
}
Download .txt
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
Download .txt
SYMBOL INDEX (116 symbols across 11 files)

FILE: crates/model/src/direction.rs
  type Direction (line 2) | pub(crate) enum Direction {
    method maybe_turn (line 10) | pub(crate) fn maybe_turn(self, turn_chance: f32) -> Direction {

FILE: crates/model/src/pipe.rs
  type Pipe (line 11) | pub struct Pipe {
    method new (line 20) | pub fn new(size: (u16, u16), color_mode: ColorMode, palette: Palette, ...
    method dup (line 33) | pub fn dup(&self, size: (u16, u16)) -> Self {
    method tick (line 45) | pub fn tick(&mut self, size: (u16, u16), turn_chance: f32, hue_shift: ...
    method to_char (line 62) | pub fn to_char(&self) -> char {
  function gen_random_direction_and_position (line 85) | fn gen_random_direction_and_position((columns, rows): (u16, u16)) -> (Di...

FILE: crates/model/src/pipe/color.rs
  type Color (line 4) | pub struct Color {
    method update (line 10) | pub(crate) fn update(&mut self, hue_shift: f32) {
  function gen_random_color (line 25) | pub(super) fn gen_random_color(color_mode: ColorMode, palette: Palette) ...
  function gen_random_ansi_color (line 33) | fn gen_random_ansi_color() -> Color {
  function gen_random_rgb_color (line 56) | fn gen_random_rgb_color(palette: Palette) -> Color {
  type ColorMode (line 86) | pub enum ColorMode {
  type Palette (line 94) | pub enum Palette {
    method get_hue_range (line 102) | pub(super) fn get_hue_range(self) -> Range<f32> {
    method get_lightness_range (line 109) | pub(super) fn get_lightness_range(self) -> Range<f32> {
    method get_chroma (line 118) | pub(super) fn get_chroma(self) -> f32 {

FILE: crates/model/src/pipe/kind.rs
  type Kind (line 6) | pub enum Kind {
    method up (line 19) | pub fn up(self) -> char {
    method down (line 23) | pub fn down(self) -> char {
    method left (line 27) | pub fn left(self) -> char {
    method right (line 31) | pub fn right(self) -> char {
    method top_left (line 35) | pub fn top_left(self) -> char {
    method top_right (line 39) | pub fn top_right(self) -> char {
    method bottom_left (line 43) | pub fn bottom_left(self) -> char {
    method bottom_right (line 47) | pub fn bottom_right(self) -> char {
    method chars (line 51) | fn chars(self) -> [char; 8] {
    method width (line 65) | fn width(self) -> KindWidth {
    constant HEAVY (line 72) | const HEAVY: [char; 8] = ['┃', '┃', '━', '━', '┏', '┓', '┗', '┛'];
    constant LIGHT (line 73) | const LIGHT: [char; 8] = ['│', '│', '─', '─', '┌', '┐', '└', '┘'];
    constant CURVED (line 74) | const CURVED: [char; 8] = ['│', '│', '─', '─', '╭', '╮', '╰', '╯'];
    constant KNOBBY (line 75) | const KNOBBY: [char; 8] = ['╽', '╿', '╼', '╾', '┎', '┒', '┖', '┚'];
    constant EMOJI (line 76) | const EMOJI: [char; 8] = ['👆', '👇', '👈', '👉', '👌', '👌', '👌', '👌'];
    constant OUTLINE (line 77) | const OUTLINE: [char; 8] = ['║', '║', '═', '═', '╔', '╗', '╚', '╝'];
    constant DOTS (line 78) | const DOTS: [char; 8] = ['•', '•', '•', '•', '•', '•', '•', '•'];
    constant BLOCKS (line 79) | const BLOCKS: [char; 8] = ['█', '█', '▀', '▀', '█', '█', '▀', '▀'];
    constant SUS (line 80) | const SUS: [char; 8] = ['ඞ', 'ඞ', 'ඞ', 'ඞ', 'ඞ', 'ඞ', 'ඞ', 'ඞ'];
  type KindWidth (line 84) | enum KindWidth {
  type KindSet (line 90) | pub struct KindSet(Vec<Kind>);
    method from_one (line 124) | pub fn from_one(kind: Kind) -> Self {
    method choose_random (line 128) | pub fn choose_random(&self) -> Kind {
    method chars (line 133) | pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
    method custom_widths (line 137) | pub fn custom_widths(&self) -> impl Iterator<Item = NonZeroUsize> + '_ {
  type Err (line 93) | type Err = anyhow::Error;
  method from_str (line 95) | fn from_str(s: &str) -> Result<Self, Self::Err> {

FILE: crates/model/src/position.rs
  type Position (line 3) | pub struct Position {
    method move_in (line 9) | pub(crate) fn move_in(&mut self, dir: Direction, size: (u16, u16)) -> ...
    method in_screen_bounds (line 30) | fn in_screen_bounds(&self, (columns, rows): (u16, u16)) -> bool {
  type InScreenBounds (line 36) | pub struct InScreenBounds(pub bool);

FILE: crates/pipes-rs/src/config.rs
  type Config (line 9) | pub struct Config {
    method read (line 24) | pub fn read() -> anyhow::Result<Self> {
    method read_from_disk_with_default (line 31) | fn read_from_disk_with_default() -> anyhow::Result<Self> {
    method path (line 41) | fn path() -> anyhow::Result<PathBuf> {
    method read_from_disk (line 59) | fn read_from_disk(path: PathBuf) -> anyhow::Result<Self> {
    method validate (line 64) | pub fn validate(&self) -> anyhow::Result<()> {
    method color_mode (line 82) | pub fn color_mode(&self) -> ColorMode {
    method palette (line 86) | pub fn palette(&self) -> Palette {
    method rainbow (line 90) | pub fn rainbow(&self) -> u8 {
    method tick_length (line 94) | pub fn tick_length(&self) -> Duration {
    method reset_threshold (line 109) | pub fn reset_threshold(&self) -> Option<f32> {
    method kinds (line 117) | pub fn kinds(&self) -> KindSet {
    method bold (line 123) | pub fn bold(&self) -> bool {
    method inherit_style (line 127) | pub fn inherit_style(&self) -> bool {
    method num_pipes (line 131) | pub fn num_pipes(&self) -> u32 {
    method turn_chance (line 135) | pub fn turn_chance(&self) -> f32 {

FILE: crates/pipes-rs/src/lib.rs
  type App (line 9) | pub struct App {
    method new (line 16) | pub fn new(config: Config) -> anyhow::Result<Self> {
    method run (line 30) | pub fn run(mut self) -> anyhow::Result<()> {
    method reset_loop (line 53) | pub fn reset_loop(&mut self, pipes: &mut Vec<Pipe>) -> anyhow::Result<...
    method tick_loop (line 71) | pub fn tick_loop(&mut self, pipes: &mut Vec<Pipe>) -> anyhow::Result<C...
    method tick_pipe (line 99) | fn tick_pipe(&mut self, pipe: &mut Pipe) {
    method render_pipe (line 115) | fn render_pipe(&mut self, pipe: &Pipe) -> anyhow::Result<()> {
    method create_pipes (line 132) | pub fn create_pipes(&mut self) -> Vec<Pipe> {
    method create_pipe (line 138) | fn create_pipe(&mut self) -> Pipe {
    method under_threshold (line 149) | fn under_threshold(&self) -> bool {
  type ControlFlow (line 158) | pub enum ControlFlow {

FILE: crates/pipes-rs/src/main.rs
  function main (line 9) | fn main() -> anyhow::Result<()> {
  function parse_args (line 20) | fn parse_args(config: &mut Config) {
  function required_value (line 159) | fn required_value(option: &str) -> ! {
  function invalid_value (line 165) | fn invalid_value(option: &str, actual: &str, expected: &str) -> ! {

FILE: crates/rng/src/lib.rs
  type Rng (line 16) | struct Rng {
  function gen_range (line 20) | pub fn gen_range(range: Range<u32>) -> u32 {
  function gen_range_float (line 24) | pub fn gen_range_float(range: Range<f32>) -> f32 {
  function gen_range_16 (line 28) | pub fn gen_range_16(range: Range<u16>) -> u16 {
  function gen_bool (line 34) | pub fn gen_bool(probability: f32) -> bool {

FILE: crates/terminal/src/lib.rs
  type Terminal (line 13) | pub struct Terminal {
    method new (line 21) | pub fn new(
    method determine_max_char_width (line 43) | fn determine_max_char_width(
    method enable_bold (line 55) | pub fn enable_bold(&mut self) -> anyhow::Result<()> {
    method reset_style (line 60) | pub fn reset_style(&mut self) -> anyhow::Result<()> {
    method set_cursor_visibility (line 65) | pub fn set_cursor_visibility(&mut self, visible: bool) -> anyhow::Resu...
    method clear (line 75) | pub fn clear(&mut self) -> anyhow::Result<()> {
    method set_raw_mode (line 82) | pub fn set_raw_mode(&self, enabled: bool) -> anyhow::Result<()> {
    method enter_alternate_screen (line 92) | pub fn enter_alternate_screen(&mut self) -> anyhow::Result<()> {
    method leave_alternate_screen (line 97) | pub fn leave_alternate_screen(&mut self) -> anyhow::Result<()> {
    method set_text_color (line 102) | pub fn set_text_color(&mut self, color: Color) -> anyhow::Result<()> {
    method move_cursor_to (line 109) | pub fn move_cursor_to(&mut self, x: u16, y: u16) -> anyhow::Result<()> {
    method portion_covered (line 116) | pub fn portion_covered(&self) -> f32 {
    method size (line 120) | pub fn size(&self) -> (u16, u16) {
    method print (line 124) | pub fn print(&mut self, c: char) -> anyhow::Result<()> {
    method flush (line 131) | pub fn flush(&mut self) -> anyhow::Result<()> {
    method get_event (line 136) | pub fn get_event(&mut self) -> anyhow::Result<Option<Event>> {
    method resize (line 170) | fn resize(&mut self, width: u16, height: u16) {
  type Color (line 177) | pub enum Color {
  function from (line 194) | fn from(color: Color) -> Self {
  type Event (line 213) | pub enum Event {

FILE: crates/terminal/src/screen.rs
  type Screen (line 1) | pub(crate) struct Screen {
    method new (line 10) | pub(crate) fn new(width: usize, height: usize) -> Self {
    method resize (line 20) | pub(crate) fn resize(&mut self, width: usize, height: usize) {
    method move_cursor_to (line 29) | pub(crate) fn move_cursor_to(&mut self, x: usize, y: usize) {
    method print (line 36) | pub(crate) fn print(&mut self) {
    method clear (line 44) | pub(crate) fn clear(&mut self) {
    method portion_covered (line 51) | pub(crate) fn portion_covered(&self) -> f32 {
    method current_cell (line 59) | fn current_cell(&mut self) -> &mut Cell {
  type Cell (line 65) | struct Cell {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (51K chars).
[
  {
    "path": ".github/workflows/cd.yaml",
    "chars": 3108,
    "preview": "name: CD\non:\n  push:\n    tags:\n      - \"v*\"\n\nenv:\n  RELEASE_BIN: pipes-rs\n\njobs:\n  create_release:\n    name: Create rele"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "chars": 1110,
    "preview": "name: CI\n\non:\n  pull_request:\n  push:\n    branches: master\n    paths:\n      - \"**.rs\"\n      - \"**.toml\"\n      - \"**.lock"
  },
  {
    "path": ".gitignore",
    "chars": 8,
    "preview": "/target\n"
  },
  {
    "path": "Cargo.toml",
    "chars": 159,
    "preview": "[workspace]\nmembers = [\"crates/*\"]\nresolver = \"2\"\n\n[profile.dev]\npanic = \"abort\"\n\n[profile.release]\npanic = \"abort\"\ncode"
  },
  {
    "path": "LICENSE.md",
    "chars": 1550,
    "preview": "# Blue Oak Model License\n\nVersion 1.0.0\n\n## Purpose\n\nThis license gives everyone as much permission to work with\nthis so"
  },
  {
    "path": "README.md",
    "chars": 5336,
    "preview": "# pipes-rs\n\n![GitHub Actions CI status](https://github.com/lhvy/pipes-rs/actions/workflows/ci.yaml/badge.svg)\n\n> An over"
  },
  {
    "path": "crates/model/Cargo.toml",
    "chars": 250,
    "preview": "[package]\nedition = \"2021\"\nlicense = \"BlueOak-1.0.0\"\nname = \"model\"\nversion = \"0.0.0\"\n\n[dependencies]\nanyhow = \"1.0.70\"\n"
  },
  {
    "path": "crates/model/src/direction.rs",
    "chars": 874,
    "preview": "#[derive(Clone, Copy, PartialEq)]\npub(crate) enum Direction {\n    Up,\n    Down,\n    Left,\n    Right,\n}\n\nimpl Direction {"
  },
  {
    "path": "crates/model/src/lib.rs",
    "chars": 47,
    "preview": "mod direction;\npub mod pipe;\npub mod position;\n"
  },
  {
    "path": "crates/model/src/pipe/color.rs",
    "chars": 3503,
    "preview": "use std::ops::Range;\n\n#[derive(Clone, Copy)]\npub struct Color {\n    pub terminal: terminal::Color,\n    pub(crate) oklch:"
  },
  {
    "path": "crates/model/src/pipe/kind.rs",
    "chars": 3772,
    "preview": "use std::num::NonZeroUsize;\nuse std::str::FromStr;\n\n#[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash, "
  },
  {
    "path": "crates/model/src/pipe.rs",
    "chars": 3498,
    "preview": "mod color;\nmod kind;\n\npub use color::{ColorMode, Palette};\npub use kind::{Kind, KindSet};\n\nuse self::color::Color;\nuse c"
  },
  {
    "path": "crates/model/src/position.rs",
    "chars": 912,
    "preview": "use crate::direction::Direction;\n\npub struct Position {\n    pub x: u16,\n    pub y: u16,\n}\n\nimpl Position {\n    pub(crate"
  },
  {
    "path": "crates/pipes-rs/Cargo.toml",
    "chars": 386,
    "preview": "[package]\ndescription = \"An over-engineered rewrite of pipes.sh in Rust\"\nedition = \"2021\"\nlicense = \"BlueOak-1.0.0\"\nname"
  },
  {
    "path": "crates/pipes-rs/src/config.rs",
    "chars": 3707,
    "preview": "use anyhow::Context;\nuse model::pipe::{ColorMode, Kind, KindSet, Palette};\nuse serde::{Deserialize, Serialize};\nuse std:"
  },
  {
    "path": "crates/pipes-rs/src/lib.rs",
    "chars": 4232,
    "preview": "mod config;\npub use config::Config;\n\nuse model::pipe::{KindSet, Pipe};\nuse model::position::InScreenBounds;\nuse std::{io"
  },
  {
    "path": "crates/pipes-rs/src/main.rs",
    "chars": 5518,
    "preview": "use mimalloc::MiMalloc;\nuse model::pipe::{ColorMode, Palette};\nuse pipes_rs::{App, Config};\nuse std::{env, process};\n\n#["
  },
  {
    "path": "crates/pipes-rs/src/usage",
    "chars": 1245,
    "preview": "An over-engineered rewrite of pipes.sh in Rust.\n\nUsage: pipes-rs [OPTIONS]\n\nOptions:\n  -c, --color-mode <COLOR_MODE>    "
  },
  {
    "path": "crates/rng/Cargo.toml",
    "chars": 164,
    "preview": "[package]\nedition = \"2021\"\nlicense = \"BlueOak-1.0.0\"\nname = \"rng\"\nversion = \"0.0.0\"\n\n[dependencies]\nonce_cell = \"1.17.1\""
  },
  {
    "path": "crates/rng/src/lib.rs",
    "chars": 957,
    "preview": "use once_cell::sync::Lazy;\nuse parking_lot::Mutex;\nuse std::ops::Range;\n\nstatic RNG: Lazy<Mutex<Rng>> = Lazy::new(|| {\n "
  },
  {
    "path": "crates/terminal/Cargo.toml",
    "chars": 169,
    "preview": "[package]\nedition = \"2021\"\nlicense = \"BlueOak-1.0.0\"\nname = \"terminal\"\nversion = \"0.0.0\"\n\n[dependencies]\nanyhow = \"1.0.7"
  },
  {
    "path": "crates/terminal/src/lib.rs",
    "chars": 5678,
    "preview": "mod screen;\n\nuse crossterm::event::{\n    self, Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,\n}"
  },
  {
    "path": "crates/terminal/src/screen.rs",
    "chars": 1658,
    "preview": "pub(crate) struct Screen {\n    cells: Vec<Cell>,\n    cursor: (usize, usize),\n    width: usize,\n    height: usize,\n    nu"
  }
]

About this extraction

This page contains the full source code of the CookieCoder15/pipes-rs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (46.7 KB), approximately 12.7k tokens, and a symbol index with 116 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!