diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f233e6..b46ed27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.1.0] - 2026-07-06 + +### Added +- Surprise mode: a bare `sl` (the classic mistyped `ls`) now randomizes the + whole show each run — train type, coal-car count, color livery (named + themes plus fully random one-offs), and speed, with an occasional whistle + (25%), a rare flight (10%), and a very rare crash (5%) + +### Changed +- Any command-line flag disables surprise mode entirely; explicit options + remain fully deterministic and backward compatible + ## [3.0.0] - 2026-07-06 ### Added diff --git a/README.md b/README.md index 1fcdd57..dc3c9a4 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,115 @@ -# 🚂 sl - Steam Locomotive +# 🚂 sl — Steam Locomotive [![GitHub stars](https://img.shields.io/github/stars/developtheweb/slTrain?style=social)](https://github.com/developtheweb/slTrain/stargazers) [![GitHub forks](https://img.shields.io/github/forks/developtheweb/slTrain?style=social)](https://github.com/developtheweb/slTrain/network/members) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/) [![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20unix-lightgrey.svg)](https://github.com/developtheweb/slTrain) +[![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen.svg)](requirements.txt) [![Maintained](https://img.shields.io/badge/maintained-yes-green.svg)](https://github.com/developtheweb/slTrain/commits/main) [![Website](https://img.shields.io/badge/website-StevenMilanese.com-blue.svg)](https://stevenmilanese.com) -A joke command that displays an animated steam locomotive in your terminal when you accidentally type 'sl' instead of 'ls'. +> You typed `sl`. You meant `ls`. The railroad thanks you for your patronage. ``` ==== ________ ___________ _D _| |_______/ \__I_I_____===__|_________| - |(_)--- | H\________/ | | =|___ ___| - / | | H | | | | ||_| |_|| - | | | H |__--------------------| [___] | - | ________|___H__/__|_____/[][]~\_______| | + |(_)--- | H\________/ | | =|___ ___| + / | | H | | | | ||_| |_|| + | | | H |__--------------------| [___] | + | ________|___H__/__|_____/[][]~\_______| | |/ | |-----------I_____I [][] [] D |=======|__ __/ =| o |=-~~\ /~~\ /~~\ /~~\ ____Y___________|__ - |/-=|___|= || || || |_____/~\___/ - \_/ \O=====O=====O=====O_/ \_/ + |/-=|___|= || || || |_____/~\___/ + \_/ \O=====O=====O=====O_/ \_/ ``` -## ðŸŽĨ Demo +Every other command punishes a typo with an error message. `sl` rewards it +with a steam locomotive. The entire point is maximal output for minimal +input — two mistyped letters buy you a train, and you *will* watch it cross +your terminal, because that is the punishment and the prize. -![sl command demo](https://raw.githubusercontent.com/developtheweb/slTrain/main/assets/sl-demo.gif) +This is a single-file, zero-dependency Python train that takes the joke +entirely too seriously: a flicker-free double-buffered renderer, wheels that +actually turn, smoke that drifts and dissipates, coal cars, a whistle, and a +crash mode that earns its flag. -*Watch the train cross your terminal when you mistype!* +## ðŸŽē No two typos look alike + +A bare `sl` — the classic fumbled `ls` — rolls the dice on everything: +locomotive type, coal-car count, color livery (four named themes plus +fully random one-offs), and speed. Sometimes it whistles. Rarely, it flies. +Very rarely, it does not make it across. + +| Event | Odds | +|-------|------| +| ðŸ“Ģ The train whistles | 1 in 4 | +| ✈ïļ The train takes flight | 1 in 10 | +| ðŸ’Ĩ The train does not arrive at the station | 1 in 20 | + +Pass any flag and the dice are off — explicit options are fully +deterministic, so your customizations always behave exactly as written. ## âœĻ Features -- 🚂 **Multiple train types** - Classic, Small, D51, and C51 locomotives (`-t`) -- 🛞 **Animated wheels** - Wheels that actually turn as the train rolls -- ðŸ’Ļ **Particle smoke** - Smoke that drifts behind the train and dissipates -- 🚃 **Coal cars** - Couple up to 8 tenders behind the locomotive (`-n`) -- ✈ïļ **Flying mode** - Make the train fly across the sky with a stardust trail (`-F`) -- ðŸ’Ĩ **Accident mode** - Screen shake, sparks, and a proper BOOM (`-a`) -- ðŸ“Ģ **Whistle** - The train toots as it passes (`-w`) -- ðŸ–Ĩïļ **Flicker-free rendering** - Double-buffered frames on the alternate screen; your terminal contents are restored when the train has passed -- ðŸŽĻ **Colorful ASCII art** - Colored trains with grayscale smoke on 256-color terminals; honors `NO_COLOR` and `--no-color` -- ⚡ **Adjustable speed** - Control animation speed -- 📐 **Terminal-aware** - Handles terminal resizing gracefully; `sl | cat` prints a static train instead of escape codes -- ðŸ›Ąïļ **Clean exit** - Proper cleanup and Ctrl+C handling -- ðŸŠķ **Lightweight** - No external dependencies, pure Python +- ðŸŽē **Surprise mode** — a bare `sl` randomizes the whole show, every run +- 🚂 **Four locomotives** — Classic, Small, D51, and C51 (`-t`) +- 🛞 **Animated wheels** — a 4-frame rotation cycle on every engine +- ðŸ’Ļ **Particle smoke** — drifts behind the train and dissipates, with grayscale shading on 256-color terminals +- 🚃 **Coal cars** — couple up to 8 tenders behind the engine (`-n`) +- ✈ïļ **Flying mode** — smoothstep climb with a stardust trail (`-F`) +- ðŸ’Ĩ **Accident mode** — screen shake, a spark shower, and a proper BOOM (`-a`) +- ðŸ“Ģ **Whistle** — the train toots as it passes (`-w`) +- ðŸ–Ĩïļ **Flicker-free** — double-buffered frames on the alternate screen; your terminal contents come back when the train has passed +- ðŸŽĻ **Respectful of your eyes** — honors `NO_COLOR` and `--no-color`; `sl | cat` prints a static train instead of escape-code soup +- 📐 **Terminal-aware** — live resize handling, clean Ctrl+C, monotonic frame pacing that doesn't drift +- ðŸŠķ **Zero dependencies** — one file, pure Python standard library ## ðŸ“Ķ Installation -### Quick Install (Recommended) - ```bash -# Clone the repository -git clone https://github.com/developtheweb/slTrain.git -cd slTrain - -# Install to /usr/local/bin -sudo make install -``` - -### Platform-Specific Instructions - -
-🐧 Linux - -```bash -# Debian/Ubuntu -git clone https://github.com/developtheweb/slTrain.git -cd slTrain -sudo make install - -# Arch Linux (AUR) -# Coming soon! - -# Manual install -sudo cp sl /usr/local/bin/ -sudo chmod +x /usr/local/bin/sl -``` -
- -
-🍎 macOS - -```bash -# Using Homebrew (coming soon) -# brew install sl - -# Manual install git clone https://github.com/developtheweb/slTrain.git cd slTrain sudo make install ``` -
-
-ðŸģ Docker +That installs to `/usr/local/bin/sl`. Prefer to do it by hand? ```bash -# Run without installing -docker run --rm -it ghcr.io/developtheweb/sl:latest - -# Alias for easy use -alias sl='docker run --rm -it ghcr.io/developtheweb/sl:latest' +sudo cp sl /usr/local/bin/ && sudo chmod +x /usr/local/bin/sl ``` -
+ +**Requirements:** Python 3.6+, a Unix-like terminal with ANSI escape +support, and a sense of humor. Nothing else — see +[requirements.txt](requirements.txt), which is proudly empty. ### Uninstall ```bash sudo make uninstall -# or -sudo rm /usr/local/bin/sl ``` +The train will remember this. + ## 🚀 Usage -Simply type `sl` instead of `ls`: +You don't *use* `sl`. You commit a typo, and `sl` happens to you: ```bash -$ sl # Classic train -$ sl -F # Flying train -$ sl -a # Train accident -$ sl -c # C51 train type -$ sl -l # Long train (D51 pulling coal cars) -$ sl -n 4 # Pull 4 coal cars -$ sl -t small # Small train for narrow terminals -$ sl -w # Sound the whistle -$ sl -s 2.0 # Double speed -$ sl --help # Show help +$ sl # ðŸŽē Surprise! Random train, livery, cars, and speed +``` + +But if you insist on driving: + +```bash +$ sl -t classic # The classic engine, no surprises +$ sl -l # Long train: D51 pulling coal cars +$ sl -n 8 # Maximum coal. The economy is booming +$ sl -F # Flight +$ sl -a # Tragedy +$ sl -w -c # A whistling C51 +$ sl -s 2.0 # You have somewhere to be +$ sl -s 0.1 # You do not ``` ### Options @@ -145,61 +128,81 @@ $ sl --help # Show help | `-v` | `--version` | Show version information | | `-h` | `--help` | Show help message | -## ðŸĪ” Why sl? +Any flag disables surprise mode. The dice only roll for a naked typo. -We've all done it - typed `sl` when we meant `ls`. Instead of getting an error, why not get a gentle reminder in the form of a steam locomotive chugging across your terminal? +## 🔧 How it works -### Benefits: -- 📚 **Learn to type more carefully** - Muscle memory training through humor -- 😄 **Add whimsy to your command line** - Because terminals can be fun too -- 🎭 **Surprise your coworkers** - Watch their confusion turn to delight -- 🧘 **Take a brief mental break** - Sometimes you need a train break +For a joke, it's built like it matters: -## 📋 Requirements +- **Double-buffered rendering.** Each frame is composed into an off-screen + cell buffer and emitted as a single write — no clear-screen between + frames, so nothing flickers, ever. +- **The alternate screen.** The animation runs on the terminal's alternate + buffer, the same trick `vim` and `less` use. When the train is gone, your + scrollback is exactly as you left it. Like it never happened. It happened. +- **A particle system.** Smoke, crash sparks, and stardust are particles + with velocity, drag, and gravity, aging through character ramps + (`@` → `O` → `o` → `*` → `.`) as they dissipate. +- **Monotonic pacing.** Frame timing is anchored to a monotonic clock, so + the train's speed doesn't drift with render cost or system load. +- **An honest fallback.** If stdout isn't a terminal, you get a static + train in plain text. `sl | cat` is a train. `sl > file.txt` is a train. + There is no escaping the train, only escape codes, and those are omitted. -- Python 3.6 or higher -- Unix-like terminal with ANSI escape code support -- A sense of humor 😄 +## ðŸĪ” Why does this exist? + +We've all done it — typed `sl` when we meant `ls`. Instead of +`command not found`, why not a gentle reminder in the form of a steam +locomotive chugging across your terminal? + +- 📚 **Typing discipline through consequences** — muscle memory training, enforced by rail +- 😄 **Whimsy in the command line** — terminals can be fun too +- 🎭 **Coworker delight** — watch confusion turn to joy, then back to confusion when it crashes +- 🧘 **Mandatory micro-breaks** — the train cannot be skipped, only awaited + +## ❓ FAQ + +**Can I stop the train?** +Ctrl+C works and exits cleanly. Learning to type `ls` also works, but nobody +has ever managed it. + +**The train crashed. Is that a bug?** +If you passed `-a`, that's a feature. If you didn't, that's a 1-in-20 roll +of surprise mode, and honestly, it's a little bit on you for typing `sl`. + +**Why would a train fly?** +1-in-10 odds say you'll find out. + +**Is this compatible with the original `sl`?** +The spirit, the D51/C51 art heritage, and the `-a`/`-F`/`-l`/`-c` flags are +all honored. The renderer, particles, and surprise mode are new. ## ðŸĪ Contributing -We love contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. - -### Quick Start for Contributors +Contributions are welcome — new locomotives, new liveries, new disasters. +See the [Contributing Guidelines](CONTRIBUTING.md). ```bash -# Fork and clone the repository git clone https://github.com/YOUR_USERNAME/slTrain.git cd slTrain - -# Create a feature branch git checkout -b feature/amazing-feature - -# Make your changes and test -./sl -F # Test your changes - -# Commit and push +./sl -F # test your changes git commit -m "Add amazing feature" git push origin feature/amazing-feature ``` ## 💖 Support the Project -If you enjoy `sl`, consider supporting the development: - -- ⭐ **Star this repository** - It helps others discover the project -- 🐛 **Report bugs** - Help us improve by [reporting issues](https://github.com/developtheweb/slTrain/issues) -- ðŸ’Ą **Suggest features** - Share your ideas for new train types or animations -- 🌐 **Visit my website** - Learn more at [StevenMilanese.com](https://stevenmilanese.com) -- ☕ **Buy me a coffee** - Support development at [StevenMilanese.com/support](https://stevenmilanese.com/support) - -## 🔒 Security - -Found a security issue? Please see our [Security Policy](SECURITY.md) for responsible disclosure. +- ⭐ **Star this repository** — it helps others discover the train +- 🐛 **Report bugs** — [open an issue](https://github.com/developtheweb/slTrain/issues) +- ðŸ’Ą **Suggest features** — new train types, animations, or calamities +- 🌐 **Visit my website** — [StevenMilanese.com](https://stevenmilanese.com) +- ☕ **Buy me a coffee** — [StevenMilanese.com/support](https://stevenmilanese.com/support) ## 📝 License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +MIT — see [LICENSE](LICENSE). The train is free. The train has always been +free. ## ðŸ‘Ļ‍ðŸ’ŧ Author @@ -212,10 +215,11 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- Inspired by the original `sl` command by Toyoda Masashi -- ASCII art trains adapted from various sources -- Thanks to all [contributors](https://github.com/developtheweb/slTrain/graphs/contributors) who have helped improve this project -- Special thanks to the first stargazer who inspired this update! ⭐ +- Inspired by the original `sl` by **Toyoda Masashi** (1993), who understood + that the punishment for a typo should be beautiful +- The coal car art is adapted from the original `sl` +- Thanks to all [contributors](https://github.com/developtheweb/slTrain/graphs/contributors) +- Special thanks to the first stargazer who inspired this update ⭐ ## 📊 Project Stats @@ -226,9 +230,9 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ---

- Remember: It's not a bug, it's a feature! 🚂 + It's not a bug, it's a locomotive. 🚂

Made with âĪïļ by Reverend Steven Milanese
Visit StevenMilanese.com for more projects -

\ No newline at end of file +

diff --git a/sl b/sl index 22ecc08..8e7e0bd 100755 --- a/sl +++ b/sl @@ -26,7 +26,7 @@ import time from typing import Dict, List, Optional # Version -__version__ = "3.0.0" +__version__ = "3.1.0" # ANSI escape codes for colors and cursor control @@ -70,6 +70,24 @@ CHAR_COLORS = { '~': ANSI.CYAN, } +# Alternate liveries for surprise mode +COLOR_THEMES = [ + CHAR_COLORS, + {'D': ANSI.YELLOW, '_': ANSI.RED, '|': ANSI.RED, + '=': ANSI.YELLOW, 'O': ANSI.WHITE, 'o': ANSI.WHITE, '~': ANSI.MAGENTA}, + {'D': ANSI.WHITE, '_': ANSI.CYAN, '|': ANSI.BLUE, + '=': ANSI.CYAN, 'O': ANSI.WHITE, 'o': ANSI.WHITE, '~': ANSI.BLUE}, + {'D': ANSI.MAGENTA, '_': ANSI.GREEN, '|': ANSI.MAGENTA, + '=': ANSI.CYAN, 'O': ANSI.YELLOW, 'o': ANSI.YELLOW, '~': ANSI.GREEN}, +] + + +def random_theme() -> Dict[str, str]: + """A one-off livery: each character class gets a random color.""" + pool = [ANSI.RED, ANSI.GREEN, ANSI.YELLOW, ANSI.BLUE, + ANSI.MAGENTA, ANSI.CYAN, ANSI.WHITE] + return {ch: random.choice(pool) for ch in CHAR_COLORS} + class Train: """The ASCII art trains, their animated wheel frames, and rolling stock.""" @@ -338,13 +356,15 @@ class SLAnimation: def __init__(self, train_type: str = "classic", speed: float = 1.0, fly: bool = False, accident: bool = False, cars: int = 0, whistle: bool = False, - use_color: bool = True): + use_color: bool = True, + palette: Optional[Dict[str, str]] = None): self.train_type = train_type self.speed = max(0.1, min(speed, 20.0)) self.fly = fly self.accident = accident self.whistle = whistle self.use_color = use_color + self.palette = palette or CHAR_COLORS self.running = True self.resized = False @@ -466,7 +486,7 @@ class SLAnimation: color=self.rail_color) particles.draw(screen) for i, line in enumerate(art): - screen.put(x, y + i, line, charmap=CHAR_COLORS, opaque=True) + screen.put(x, y + i, line, charmap=self.palette, opaque=True) if frame_i < toot_until: screen.put(x + self.funnel_dx + 3, y + self.funnel_dy - 2, 'TOOT! TOOT!', color=ANSI.WHITE) @@ -517,7 +537,7 @@ class SLAnimation: particles.draw(screen) for i, line in enumerate(art): screen.put(x + dx, y + i + dy, line, - charmap=CHAR_COLORS, opaque=True) + charmap=self.palette, opaque=True) if f < 32 and (f // 3) % 2 == 0: color = ANSI.RED if (f // 6) % 2 == 0 else ANSI.YELLOW screen.put(x + 6, y - 2, messages[(f // 6) % 2], color=color) @@ -576,15 +596,32 @@ def main(): and 'NO_COLOR' not in os.environ and sys.stdout.isatty()) + # Surprise mode: a bare `sl` -- the classic mistyped `ls` -- rolls the + # dice on everything, so no two typos look alike. Any flag at all + # switches back to fully deterministic behavior. + palette = None + speed = args.speed + fly, accident, whistle = args.fly, args.accident, args.whistle + if len(sys.argv) == 1: + train_type = random.choice(list(Train.TRAINS)) + cars = random.choice([0, 0, 0, 1, 2, 2, 3, 4, 8]) + palette = random.choice(COLOR_THEMES + [random_theme()]) + speed = random.uniform(0.8, 1.5) + whistle = random.random() < 0.25 + roll = random.random() + accident = roll < 0.05 # rare: the typo ends in tragedy + fly = 0.05 <= roll < 0.15 # rare: the typo takes flight + # Run animation animation = SLAnimation( train_type=train_type, - speed=args.speed, - fly=args.fly, - accident=args.accident, + speed=speed, + fly=fly, + accident=accident, cars=cars, - whistle=args.whistle, + whistle=whistle, use_color=use_color, + palette=palette, ) animation.run()