From 74e294771040a7a03a3ad7ac989bbd73d59fcf99 Mon Sep 17 00:00:00 2001 From: oiwn Date: Thu, 30 Oct 2025 13:56:17 +0700 Subject: [PATCH 1/5] add docs with guide how to prepare for release --- specs/reddit_post.md | 75 -------------------------------------------- specs/release.md | 66 ++++++++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 91 deletions(-) delete mode 100644 specs/reddit_post.md diff --git a/specs/reddit_post.md b/specs/reddit_post.md deleted file mode 100644 index 4bd3c4d..0000000 --- a/specs/reddit_post.md +++ /dev/null @@ -1,75 +0,0 @@ -# Tarts: Beautiful terminal screensavers in Rust - new release - ---- - -# **Title:** Tarts: Beautiful terminal screensavers in Rust - v0.1.24 - ---- - -## **Post Body:** - -Hey r/rust! I'd like to share my project **Tarts** - a collection of -terminal-based screensavers written in Rust. Just released v0.1.24 with some -exciting updates! - -## ๐ŸŽจ **What is Tarts?** - -Tarts is a lightweight, fast collection of terminal screensavers that brings -visual effects to your terminal. Think of it as the Linux `cmatrix` but with a -dozen different effects and modern Rust implementation. - -## โœจ **New in v0.1.24:** - -- **Removed unmaintained dependencies** - Removed CLI parsing dep for even smaller binariy -- **New help system** - Run `tarts --help` to see all available effects with descriptions -- **Better CLI experience** - Added `--version` flag and improved error handling -- **Homebrew tap** - Easy installation on macOS - -## ๐ŸŽญ **Featured Effects:** - -**Digital Rain** - Authentic Matrix-style digital rain with smooth animation and character flow - -**Maze Generation** - Real-time maze generation with perfect algorithms - -**3D Donut** - Classic 3D donut rotation with proper shading and perspective - -**And 8 more effects:** -- Conway's Game of Life (it looks terrible, need to make it interesting) -- Boids flocking simulation (need to improve) -- 3D Cube rotation -- Fire simulation -- Plasma effects -- Pipe maze animation -- ASCII crabs - -## ๐Ÿš€ **Installation:** - -### macOS (Recommended): -```bash -brew install oiwn/tap/tarts -``` - -### Anywhere via Cargo: -```bash -cargo install tarts -``` - -## ๐Ÿ’ป **Usage:** - -```bash -# Run any effect -tarts matrix -tarts maze -tarts donut - -# See all effects -tarts --help -``` - -## ๐Ÿ”ฎ **What's Next:** - -- Polish and optimize existing effects -- Add configuration system for customization -- More effect ideas and community contributions welcome! - -**GitHub:** https://github.com/oiwn/tarts diff --git a/specs/release.md b/specs/release.md index b1480c7..67a6e73 100644 --- a/specs/release.md +++ b/specs/release.md @@ -22,7 +22,7 @@ git pull origin main ### Step 2: Automated Release (GitHub Actions) 1. Push to main triggers `.github/workflows/release.yml` 2. GitHub Action: - - Builds binaries for macOS (x86_64, arm64), Linux + - Builds binaries for macOS (x86_64, arm64), Linux (not implemented yet) - Creates GitHub Release with changelog - Uploads release assets - Publishes to crates.io @@ -92,21 +92,55 @@ Could create GitHub Action that: brew install oiwn/tap/tarts cargo install tarts ``` -``` -### Homebrew Tap Update Script -```bash -#!/bin/bash -VERSION="0.1.24" # get this from Cargo.toml -URL="https://github.com/oiwn/tarts/releases/download/v${VERSION}/tarts-macos-x86_64" -SHA256=$(curl -L -s "$URL" | shasum -a 256 | cut -d' ' -f1) - -# Update tarts.rb with sed or similar -sed -i.bak "s/version \".*\"/version \"${VERSION}\"/" tarts.rb -sed -i.bak "s/sha256 \".*\"/sha256 \"${SHA256}\"/" tarts.rb -``` ## Current Status -- Ready to release v0.1.24 with lag fixes +- Ready to release v0.1.24 with pico-args removal and CLI improvements - Homebrew tap currently at v0.1.23 -- Release workflow configured and tested -- Documentation updated +- Release v0.1.24 is live: https://github.com/oiwn/tarts/releases/tag/v0.1.24 +- Need to update Homebrew tap to v0.1.24 + +## v0.1.24 Tap Update - Immediate Action Needed + +### Current Formula Details +- File: https://raw.githubusercontent.com/oiwn/homebrew-tap/refs/heads/main/tarts.rb +- Current version: v0.1.23 +- Current SHA256: `707c10aa58a41cc8ec3e995db640411b73c0b21343d3ec3200faec0a49d19d38` + +### Updated Formula Requirements +- New version: v0.1.24 +- New URL: `https://github.com/oiwn/tarts/archive/refs/tags/v0.1.24.tar.gz` +- New SHA256: `__NEED_TO_CALCULATE__` + +### Steps to Calculate SHA256 +```bash +curl -sL https://github.com/oiwn/tarts/archive/refs/tags/v0.1.24.tar.gz | sha256sum +``` + +### Why Build from Source? +- Bypass Apple's Gatekeeper warnings on macOS +- Ensure compatibility with user's system architecture +- Allow optimization for user's specific CPU +- Reduce maintenance burden (no need to build binaries for multiple platforms) + +### Updated Formula Preview +```ruby +class Tarts < Formula + desc "Terminal Arts - Screen savers and visual effects for terminal" + homepage "https://github.com/oiwn/tarts" + license "MIT" + + url "https://github.com/oiwn/tarts/archive/refs/tags/v0.1.24.tar.gz" + sha256 "__NEW_SHA256_HERE__" # Replace with actual SHA256 + version "0.1.24" + + depends_on "rust" => :build + + def install + system "cargo", "install", *std_cargo_args + end + + test do + assert_match version.to_s, shell_output("#{bin}/tarts --version") + end +end +``` From b4d54de1f4aa7105990510911c0d8fdc21e3d415 Mon Sep 17 00:00:00 2001 From: oiwn Date: Mon, 11 May 2026 01:56:54 +0700 Subject: [PATCH 2/5] update deps, basic global config implementation --- .tmuxp.yaml | 2 +- CHANGELOG.md | 23 ++- Cargo.lock | 390 ++++++++++++++++++++++++++++++--------- Cargo.toml | 10 +- specs/cleanup.md | 58 ++++++ specs/ctx.md | 1 + specs/current_task.md | 80 -------- specs/record_gif.md | 10 + src/blank/effect.rs | 3 +- src/blank/mod.rs | 2 +- src/boids/effect.rs | 71 ++++--- src/boids/mod.rs | 2 +- src/config.rs | 253 ++++++++++++------------- src/crab/effect.rs | 8 +- src/cube/effect.rs | 10 +- src/donut/effect.rs | 5 +- src/error.rs | 2 - src/fire/effect.rs | 4 +- src/lib.rs | 24 +++ src/life/effect.rs | 6 +- src/main.rs | 92 ++++----- src/maze/effect.rs | 2 +- src/pipes/effect.rs | 8 +- src/plasma/effect.rs | 2 +- src/rain/digital_rain.rs | 10 +- src/rain/rain_drop.rs | 2 +- src/terrain/effect.rs | 2 +- 27 files changed, 671 insertions(+), 411 deletions(-) create mode 100644 specs/cleanup.md create mode 100644 specs/ctx.md delete mode 100644 specs/current_task.md diff --git a/.tmuxp.yaml b/.tmuxp.yaml index 8832d14..aeb8b36 100644 --- a/.tmuxp.yaml +++ b/.tmuxp.yaml @@ -11,7 +11,7 @@ windows: start-directory: ./ panes: - shell_command: - - clear + - opencode - window_name: cmd1 start-directory: ./ panes: diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e720b..e2e8832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. +## [0.1.25] - 2026-05-11 + +### Added +- Config system: loads from `~/.config/tarts.toml` (macOS/Linux) or `%APPDATA%/tarts.toml` (Windows) +- `--print-config` flag: prints default config as TOML to stdout (pipe to file) +- Screen-adaptive coefficients: effects scale naturally to terminal size (`drops_coeff`, `speed_coeff`, `cells_coeff`, `boid_coeff`, `crab_coeff`, `k1_coeff`) +- `BoidCharset` enum: Braille (default), Arrow, Simple, Dot โ€” configurable boid character sets +- `src/lib.rs` module-level docstring with effect description table + +### Changed +- Builder defaults aligned with original `DefaultOptions` values: cube size, fire colors, donut projection, pipes density, plasma speed, terrain scale, crab count +- `Config::load()` returns defaults in memory when config file missing (no auto-write) +- Config status printed after effect exits (loaded path or "using defaults") +- Updated dependencies: rand 0.10, toml 1.x, criterion 0.8, tempfile 3.27 + +### Removed +- `--generate-config` flag (replaced by `--print-config`) + ## [0.1.24] - 2025-01-27 ### Changed @@ -22,12 +40,9 @@ All notable changes to this project will be documented in this file. - **Full control** over CLI behavior - **Better user experience** with proper help and error messages -### Effects Available -- matrix, life, maze, boids, cube, crab, donut, pipes, plasma, fire, blank, terrain - ## [0.1.23] - Previous ### Features - Multiple terminal screensaver effects - Homebrew tap integration -- Cross-platform support (macOS, Linux) \ No newline at end of file +- Cross-platform support (macOS, Linux) diff --git a/Cargo.lock b/Cargo.lock index 15c8b40..caec990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "anes" version = "0.1.6" @@ -67,6 +76,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "autocfg" version = "1.5.0" @@ -91,12 +106,33 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -164,20 +200,31 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "criterion" -version = "0.6.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", - "itertools 0.13.0", + "itertools", "num-traits", "oorandom", + "page_size", "plotters", "rayon", "regex", @@ -189,12 +236,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools 0.10.5", + "itertools", ] [[package]] @@ -423,12 +470,24 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "getrandom" version = "0.2.16" @@ -442,14 +501,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core", "wasip2", + "wasip3", ] [[package]] @@ -465,9 +526,30 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "ident_case" @@ -477,12 +559,14 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -491,15 +575,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -549,11 +624,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" @@ -567,9 +648,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" @@ -643,6 +724,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -710,12 +801,13 @@ dependencies = [ ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "zerocopy", + "proc-macro2", + "syn", ] [[package]] @@ -738,38 +830,26 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom 0.4.2", "rand_core", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" @@ -842,9 +922,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -880,6 +960,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -925,13 +1011,19 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" @@ -987,7 +1079,7 @@ dependencies = [ [[package]] name = "tarts" -version = "0.1.24" +version = "0.1.25" dependencies = [ "criterion", "crossterm", @@ -1004,12 +1096,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1047,44 +1139,42 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "unicode-ident" @@ -1098,6 +1188,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1126,7 +1222,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -1174,6 +1279,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" version = "0.3.82" @@ -1306,12 +1445,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wit-bindgen" @@ -1319,6 +1455,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "zerocopy" version = "0.8.27" diff --git a/Cargo.toml b/Cargo.toml index d2d86a7..d7ca82d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tarts" # shortcut from Terminal Arts -version = "0.1.24" +version = "0.1.25" edition = "2024" authors = ["oiwn "] @@ -17,14 +17,14 @@ directories = "6.0" env_logger = "0.11" log = "0.4" -rand = "0.9" +rand = "0.10" serde = { version = "1", features = ["derive"] } thiserror = "2" -toml = "0.8" +toml = "1" [dev-dependencies] -criterion = "0.6" -tempfile = "3.20" +criterion = "0.8" +tempfile = "3.27" [[bench]] name = "rain_benchmarks" diff --git a/specs/cleanup.md b/specs/cleanup.md new file mode 100644 index 0000000..8583cf1 --- /dev/null +++ b/specs/cleanup.md @@ -0,0 +1,58 @@ +# Cleanup / Technical Debt Notes + +## Config vs Runtime Calculations + +The current config structs (e.g. `DigitalRainOptions`, `BoidsOptions`, `CrabOptions`) serve double duty: they store both user-facing config fields and runtime-computed values that depend on screen size. This creates two problems: + +1. **`#[serde(skip)]` hack** โ€” Fields like `drops_range`, `speed_range`, `initial_cells`, `screen_size`, `boid_count`, and `crab_count` are hidden from TOML serialization because they're computed at runtime. The struct carries dead weight for no reason. + +2. **Leaky abstraction** โ€” Effect constructors read from options fields that must be pre-populated before use. The config getter methods (`get_matrix_options()`, `get_boids_options()`, `get_crab_options()`, etc.) are responsible for filling them in. Direct construction of an options struct (outside the config path) would use wrong builder defaults. + +### Affected effects + +- **matrix**: `drops_range`, `speed_range` are hidden; `drops_coeff`, `speed_coeff` are user-facing +- **life**: `initial_cells` is hidden; `cells_coeff` is user-facing +- **boids**: `screen_size`, `boid_count` are hidden; `boid_coeff` is user-facing +- **crab**: `crab_count` is hidden; `crab_coeff` is user-facing + +### Ideal design + +Separate config from runtime state: + +```rust +// Clean, serializable config โ€” lives in tarts.toml +#[derive(Debug, Serialize, Deserialize)] +pub struct DigitalRainConfig { + pub drops_coeff: f32, // default 1.0 + pub speed_coeff: f32, // default 1.0 +} + +// Runtime state โ€” computed from config + screen size +pub struct DigitalRainOptions { + pub drops_range: (u16, u16), + pub speed_range: (u16, u16), +} + +impl DigitalRainConfig { + pub fn to_runtime(self, screen_size: (u16, u16)) -> DigitalRainOptions { + let (w, h) = screen_size; + let area = w as f32 * h as f32; + DigitalRainOptions { + drops_range: { + let min = (area / 160.0 * self.drops_coeff) as u16; + let max = (area / 80.0 * self.drops_coeff) as u16; + (min.max(10), max.max(20)) + }, + speed_range: { + let min = ((h as f32 / 20.0 * self.speed_coeff) as u16).max(2); + let max = ((h as f32 / 10.0 * self.speed_coeff) as u16).max(16); + (min, max) + }, + } + } +} +``` + +Same pattern applies to `ConwayLifeConfig`/`ConwayLifeOptions`, `BoidsConfig`/`BoidsOptions`, and `CrabConfig`/`CrabOptions`. + +This eliminates the `#[serde(skip)]` hacks, makes the TOML schema self-documenting, and ensures runtime options are always computed correctly regardless of construction path. diff --git a/specs/ctx.md b/specs/ctx.md new file mode 100644 index 0000000..5b60e9a --- /dev/null +++ b/specs/ctx.md @@ -0,0 +1 @@ +# Current Task: diff --git a/specs/current_task.md b/specs/current_task.md deleted file mode 100644 index be712e8..0000000 --- a/specs/current_task.md +++ /dev/null @@ -1,80 +0,0 @@ -# Current Task: Release Preparation - -## Goal -Prepare the `tarts` project for public release and Reddit announcement. The project is now ready with multiple effects and Homebrew tap integration. - -## Current Status -- โœ… Multiple effects implemented and working (11 effects) -- โœ… Homebrew tap created at: https://raw.githubusercontent.com/oiwn/homebrew-tap/refs/heads/main/tarts.rb -- โœ… Release workflow builds macOS binaries (x86_64, arm64) -- โœ… pico-args dependency removed and replaced with manual parsing -- โœ… Comprehensive help system implemented -- โœ… All CLI functionality working correctly -- โœ… Code quality checks passing (cargo test, clippy) -- โœ… Ready for release - -## Remaining Tasks for v0.1.24 Release - -### 1. Release Assets -- [x] Create release notes for v0.1.24 -- [x] Verify Homebrew formula installation works -- [ ] Test installation from tap: `brew install oiwn/tap/tarts` - -### 2. Reddit Announcement Preparation -- [ ] Prepare engaging title and description -- [x] Create visual assets (screenshots/GIFs) -- [x] Write clear installation instructions - -#### Reddit Content Ideas - -**Project Highlights to Include:** -- Terminal-based screensavers collection written in Rust -- Dozen of different effects (Matrix rain, Conway's Game of Life, 3D donut/cube, maze generation, etc.) -- Community contributions since initial release (multiple new effects added, nix package) -- Cross-platform support with easy installation options (for OSX via brew and nix for others) -- Recently optimized: removed unmaintained dependencies for lighter binary - -**Feature Highlights to Mention:** -- **Digital Rain** - Authentic Matrix-style effect with smooth animation -- **Maze Generation** - Real-time maze generation -- **3D Donut** - Classic 3D donut rotation with proper shading -- Other effects: Conway's Game of Life, Boids simulation, Fire effect, Plasma, etc. - -**Installation Options:** -- Homebrew: `brew install oiwn/tap/tarts` -- Cargo: `cargo install tarts` -- Manual downloads from GitHub releases - -**Future Plans:** -- Polish and optimize existing effects -- Add configuration system for customization -- Community contributions welcome - -**Title:** -"Tarts: Beautiful terminal screensavers in Rust - new release" - -**Key Selling Points:** -- Lightweight and fast (Rust, minimal dependencies) -- Cross-platform (macOS, Linux) -- Easy installation (Homebrew, Cargo) -- Open source (MIT license) - -### 3. Final Checks -- [x] Verify all effects start and run without errors -- [x] Test on different terminal sizes -- [x] Ensure graceful exit on Ctrl+C - -## Available Effects for Release -- matrix, life, maze, boids, cube, crab, donut, pipes, plasma, fire, blank - -## Installation Methods -1. **Homebrew**: `brew install oiwn/tap/tarts` -2. **Cargo**: `cargo install tarts` -3. **Manual**: Download binaries from GitHub releases - -## v0.1.24 Changes Summary -- Removed pico-args dependency for lighter binary -- Added comprehensive help system with `--help` -- Added version information with `--version` -- Improved CLI argument parsing -- Better error handling and user experience diff --git a/specs/record_gif.md b/specs/record_gif.md index 6462100..5a5a24b 100644 --- a/specs/record_gif.md +++ b/specs/record_gif.md @@ -1,5 +1,15 @@ # Recording tarts Effects as GIFs +## TODO NOTE: + +There is new crate "https://github.com/joshka/betamax": + +``` +Betamax is a Rust-first terminal capture tool in the spirit of VHS. It reads tape files, runs commands in a PTY, feeds terminal output through libghostty-vt, rasterizes frames in process with cosmic-text and swash, and writes screenshots or animations. +``` + +Need to consider integration of this. + ## Goal Create clean, isolated recordings of tarts effects using Docker+Nix to prevent leaking sensitive information. diff --git a/src/blank/effect.rs b/src/blank/effect.rs index ec0ed9c..d4051e5 100644 --- a/src/blank/effect.rs +++ b/src/blank/effect.rs @@ -2,8 +2,9 @@ use crate::buffer::{Buffer, Cell}; use crate::common::TerminalEffect; use crossterm::style; use derive_builder::Builder; +use serde::{Deserialize, Serialize}; -#[derive(Builder, Default, Debug, Clone)] +#[derive(Builder, Default, Debug, Clone, Serialize, Deserialize)] #[builder(public, setter(into))] pub struct BlankOptions {} diff --git a/src/blank/mod.rs b/src/blank/mod.rs index 5143db7..1489fbd 100644 --- a/src/blank/mod.rs +++ b/src/blank/mod.rs @@ -1,2 +1,2 @@ pub mod effect; -pub use effect::{Blank, BlankOptionsBuilder}; +pub use effect::{Blank, BlankOptions, BlankOptionsBuilder}; diff --git a/src/boids/effect.rs b/src/boids/effect.rs index 18851fe..37c85b9 100644 --- a/src/boids/effect.rs +++ b/src/boids/effect.rs @@ -2,10 +2,30 @@ use crate::buffer::{Buffer, Cell}; use crate::common::{DefaultOptions, TerminalEffect}; use crossterm::style; use derive_builder::Builder; -use rand::Rng; +use rand::RngExt; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub enum BoidCharset { + #[default] + Braille, + Arrow, + Simple, + Dot, +} + +impl BoidCharset { + pub fn chars(&self) -> [char; 8] { + match self { + BoidCharset::Braille => ['โฃค', 'โขฐ', 'โฃฐ', 'โก†', 'โก‡', 'โ ‡', 'โ ›', 'โ ™'], + BoidCharset::Arrow => ['โ†’', 'โ†˜', 'โ†“', 'โ†™', 'โ†', 'โ†–', 'โ†‘', 'โ†—'], + BoidCharset::Simple => ['>', '>', 'v', 'v', '<', '<', '^', '^'], + BoidCharset::Dot => ['โ€ข'; 8], + } + } +} + // Individual boid #[derive(Clone)] struct Boid { @@ -18,9 +38,15 @@ struct Boid { #[derive(Builder, Default, Debug, Clone, Serialize, Deserialize)] #[builder(public, setter(into))] pub struct BoidsOptions { + #[builder(default)] + #[serde(skip)] pub screen_size: (u16, u16), #[builder(default = "100")] - boid_count: u16, + #[serde(skip)] + pub boid_count: u16, + + #[builder(default = "1.0")] + pub boid_coeff: f32, // Separation parameters #[builder(default = "1.5")] @@ -52,13 +78,16 @@ pub struct BoidsOptions { max_speed: f32, #[builder(default = "0.2")] min_speed: f32, + + #[builder(default)] + charset: BoidCharset, } pub struct Boids { options: BoidsOptions, buffer: Buffer, boids: Vec, - // rng: rand::prelude::ThreadRng, + charset_chars: [char; 8], } impl Boid { @@ -71,28 +100,16 @@ impl Boid { } } - // Get character based on direction - fn get_direction_char(&self) -> char { + fn get_direction_char(&self, charset: &[char; 8]) -> char { let (vx, vy) = self.velocity; let angle = f32::atan2(vy, vx); - // Map angle to 8 directions - match ((angle / PI * 4.0).round() as i32 + 8) % 8 { - 0 => 'โ†’', - 1 => 'โ†˜', - 2 => 'โ†“', - 3 => 'โ†™', - 4 => 'โ†', - 5 => 'โ†–', - 6 => 'โ†‘', - 7 => 'โ†—', - _ => 'โ€ข', - } + let idx = ((angle / PI * 4.0).round() as i32 + 8) % 8; + charset[idx as usize] } - // Update character and color based on velocity - fn update_visual(&mut self) { - self.character = self.get_direction_char(); + fn update_visual(&mut self, charset: &[char; 8]) { + self.character = self.get_direction_char(charset); // Speed-based color (green to white) let speed = (self.velocity.0.powi(2) + self.velocity.1.powi(2)).sqrt(); @@ -160,6 +177,8 @@ impl Boids { let height = options.screen_size.1 as f32; // Create initial boids with random positions and velocities + let charset_chars = options.charset.chars(); + let mut boids = Vec::with_capacity(options.boid_count as usize); for _ in 0..options.boid_count { let position = @@ -169,7 +188,7 @@ impl Boids { (rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0)); let mut boid = Boid::new(position, velocity); - boid.update_visual(); + boid.update_visual(&charset_chars); boids.push(boid); } @@ -177,6 +196,7 @@ impl Boids { options, buffer, boids, + charset_chars, } } @@ -309,13 +329,6 @@ impl Boids { toward_center.1 * self.options.cohesion_weight * 0.03 + swirl_normalized.1 * self.options.swirl_factor * 0.02, ); - - // cohesion_adjustments[i] = ( - // toward_center.0 * self.options.cohesion_weight * 0.01 - // + swirl_normalized.0 * self.options.swirl_factor * 0.01, - // toward_center.1 * self.options.cohesion_weight * 0.01 - // + swirl_normalized.1 * self.options.swirl_factor * 0.01, - // ); } // Apply border avoidance @@ -422,7 +435,7 @@ impl Boids { } // Update visual representation - boid.update_visual(); + boid.update_visual(&self.charset_chars); } } } diff --git a/src/boids/mod.rs b/src/boids/mod.rs index a083e28..bdaf112 100644 --- a/src/boids/mod.rs +++ b/src/boids/mod.rs @@ -1,3 +1,3 @@ pub mod effect; #[allow(unused)] -pub use effect::{Boids, BoidsOptions, BoidsOptionsBuilder}; +pub use effect::{BoidCharset, Boids, BoidsOptions, BoidsOptionsBuilder}; diff --git a/src/config.rs b/src/config.rs index 2524ae8..6f07bf6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,162 +1,126 @@ use crate::{ + blank::{BlankOptions, BlankOptionsBuilder}, boids::{BoidsOptions, BoidsOptionsBuilder}, crab::{CrabOptions, CrabOptionsBuilder}, cube::{CubeOptions, CubeOptionsBuilder}, + donut::{DonutOptions, DonutOptionsBuilder}, error::{ConfigError, Result, TartsError}, + fire::{FireOptions, FireOptionsBuilder}, life::{ConwayLifeOptions, ConwayLifeOptionsBuilder}, maze::{MazeOptions, MazeOptionsBuilder}, + pipes::{PipesOptions, PipesOptionsBuilder}, + plasma::{PlasmaOptions, PlasmaOptionsBuilder}, rain::digital_rain::{DigitalRainOptions, DigitalRainOptionsBuilder}, + terrain::{TerrainOptions, TerrainOptionsBuilder}, }; -use directories::ProjectDirs; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +fn config_path() -> PathBuf { + #[cfg(target_os = "windows")] + { + let appdata = std::env::var("APPDATA").unwrap_or_else(|_| { + directories::BaseDirs::new() + .unwrap() + .home_dir() + .join("AppData/Roaming") + .display() + .to_string() + }); + PathBuf::from(appdata).join("tarts.toml") + } + #[cfg(not(target_os = "windows"))] + { + directories::BaseDirs::new() + .unwrap() + .home_dir() + .join(".config/tarts.toml") + } +} #[derive(Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] pub struct Config { #[serde(default)] - matrix: DigitalRainOptions, + pub matrix: DigitalRainOptions, + #[serde(default)] + pub life: ConwayLifeOptions, + #[serde(default)] + pub maze: MazeOptions, + #[serde(default)] + pub boids: BoidsOptions, #[serde(default)] - life: ConwayLifeOptions, + pub blank: BlankOptions, #[serde(default)] - maze: MazeOptions, + pub cube: CubeOptions, #[serde(default)] - boids: BoidsOptions, + pub crab: CrabOptions, #[serde(default)] - cube: CubeOptions, + pub donut: DonutOptions, #[serde(default)] - crab: CrabOptions, + pub pipes: PipesOptions, + #[serde(default)] + pub plasma: PlasmaOptions, + #[serde(default)] + pub fire: FireOptions, + #[serde(default)] + pub terrain: TerrainOptions, } impl Config { - // Generate and save default config - pub fn save_default_config() -> Result<()> { - let proj_dirs = ProjectDirs::from("", "", "tarts").ok_or_else(|| { - TartsError::Config(ConfigError::MissingField( - "Could not determine config directory".into(), - )) - })?; - - // Create config directory if it doesn't exist - let config_dir = proj_dirs.config_dir(); - std::fs::create_dir_all(config_dir)?; - - let config_path = config_dir.join("tarts.toml"); - - // Create default config - let default_config = Config::default(); - - // Convert to TOML and save - let contents = toml::to_string(&default_config) + /// Print default config as TOML to stdout (for piping into a file). + pub fn print_default_config() -> Result<()> { + let contents = toml::to_string_pretty(&Config::default()) .map_err(|e| TartsError::Config(ConfigError::SerializeFormat(e)))?; - std::fs::write(config_path, contents)?; - + println!("{}", contents); Ok(()) } - // Modify your existing load method to create default if missing - #[allow(dead_code)] - pub fn load_old() -> Result { - let proj_dirs = ProjectDirs::from("", "", "tarts").ok_or_else(|| { - TartsError::Config(ConfigError::MissingField( - "Could not determine config directory".into(), - )) - })?; - - let config_path = proj_dirs.config_dir().join("tarts.toml"); - - if !config_path.exists() { - // Create config directory if it doesn't exist - std::fs::create_dir_all(proj_dirs.config_dir())?; - - // Create and save default config - let default_config = Config::default(); - let contents = toml::to_string(&default_config) - .map_err(|e| TartsError::Config(ConfigError::SerializeFormat(e)))?; - std::fs::write(&config_path, contents)?; - return Ok(default_config); - } - - let contents = std::fs::read_to_string(config_path)?; - toml::from_str(&contents) - .map_err(|e| TartsError::Config(ConfigError::DeserializeFormat(e))) - } - - #[allow(dead_code)] - pub fn load() -> Result { - let proj_dirs = ProjectDirs::from("", "", "tarts").ok_or_else(|| { - eprintln!("Failed to get project directory"); - TartsError::Config(ConfigError::MissingField( - "Could not determine config directory".into(), - )) - })?; - - println!("Config dir: {:?}", proj_dirs.config_dir()); - let config_path = proj_dirs.config_dir().join("tarts.toml"); - println!("Config path: {:?}", config_path); - - if !config_path.exists() { - println!("Config file doesn't exist, creating default"); - - // Create config directory if it doesn't exist - match std::fs::create_dir_all(proj_dirs.config_dir()) { - Ok(_) => println!("Created config directory"), - Err(e) => println!("Failed to create config directory: {}", e), - } - - // Create default config using builders explicitly - let default_config = Config { - matrix: DigitalRainOptionsBuilder::default().build().unwrap(), - life: ConwayLifeOptionsBuilder::default().build().unwrap(), - maze: MazeOptionsBuilder::default().build().unwrap(), - boids: BoidsOptionsBuilder::default().build().unwrap(), - cube: CubeOptionsBuilder::default().build().unwrap(), - crab: CrabOptionsBuilder::default().build().unwrap(), - }; - - println!("Default cube options: {:?}", default_config.cube); - - // Serialize and write - let contents = toml::to_string(&default_config).map_err(|e| { - println!("Failed to serialize config: {}", e); - TartsError::Config(ConfigError::SerializeFormat(e)) + /// Load config from platform path. Returns the config and a status message. + /// If no config file exists, returns default config in memory (does NOT write to disk). + pub fn load() -> Result<(Self, String)> { + let path = config_path(); + if path.exists() { + let contents = std::fs::read_to_string(&path)?; + let config = toml::from_str(&contents).map_err(|e| { + TartsError::Config(ConfigError::DeserializeFormat(e)) })?; - - println!("TOML contents: {}", contents); - - std::fs::write(&config_path, &contents)?; - println!("Wrote config file successfully"); - - return Ok(default_config); + Ok((config, format!("Loaded config from {}", path.display()))) + } else { + Ok(( + Config::default(), + format!("No config found at {}, using defaults", path.display()), + )) } - - println!("Config file exists, loading it"); - let contents = std::fs::read_to_string(&config_path)?; - println!("Loaded contents: {}", contents); - - let config = toml::from_str(&contents).map_err(|e| { - println!("Failed to parse config: {}", e); - TartsError::Config(ConfigError::DeserializeFormat(e)) - })?; - - println!("Parsed config successfully"); - Ok(config) } } -// FIXIT: need to do something with this mess -#[allow(unused)] impl Config { - // Add these methods pub fn get_matrix_options( &self, - _screen_size: (u16, u16), + screen_size: (u16, u16), ) -> DigitalRainOptions { - // If default is needed, create it, otherwise use stored config - self.matrix.clone() + let mut options = self.matrix.clone(); + let (w, h) = screen_size; + let area = w as f32 * h as f32; + options.drops_range = { + let min = (area / 160.0 * options.drops_coeff) as u16; + let max = (area / 80.0 * options.drops_coeff) as u16; + (min.max(10), max.max(20)) + }; + options.speed_range = { + let min = ((h as f32 / 20.0 * options.speed_coeff) as u16).max(2); + let max = ((h as f32 / 10.0 * options.speed_coeff) as u16).max(16); + (min, max) + }; + options } - pub fn get_life_options(&self, _screen_size: (u16, u16)) -> ConwayLifeOptions { - self.life.clone() + pub fn get_life_options(&self, screen_size: (u16, u16)) -> ConwayLifeOptions { + let mut options = self.life.clone(); + let (w, h) = screen_size; + options.initial_cells = + (w as f32 * h as f32 * 0.15 * options.cells_coeff) as u32; + options } pub fn get_maze_options(&self, _screen_size: (u16, u16)) -> MazeOptions { @@ -166,15 +130,50 @@ impl Config { pub fn get_boids_options(&self, screen_size: (u16, u16)) -> BoidsOptions { let mut options = self.boids.clone(); options.screen_size = screen_size; + let (w, h) = screen_size; + options.boid_count = ((w as f32 * h as f32 * 0.5 * options.boid_coeff) + as u16) + .clamp(50, 300); options } + pub fn get_blank_options(&self) -> BlankOptions { + self.blank.clone() + } + pub fn get_cube_options(&self) -> CubeOptions { self.cube.clone() } - pub fn get_crab_options(&self) -> CrabOptions { - self.crab.clone() + pub fn get_crab_options(&self, screen_size: (u16, u16)) -> CrabOptions { + let mut options = self.crab.clone(); + let screen_area = screen_size.0 as f32 * screen_size.1 as f32; + options.crab_count = + (screen_area / 800.0 * options.crab_coeff).clamp(3.0, 15.0) as u16; + options + } + + pub fn get_donut_options(&self, screen_size: (u16, u16)) -> DonutOptions { + let mut options = self.donut.clone(); + let min_dim = screen_size.0.min(screen_size.1) as f32; + options.k1 = min_dim * 0.8 * options.k1_coeff; + options + } + + pub fn get_pipes_options(&self) -> PipesOptions { + self.pipes.clone() + } + + pub fn get_plasma_options(&self) -> PlasmaOptions { + self.plasma.clone() + } + + pub fn get_fire_options(&self) -> FireOptions { + self.fire.clone() + } + + pub fn get_terrain_options(&self) -> TerrainOptions { + self.terrain.clone() } } @@ -185,8 +184,14 @@ impl Default for Config { life: ConwayLifeOptionsBuilder::default().build().unwrap(), maze: MazeOptionsBuilder::default().build().unwrap(), boids: BoidsOptionsBuilder::default().build().unwrap(), + blank: BlankOptionsBuilder::default().build().unwrap(), cube: CubeOptionsBuilder::default().build().unwrap(), crab: CrabOptionsBuilder::default().build().unwrap(), + donut: DonutOptionsBuilder::default().build().unwrap(), + pipes: PipesOptionsBuilder::default().build().unwrap(), + plasma: PlasmaOptionsBuilder::default().build().unwrap(), + fire: FireOptionsBuilder::default().build().unwrap(), + terrain: TerrainOptionsBuilder::default().build().unwrap(), } } } diff --git a/src/crab/effect.rs b/src/crab/effect.rs index eacc873..c35c6af 100644 --- a/src/crab/effect.rs +++ b/src/crab/effect.rs @@ -2,7 +2,7 @@ use crate::buffer::{Buffer, Cell}; use crate::common::{DefaultOptions, TerminalEffect}; use crossterm::style; use derive_builder::Builder; -use rand::Rng; +use rand::RngExt; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -69,6 +69,7 @@ struct CrabEntity { #[builder(public, setter(into))] pub struct CrabOptions { #[builder(default = "5")] + #[serde(skip)] pub crab_count: u16, #[builder(default = "0.2")] @@ -77,8 +78,11 @@ pub struct CrabOptions { #[builder(default = "0.05")] pub clap_chance: f32, // Random chance for special animation - #[builder(default = "1.0")] + #[builder(default = "5.0")] pub movement_speed: f32, + + #[builder(default = "1.0")] + pub crab_coeff: f32, } pub struct Crab { diff --git a/src/cube/effect.rs b/src/cube/effect.rs index 0848e0a..a6abe2a 100644 --- a/src/cube/effect.rs +++ b/src/cube/effect.rs @@ -29,15 +29,15 @@ struct Edge { #[derive(Builder, Default, Debug, Clone, Serialize, Deserialize)] #[builder(public, setter(into))] pub struct CubeOptions { - #[builder(default = "5.0")] + #[builder(default = "1.0")] pub cube_size: f32, - #[builder(default = "0.5")] + #[builder(default = "0.6")] pub rotation_speed_x: f32, - #[builder(default = "0.7")] + #[builder(default = "0.8")] pub rotation_speed_y: f32, - #[builder(default = "0.3")] + #[builder(default = "0.4")] pub rotation_speed_z: f32, - #[builder(default = "3.0")] + #[builder(default = "3.5")] pub distance: f32, #[builder(default = "true")] pub use_braille: bool, diff --git a/src/donut/effect.rs b/src/donut/effect.rs index 56faef4..404d295 100644 --- a/src/donut/effect.rs +++ b/src/donut/effect.rs @@ -15,10 +15,13 @@ pub struct DonutOptions { pub rotation_speed_a: f32, #[builder(default = "0.03")] pub rotation_speed_b: f32, - #[builder(default = "5.0")] + #[builder(default = "5.5")] pub distance: f32, #[builder(default = "25.0")] + #[serde(skip)] pub k1: f32, + #[builder(default = "1.0")] + pub k1_coeff: f32, #[builder( default = "vec!['.', ',', '-', '~', ':', ';', '=', '!', '*', '#', '$', '@']" )] diff --git a/src/error.rs b/src/error.rs index 60731ec..f05e4c7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,6 +21,4 @@ pub enum ConfigError { #[error("Failed to serialize config: {0}")] SerializeFormat(#[from] toml::ser::Error), - #[error("Missing required field: {0}")] - MissingField(String), } diff --git a/src/fire/effect.rs b/src/fire/effect.rs index 51a7182..3f68955 100644 --- a/src/fire/effect.rs +++ b/src/fire/effect.rs @@ -2,7 +2,7 @@ use crate::buffer::{Buffer, Cell}; use crate::common::{DefaultOptions, TerminalEffect}; use crossterm::style; use derive_builder::Builder; -use rand::Rng; +use rand::RngExt; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -55,7 +55,7 @@ const MAXTABLE: usize = 256 * 5; #[derive(Builder, Default, Debug, Clone, Serialize, Deserialize)] #[builder(public, setter(into))] pub struct FireOptions { - #[builder(default = "false")] + #[builder(default = "true")] pub use_colors: bool, } diff --git a/src/lib.rs b/src/lib.rs index e43ec2a..979e5b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,27 @@ +//! Terminal-based screensavers and visual effects. +//! +//! Modules: +//! +//! | Module | Description | +//! |----------|------------------------------------------| +//! | `blank` | Blank screen โ€” no-op placeholder | +//! | `boids` | Boids flocking simulation | +//! | `buffer` | Terminal cell buffer for colored output | +//! | `check` | Terminal event checking (input, resize) | +//! | `common` | Shared traits and types (TerminalEffect) | +//! | `config` | CLI configuration and argument parsing | +//! | `crab` | ASCII crab walking animation | +//! | `cube` | 3D rotating cube in ASCII | +//! | `donut` | 3D rotating donut in ASCII | +//! | `error` | Error types for the crate | +//! | `fire` | Fire simulation effect | +//! | `life` | Conway's Game of Life | +//! | `maze` | Maze generation and animation | +//! | `pipes` | Pipe maze animation | +//! | `plasma` | Plasma color wave effect | +//! | `rain` | Matrix-style digital rain | +//! | `terrain`| Terrain generation โ€” scrolling landscape | + pub mod blank; pub mod boids; pub mod buffer; diff --git a/src/life/effect.rs b/src/life/effect.rs index 552f850..4033f59 100644 --- a/src/life/effect.rs +++ b/src/life/effect.rs @@ -14,7 +14,7 @@ use crate::buffer::{Buffer, Cell}; use crate::common::{DefaultOptions, TerminalEffect}; use crossterm::style; use derive_builder::Builder; -use rand::Rng; +use rand::RngExt; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::LazyLock; @@ -28,7 +28,11 @@ static DEAD_CELLS_CHARS: LazyLock> = LazyLock::new(|| { #[derive(Builder, Default, Debug, Serialize, Deserialize, Clone)] #[builder(public, setter(into))] pub struct ConwayLifeOptions { + #[builder(default = "200")] + #[serde(skip)] pub initial_cells: u32, + #[builder(default = "1.0")] + pub cells_coeff: f32, } #[derive(Clone)] diff --git a/src/main.rs b/src/main.rs index 3ac0d70..227d329 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,35 +1,5 @@ -//! # tarts -//! -//! `tarts` or TerminalArts is a collection of terminal-based screen savers written -//! in Rust. This crate provides a variety of screen savers like "Matrix Rain", -//! "Conway's Game of Life", and "Maze Generation" (not yet), all running directly -//! in your terminal. -//! -//! ## Features -//! -//! - Matrix Rain: Simulates the famous "Matrix" digital rain effect in your terminal. -//! - Conway's Game of Life: Implements the classic cellular automaton in the terminal. -//! - Maze Generation: Generates and displays a random maze. -//! - Boids -//! -//! ## Usage -//! -//! To use the screen savers, run the executable with the desired screen saver's -//! name as an argument: -//! -//! ```bash -//! tarts matrix -//! tarts life -//! tarts maze -//! tarts boids -//! tarts cube -//! tarts crab -//! ``` #![cfg(not(test))] use crossterm::{self, cursor, execute, terminal}; -// use tarts::{config, rain}; -// use log::info; -use crate::common::DefaultOptions; use std::{io, process}; mod blank; @@ -54,7 +24,7 @@ use crate::config::Config; const VALID_SAVERS: &[&str] = &[ "matrix", "life", "maze", "boids", "blank", "cube", "crab", "donut", "pipes", - "plasma", "fire", + "plasma", "fire", "terrain", ]; #[derive(Debug)] @@ -92,7 +62,7 @@ impl TerminalGuard { impl Drop for TerminalGuard { fn drop(&mut self) { - // Ignore errors during drop - we'''re doing best effort cleanup + // Ignore errors during drop - we're doing best effort cleanup let _ = execute!( self.stdout, cursor::Show, @@ -105,7 +75,6 @@ impl Drop for TerminalGuard { fn main() -> Result<(), error::TartsError> { env_logger::init(); - // let config = Config::load()?; let args = match parse_args() { Ok(v) => v, @@ -128,77 +97,72 @@ fn main() -> Result<(), error::TartsError> { return Ok(()); } + let (config, config_status) = Config::load()?; + let fps = { let mut guard = TerminalGuard::new()?; let (width, height) = terminal::size()?; match args.screen_saver.as_str() { "matrix" => { - // let options = config.get_matrix_options((width, height)); - let options = - rain::digital_rain::DigitalRain::default_options(width, height); + let options = config.get_matrix_options((width, height)); let mut digital_rain = rain::digital_rain::DigitalRain::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut digital_rain, None)? } "life" => { - // let options = config.get_life_options((width, height)); - let options = life::ConwayLife::default_options(width, height); + let options = config.get_life_options((width, height)); let mut conway_life = life::ConwayLife::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut conway_life, None)? } "maze" => { - // let options = config.get_maze_options((width, height)); - let options = maze::Maze::default_options(width, height); + let options = config.get_maze_options((width, height)); let mut maze = maze::Maze::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut maze, None)? } "boids" => { - // let options = config.get_boids_options((width, height)); - let options = boids::Boids::default_options(width, height); + let options = config.get_boids_options((width, height)); let mut boids = boids::Boids::new(options); common::run_loop(guard.get_stdout(), &mut boids, None)? } "blank" => { - let options = - blank::BlankOptionsBuilder::default().build().unwrap(); - let mut check = blank::Blank::new(options, (width, height)); - common::run_loop(guard.get_stdout(), &mut check, None)? + let options = config.get_blank_options(); + let mut blank = blank::Blank::new(options, (width, height)); + common::run_loop(guard.get_stdout(), &mut blank, None)? } "cube" => { - // let options = config.get_cube_options(); - let options = cube::effect::Cube::default_options(width, height); + let options = config.get_cube_options(); let mut cube = cube::Cube::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut cube, None)? } "crab" => { - let options = crab::Crab::default_options(width, height); + let options = config.get_crab_options((width, height)); let mut crab = crab::Crab::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut crab, None)? } "donut" => { - let options = donut::Donut::default_options(width, height); + let options = config.get_donut_options((width, height)); let mut donut = donut::Donut::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut donut, None)? } "pipes" => { - let options = pipes::Pipes::default_options(width, height); + let options = config.get_pipes_options(); let mut pipes = pipes::Pipes::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut pipes, None)? } "plasma" => { - let options = plasma::Plasma::default_options(width, height); + let options = config.get_plasma_options(); let mut plasma = plasma::Plasma::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut plasma, None)? } "fire" => { - let options = fire::Fire::default_options(width, height); + let options = config.get_fire_options(); let mut fire = fire::Fire::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut fire, None)? } "terrain" => { - let options = terrain::Terrain::default_options(width, height); + let options = config.get_terrain_options(); let mut terrain = terrain::Terrain::new(options, (width, height)); common::run_loop(guard.get_stdout(), &mut terrain, None)? } @@ -211,7 +175,8 @@ fn main() -> Result<(), error::TartsError> { } }; - println!("Frames per second: {}", fps); + println!("{}", config_status); + println!("Frames per second: {:.1}", fps); Ok(()) } @@ -235,12 +200,11 @@ fn parse_args() -> Result { "--check" => { check = true; } - "--generate-config" => { - if let Err(e) = Config::save_default_config() { - eprintln!("Failed to generate config: {}", e); + "--print-config" => { + if let Err(e) = Config::print_default_config() { + eprintln!("Failed to print config: {}", e); std::process::exit(1); } - println!("Default configuration generated successfully"); std::process::exit(0); } "--effect" => { @@ -289,6 +253,7 @@ fn print_help() { println!(" pipes Pipe maze animation"); println!(" plasma Plasma effect"); println!(" fire Fire simulation"); + println!(" terrain Terrain generation"); println!(" blank Blank screen"); println!(); println!("OPTIONS:"); @@ -297,13 +262,20 @@ fn print_help() { println!(" --check Run test mode"); println!(" --effect Effect to test (with --check)"); println!(" --frames Number of frames to run (with --check)"); - println!(" --generate-config Generate default config file"); + println!(" --print-config Print default config as TOML to stdout"); + println!(); + println!("CONFIG:"); + println!(" Config file (optional): ~/.config/tarts.toml"); + println!( + " Generate one with: tarts --print-config > ~/.config/tarts.toml" + ); println!(); println!("EXAMPLES:"); println!(" tarts matrix Run Matrix effect"); println!(" tarts --check Test with default effect"); println!(" tarts --check life Test Life effect"); println!(" tarts --check --frames 100 life"); + println!(" tarts --print-config > ~/.config/tarts.toml"); println!(" tarts --version Show version"); } diff --git a/src/maze/effect.rs b/src/maze/effect.rs index c081973..078cb07 100644 --- a/src/maze/effect.rs +++ b/src/maze/effect.rs @@ -2,7 +2,7 @@ use crate::buffer::{Buffer, Cell}; use crate::common::{DefaultOptions, TerminalEffect}; use crossterm::style; use derive_builder::Builder; -use rand::{Rng, seq::SliceRandom}; +use rand::{RngExt, seq::SliceRandom}; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet, VecDeque}, diff --git a/src/pipes/effect.rs b/src/pipes/effect.rs index 0d5a3b7..ccb4f50 100644 --- a/src/pipes/effect.rs +++ b/src/pipes/effect.rs @@ -2,7 +2,7 @@ use crate::buffer::{Buffer, Cell}; use crate::common::{DefaultOptions, TerminalEffect}; use crossterm::style; use derive_builder::Builder; -use rand::Rng; +use rand::RngExt; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -28,11 +28,11 @@ static LINE_CHARS: LazyLock>> = LazyLock::new(|| { #[derive(Builder, Default, Debug, Clone, Serialize, Deserialize)] #[builder(public, setter(into))] pub struct PipesOptions { - #[builder(default = "0.1")] + #[builder(default = "0.2")] pub turn_probability: f64, - #[builder(default = "1")] + #[builder(default = "2")] pub line_type: usize, - #[builder(default = "1")] + #[builder(default = "5")] pub num_lines: usize, #[builder(default = "0.3")] pub pipe_type_change: f64, diff --git a/src/plasma/effect.rs b/src/plasma/effect.rs index a162bd9..5d2f54f 100644 --- a/src/plasma/effect.rs +++ b/src/plasma/effect.rs @@ -12,7 +12,7 @@ pub struct PlasmaOptions { pub time_scale: f64, #[builder(default = "1.0")] pub spatial_scale: f64, - #[builder(default = "100.0")] + #[builder(default = "150.0")] pub color_speed: f64, } diff --git a/src/rain/digital_rain.rs b/src/rain/digital_rain.rs index 4ed55f1..b6147df 100644 --- a/src/rain/digital_rain.rs +++ b/src/rain/digital_rain.rs @@ -5,14 +5,22 @@ use crate::buffer::{Buffer, Cell}; use crate::common::{DefaultOptions, TerminalEffect}; use derive_builder::Builder; -use rand::{self, Rng}; +use rand::{self, RngExt}; use serde::{Deserialize, Serialize}; use std::time::Duration; #[derive(Builder, Default, Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct DigitalRainOptions { + #[builder(default = "(10, 20)")] + #[serde(skip)] pub drops_range: (u16, u16), + #[builder(default = "(2, 16)")] + #[serde(skip)] pub speed_range: (u16, u16), + #[builder(default = "1.0")] + pub drops_coeff: f32, + #[builder(default = "1.0")] + pub speed_coeff: f32, } pub struct DigitalRain { diff --git a/src/rain/rain_drop.rs b/src/rain/rain_drop.rs index b2b2cba..fae2996 100644 --- a/src/rain/rain_drop.rs +++ b/src/rain/rain_drop.rs @@ -1,7 +1,7 @@ // use super::rain_options::DigitalRainOptions; use crate::rain::digital_rain::DigitalRainOptions; use rand::{ - self, Rng, + self, Rng, RngExt, distr::{Distribution, StandardUniform}, seq::IndexedRandom, }; diff --git a/src/terrain/effect.rs b/src/terrain/effect.rs index cf7b12f..3aefaa4 100644 --- a/src/terrain/effect.rs +++ b/src/terrain/effect.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; pub struct TerrainOptions { #[builder(default = "42")] pub seed: u32, - #[builder(default = "0.05")] + #[builder(default = "0.02")] pub scale: f64, #[builder(default = "4")] pub octaves: i32, From 96658df277671665ddc617c4326d05ae0d6ad01a Mon Sep 17 00:00:00 2001 From: oiwn Date: Mon, 11 May 2026 02:16:15 +0700 Subject: [PATCH 3/5] make typos happy --- CHANGELOG.md | 2 ++ _typos.toml | 2 ++ notes.org | 4 ++-- prek.toml | 11 +++++++++++ src/buffer.rs | 2 +- src/check.rs | 2 +- src/common.rs | 2 +- src/config.rs | 4 +--- src/maze/effect.rs | 2 +- src/rain/digital_rain.rs | 4 ++-- src/rain/rain_drop.rs | 8 ++++---- 11 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 _typos.toml create mode 100644 prek.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index e2e8832..5654883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,13 @@ All notable changes to this project will be documented in this file. ## [0.1.25] - 2026-05-11 ### Added +- **Constellation effect** โ€” drifting stars and dotted connections (thanks [@AnarchistHoneybun](https://github.com/AnarchistHoneybun)) - Config system: loads from `~/.config/tarts.toml` (macOS/Linux) or `%APPDATA%/tarts.toml` (Windows) - `--print-config` flag: prints default config as TOML to stdout (pipe to file) - Screen-adaptive coefficients: effects scale naturally to terminal size (`drops_coeff`, `speed_coeff`, `cells_coeff`, `boid_coeff`, `crab_coeff`, `k1_coeff`) - `BoidCharset` enum: Braille (default), Arrow, Simple, Dot โ€” configurable boid character sets - `src/lib.rs` module-level docstring with effect description table +- `prek` pre-commit checks ### Changed - Builder defaults aligned with original `DefaultOptions` values: cube size, fire colors, donut projection, pipes density, plasma speed, terrain scale, crab count diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000..671973a --- /dev/null +++ b/_typos.toml @@ -0,0 +1,2 @@ +[default.extend-identifiers] +ba = "ba" # variable name in Perlin noise algorithm (src/terrain/noise.rs) diff --git a/notes.org b/notes.org index f20a73c..5478ef7 100644 --- a/notes.org +++ b/notes.org @@ -3,7 +3,7 @@ * Digital rain ** config which store per effect options ** DONE Add config -** TODO Add valudation for config +** TODO Add validation for config ** Improve readme (remove configuration section) ** make separate thing for collision detection ** DONE better CI/CD @@ -44,6 +44,6 @@ map completely removed * Ideas -** A lot of different objects 2x2 3x3 with different speeds flying arround with collisions +** A lot of different objects 2x2 3x3 with different speeds flying around with collisions ** Pipes ** Some destructable environment? diff --git a/prek.toml b/prek.toml new file mode 100644 index 0000000..5b490a7 --- /dev/null +++ b/prek.toml @@ -0,0 +1,11 @@ +fail_fast = true + +[[repos]] +repo = "local" +hooks = [ + { id = "fmt", name = "cargo fmt", language = "system", entry = "cargo fmt --check", types = ["rust"], pass_filenames = false }, + { id = "clippy", name = "cargo clippy", language = "system", entry = "cargo clippy --all-targets --all-features -- -D warnings", types = ["rust"], pass_filenames = false }, + { id = "test", name = "cargo test", language = "system", entry = "cargo test --all-features", types = ["rust"], pass_filenames = false }, + { id = "typos", name = "typos", language = "system", entry = "typos", pass_filenames = false }, + { id = "gitleaks", name = "gitleaks", language = "system", entry = "gitleaks git --verbose", pass_filenames = false }, +] diff --git a/src/buffer.rs b/src/buffer.rs index 81fa2b0..fbff1c8 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -41,7 +41,7 @@ impl Buffer { // Indexing from 1: 1 2 3 4 5 | Square: 25 // Need to check width of height are greater than zero pub fn new(width: usize, height: usize) -> Self { - // fill buffer with dafault values + // fill buffer with default values debug_assert!(width > 0 && height > 0); Self { width, diff --git a/src/check.rs b/src/check.rs index 3a020c9..93dfd40 100644 --- a/src/check.rs +++ b/src/check.rs @@ -82,7 +82,7 @@ pub fn test_effect(effect: &mut T, frames: usize) -> Result<( Ok(()) } -/// Run appropirate effect till frame number +/// Run appropriate effect till frame number pub fn run_test_for_effect(effect_name: &str, frames: usize) -> Result<()> { match effect_name { "matrix" => { diff --git a/src/common.rs b/src/common.rs index aaf20cb..336ae90 100644 --- a/src/common.rs +++ b/src/common.rs @@ -104,7 +104,7 @@ where buffered_stdout.flush()?; effect.update(); - // stabilize fps if requred + // stabilize fps if required let ended_at = std::time::SystemTime::now(); let delta = ended_at .duration_since(started_at) diff --git a/src/config.rs b/src/config.rs index a51d6b2..e47faff 100644 --- a/src/config.rs +++ b/src/config.rs @@ -199,9 +199,7 @@ impl Default for Config { plasma: PlasmaOptionsBuilder::default().build().unwrap(), fire: FireOptionsBuilder::default().build().unwrap(), terrain: TerrainOptionsBuilder::default().build().unwrap(), - constellation: ConstellationOptionsBuilder::default() - .build() - .unwrap(), + constellation: ConstellationOptionsBuilder::default().build().unwrap(), } } } diff --git a/src/maze/effect.rs b/src/maze/effect.rs index 078cb07..cdb3a38 100644 --- a/src/maze/effect.rs +++ b/src/maze/effect.rs @@ -18,7 +18,7 @@ static CHARACTERS_MAP: LazyLock> = LazyLock::new(|| { m }); -/// Characters to draw more interesing view +/// Characters to draw more interesting view static CHARACTERS: LazyLock> = LazyLock::new(|| { let mut v = Vec::new(); for (_, chars) in CHARACTERS_MAP.iter() { diff --git a/src/rain/digital_rain.rs b/src/rain/digital_rain.rs index b6147df..8e21e87 100644 --- a/src/rain/digital_rain.rs +++ b/src/rain/digital_rain.rs @@ -72,8 +72,8 @@ impl TerminalEffect for DigitalRain { } /// Process digital rain effect. -/// Noice that all processing done implying coordinates started from 0, 0 -/// and width / height is actual number of columnts and rows +/// Note that all processing done implying coordinates started from 0, 0 +/// and width / height is actual number of columns and rows impl DigitalRain { // Initialize screensaver pub fn new(options: DigitalRainOptions, screen_size: (u16, u16)) -> Self { diff --git a/src/rain/rain_drop.rs b/src/rain/rain_drop.rs index fae2996..0825c8b 100644 --- a/src/rain/rain_drop.rs +++ b/src/rain/rain_drop.rs @@ -190,11 +190,11 @@ impl RainDrop { /// Update rain drops to change position/grow etc /// there can be 4 cases: /// rain drop vector not yet fully come from top - /// rain drop vector somewhere in the middle of the scren + /// rain drop vector somewhere in the middle of the screen /// rain drop vector reach bottom and need to fade out /// raid drop vector tail out of screen rect visibility /// - /// Note that rain drop coordiantes can be outside bounds defined + /// Note that rain drop coordinates can be outside bounds defined /// by screen width and height, this should be handled during draw process pub fn update( &mut self, @@ -396,7 +396,7 @@ mod tests { &mut rng, ); assert_eq!(new_drop.body.len(), 1); - assert_eq!(new_drop.fy, 0.0); // should be out of the h bounds and reseted + assert_eq!(new_drop.fy, 0.0); // should be out of the h bounds and reset // when tail_y < 0 let mut new_drop = RainDrop::from_values( @@ -460,7 +460,7 @@ mod tests { Duration::from_millis(1000), &mut rng, ); - assert_eq!(new_drop.fy, 33.0); // should be reseted there + assert_eq!(new_drop.fy, 33.0); // should be reset there } #[test] From e83bfe501a693dc7aa4c409aabfdfac4cf855324 Mon Sep 17 00:00:00 2001 From: oiwn Date: Mon, 11 May 2026 02:25:28 +0700 Subject: [PATCH 4/5] add a bit more tests --- src/buffer.rs | 141 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index fbff1c8..69be7b9 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -110,14 +110,140 @@ impl Buffer { mod tests { use super::*; + #[test] + fn cell_default() { + let c = Cell::default(); + assert_eq!(c.symbol, ' '); + assert_eq!(c.color, style::Color::Black); + assert_eq!(c.attr, style::Attribute::Reset); + } + + #[test] + fn cell_new() { + let c = Cell::new('x', style::Color::Red, style::Attribute::Bold); + assert_eq!(c.symbol, 'x'); + assert_eq!(c.color, style::Color::Red); + assert_eq!(c.attr, style::Attribute::Bold); + } + + #[test] + fn cell_partial_eq() { + let a = Cell::new('a', style::Color::Green, style::Attribute::Bold); + let b = Cell::new('a', style::Color::Green, style::Attribute::Bold); + let c = Cell::new('b', style::Color::Green, style::Attribute::Bold); + let d = Cell::new('a', style::Color::Red, style::Attribute::Bold); + let e = Cell::new('a', style::Color::Green, style::Attribute::Reset); + assert_eq!(a, b); + assert_ne!(a, c); + assert_ne!(a, d); + assert_ne!(a, e); + } + #[test] fn create_new() { let buf = Buffer::new(5, 4); assert_eq!(buf.width, 5); assert_eq!(buf.height, 4); + assert_eq!(buf.buffer.len(), 20); + } + + #[test] + fn new_filled_with_defaults() { + let buf = Buffer::new(3, 2); + for cell in &buf.buffer { + assert_eq!(*cell, Cell::default()); + } + } + + #[test] + fn get_size() { + let buf = Buffer::new(10, 20); + assert_eq!(buf.get_size(), (10, 20)); + } + + #[test] + fn index_of() { + let buf = Buffer::new(4, 3); + assert_eq!(buf.index_of(0, 0), 0); + assert_eq!(buf.index_of(3, 0), 3); + assert_eq!(buf.index_of(0, 1), 4); + assert_eq!(buf.index_of(3, 1), 7); + assert_eq!(buf.index_of(0, 2), 8); + assert_eq!(buf.index_of(3, 2), 11); + } + + #[test] + fn pos_of() { + let buf = Buffer::new(4, 3); + assert_eq!(buf.pos_of(0), (0, 0)); + assert_eq!(buf.pos_of(3), (3, 0)); + assert_eq!(buf.pos_of(4), (0, 1)); + assert_eq!(buf.pos_of(7), (3, 1)); + assert_eq!(buf.pos_of(11), (3, 2)); + } + + #[test] + fn pos_of_roundtrip() { + let buf = Buffer::new(5, 4); + for y in 0..buf.height { + for x in 0..buf.width { + let idx = buf.index_of(x, y); + let (rx, ry) = buf.pos_of(idx); + assert_eq!((rx, ry), (x, y)); + } + } + } - let size = buf.buffer.len(); - assert_eq!(size, 20); + #[test] + fn set_and_get() { + let mut buf = Buffer::new(3, 3); + let cell = Cell::new('*', style::Color::Yellow, style::Attribute::Bold); + buf.set(1, 2, cell); + assert_eq!(buf.get(1, 2), cell); + assert_eq!(buf.get(0, 0), Cell::default()); + } + + #[test] + fn fill_with() { + let mut buf = Buffer::new(3, 2); + let cell = Cell::new('.', style::Color::Blue, style::Attribute::Reset); + buf.fill_with(&cell); + for c in &buf.buffer { + assert_eq!(*c, cell); + } + } + + #[test] + fn diff_identical() { + let buf1 = Buffer::new(3, 3); + let buf2 = Buffer::new(3, 3); + assert!(buf1.diff(&buf2).is_empty()); + } + + #[test] + fn diff_single_cell() { + let buf1 = Buffer::new(3, 3); + let mut buf2 = Buffer::new(3, 3); + let cell = Cell::new('X', style::Color::Red, style::Attribute::Bold); + buf2.set(1, 1, cell); + + let diff = buf1.diff(&buf2); + assert_eq!(diff.len(), 1); + assert_eq!(diff[0], (1, 1, cell)); + } + + #[test] + fn diff_different_sizes_compares_overlap() { + let buf1 = Buffer::new(2, 2); + let mut buf2 = Buffer::new(3, 3); + buf2.set( + 0, + 0, + Cell::new('X', style::Color::Red, style::Attribute::Bold), + ); + // Only compares cells that exist in both buffers (first 4 cells) + let diff = buf1.diff(&buf2); + assert_eq!(diff.len(), 1); } #[test] @@ -147,4 +273,15 @@ mod tests { let diff = buf.diff(&next_buf); assert_eq!(diff.len(), 3); } + + #[test] + fn iter() { + let buf = Buffer::new(2, 2); + let mut count = 0; + for c in buf.iter() { + assert_eq!(*c, Cell::default()); + count += 1; + } + assert_eq!(count, 4); + } } From 8b4532f7e810ce4a3d8160ce17b2ee7356f04cd4 Mon Sep 17 00:00:00 2001 From: oiwn Date: Mon, 11 May 2026 07:20:12 +0700 Subject: [PATCH 5/5] update readme --- README.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README.md b/README.md index 4901e16..6801be6 100644 --- a/README.md +++ b/README.md @@ -82,23 +82,6 @@ tarts blank # Simple blank screen tarts matrix ``` -## ๐ŸŽจ Effects Gallery - -| Effect | Description | Perfect for | -|--------|-------------|-------------| -| **matrix** | Classic digital rain falling down your screen | Retro computing vibes | -| **life** | Conway's cellular automaton evolution | Watching patterns emerge | -| **maze** | Procedurally generated maze in real-time | Maze enthusiasts | -| **boids** | Flocking simulation with emergent behavior | Nature lovers | -| **cube** | 3D rotating cube with braille patterns | Geometry fans | -| **crab** | Animated crabs walking and interacting | Cute animations | -| **donut** | Mathematical 3D donut rotation | Math enthusiasts | -| **pipes** | Smooth flowing pipe animations | Mesmerizing effects | -| **fire** | Cozy fireplace simulation | Ambience and relaxation | -| **plasma** | Electric plasma with vibrant colors | Psychedelic effects | -| **constellation** | Drifting stars linked by dotted lines | Calm ambient visuals | -| **blank** | Minimal blank screen | Resource conservation | - ## ๐Ÿงช Development This project uses standard Rust tooling: