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
[](https://github.com/developtheweb/slTrain/stargazers)
[](https://github.com/developtheweb/slTrain/network/members)
[](https://opensource.org/licenses/MIT)
[](https://www.python.org/downloads/)
[](https://github.com/developtheweb/slTrain)
+[](requirements.txt)
[](https://github.com/developtheweb/slTrain/commits/main)
[](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.
-
+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()