diff --git a/CHANGES.md b/CHANGES.md
index 875b6c5..dc22e08 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,15 @@
# woad.Rust - Changes
+## 0.1.0-alpha1 - 22nd October 2026
+
+* Colour policy from `WOAD_COLOUR` / `WOAD_COLOR`, `NO_COLOR`, `CLICOLOR_FORCE`, and `CLICOLOR`, cached until `refresh()`;
+* Stream-conditional sequences (`seq_for`, `supports_colour`, attribute helpers) for `IsTerminal` destinations, plus Unix `AsFd` equivalents of the descriptor API;
+* Packed `version()` in the **woad** `WOAD_VER` layout (`0x41` alpha 1) and `version_string()`;
+* Attribute sequences `BOLD` and `BLINKING`;
+* Windows console-mode and handle currencies are left for a later alpha;
+
+
## 0.0.1 - 12th September 2026
* SGR colour and reset codes (`RESET`, `FG_*`, `BG_*`, including bright variants);
diff --git a/Cargo.lock b/Cargo.lock
index 5c479f6..08c6ee0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,4 +4,4 @@ version = 3
[[package]]
name = "woad"
-version = "0.0.1"
+version = "0.1.0-alpha1"
diff --git a/Cargo.toml b/Cargo.toml
index 2e233f1..9ddd57f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,7 +10,7 @@ authors = [
categories = [
"command-line-interface",
]
-description = "Minimal ANSI terminal colour codes (for Rust)"
+description = "Minimal ANSI terminal colour codes, with stream-conditional emission (for Rust)"
documentation = "https://docs.rs/woad"
edition = "2021"
exclude = [
@@ -26,6 +26,7 @@ homepage = "https://github.com/synesissoftware/woad.Rust"
keywords = [
"ansi",
"colour",
+ "no-color",
"sgr",
"terminal",
"tty",
@@ -35,7 +36,7 @@ name = "woad"
readme = "README.md"
repository = "https://github.com/synesissoftware/woad.Rust"
rust-version = "1.74"
-version = "0.0.1"
+version = "0.1.0-alpha1"
# ##########################################################
@@ -45,6 +46,10 @@ version = "0.0.1"
name = "woad"
path = "src/lib.rs"
+[[example]]
+name = "colour_for"
+path = "examples/colour_for/main.rs"
+
[[example]]
name = "versions"
path = "examples/versions/main.rs"
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 89c78a2..6e70ae1 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -1,8 +1,10 @@
# woad.Rust - Examples
-| Name | Source | Summary |
-| ---- | ------ | ------- |
+| Name | Source | Summary |
+| -------------- | -------------------------------------------------------------- | ---------------------------------------------------- |
+| **colour_for** | [examples/colour_for/main.rs](./examples/colour_for/main.rs) | Emits colour only when the destination warrants it |
+| **versions** | [examples/versions/main.rs](./examples/versions/main.rs) | Prints `version_string()` and packed `version()` |
diff --git a/NEWS.md b/NEWS.md
index 603a10d..8914cb2 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,10 +1,11 @@
# woad.Rust - News
-| Date | News Item | Details |
-| ---------------- | --------- | --------------------------- |
-| 12th September 2026 | [**woad.Rust** 0.0.1](https://github.com/synesissoftware/woad.Rust/releases/tag/0.0.1) | SGR colour and reset codes |
-| 15th August 2026 | [**woad.Rust** 0.0.0](https://github.com/synesissoftware/woad.Rust/releases/tag/0.0.0) | Initial project scaffolding |
+| Date | News Item | Details |
+| ------------------- | --------------------------- | ------- |
+| 22nd October 2026 | [**woad.Rust** 0.1.0-alpha1](https://github.com/synesissoftware/woad.Rust/releases/tag/0.1.0-alpha1) | Stream-conditional `seq_for` and colour policy |
+| 12th September 2026 | [**woad.Rust** 0.0.1](https://github.com/synesissoftware/woad.Rust/releases/tag/0.0.1) | SGR colour and reset codes |
+| 15th August 2026 | [**woad.Rust** 0.0.0](https://github.com/synesissoftware/woad.Rust/releases/tag/0.0.0) | Initial project scaffolding |
diff --git a/README.md b/README.md
index c1a0c23..e307a87 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,13 @@ Minimal ANSI terminal colour codes, for Rust
## Table of Contents
- [Introduction](#introduction)
+- [Developer Experience](#developer-experience)
- [Installation](#installation)
- [Components](#components)
+ - [Fixed sequences](#fixed-sequences)
+ - [Stream-conditional sequences](#stream-conditional-sequences)
+ - [Colour policy](#colour-policy)
+ - [Version](#version)
- [Project Information](#project-information)
- [Where to get help](#where-to-get-help)
- [Contribution guidelines](#contribution-guidelines)
@@ -29,9 +34,21 @@ Minimal ANSI terminal colour codes, for Rust
## Introduction
-**woad** provides the smallest useful set of fixed ANSI SGR colour sequences for library authors. It is not a console or TUI framework.
+**woad** provides fixed ANSI SGR sequences, and stream-conditional counterparts that emit only when the destination warrants colour. It is not a console or TUI framework.
-**woad.Rust** is the **Rust** implementation.
+**woad.Rust** is the **Rust** implementation. This alpha matches the dynamic API of **woad** off Windows. Console-mode changes and Windows handle currencies are not included.
+
+
+## Developer Experience
+
+**woad.Rust** builds with Cargo. The minimum supported Rust version is **1.74**.
+
+```bash
+cargo test
+cargo run --example colour_for
+```
+
+`colour_for` prints colour only when stdout or stderr warrants it. Pipe it through `cat`, then repeat with `WOAD_COLOUR=always` and `WOAD_COLOUR=never`. Fixed sequences need no policy check. The conditional helpers are under [Components](#components).
## Installation
@@ -39,13 +56,16 @@ Minimal ANSI terminal colour codes, for Rust
Reference in **Cargo.toml** in the usual way:
```toml
-woad = { version = "0" }
+woad = { version = "0.1.0-alpha1" }
```
## Components
-**woad.Rust** ships SGR string constants (`RESET`, `FG_*`, `BG_*`, including bright variants) and `VERSION`. TTY/stream gating and Windows virtual-terminal opt-in are not implemented yet.
+
+### Fixed sequences
+
+String constants: `RESET`, `BOLD`, `BLINKING`, `FG_*`, and `BG_*`, the colours each in normal and `BRIGHT` variants. They always emit.
```rust
use woad::{FG_GREEN, RESET};
@@ -56,6 +76,44 @@ fn main() {
```
+### Stream-conditional sequences
+
+`seq_for`, `fg_for`, and `bg_for` yield the sequence when the destination warrants colour, and `""` when it does not. `reset_for`, `bold_for`, and `blinking_for` are the attribute forms. The destination is any [`std::io::IsTerminal`](https://doc.rust-lang.org/std/io/trait.IsTerminal.html) value (`Stdout`, `Stderr`, `Stdin`, `File`, and on Unix a borrowed file descriptor).
+
+```rust
+let stdout = std::io::stdout();
+
+println!(
+ "{}ok{}",
+ woad::seq_for(woad::FG_GREEN, &stdout),
+ woad::reset_for(&stdout),
+);
+```
+
+On Unix, `fd_is_tty`, `fd_supports_colour`, and `seq_for_fd` take [`AsFd`](https://doc.rust-lang.org/std/os/fd/trait.AsFd.html). A missing destination is `supports_colour_absent` / `seq_for_absent` (the C null-stream case: the policy alone).
+
+
+### Colour policy
+
+The environment is read once, on first need, until `refresh()`:
+
+| Variable | Value | Effect |
+| --- | --- | --- |
+| `WOAD_COLOUR`, or `WOAD_COLOR` | `1`, `always`, `force`, `on`, `true`, `yes` | Force colour |
+| `WOAD_COLOUR`, or `WOAD_COLOR` | `0`, `false`, `never`, `no`, `none`, `off` | Inhibit colour |
+| `WOAD_COLOUR`, or `WOAD_COLOR` | `auto`, or anything unrecognised | Defer to the destination |
+| `NO_COLOR` | set and non-empty | Inhibit colour |
+| `CLICOLOR_FORCE` | set, non-empty, and not `0` | Force colour |
+| `CLICOLOR` | `0` | Inhibit colour |
+
+`WOAD_COLOUR` / `WOAD_COLOR` are case-insensitive. In the deferring case, a `TERM` of `dumb` also suppresses colour. `console_supports_ansi()` is always `true` in this alpha.
+
+
+### Version
+
+`VERSION` and `version_string()` are the SemVer string, including the prerelease label (`0.1.0-alpha1`). `version()` is the packed **woad** `WOAD_VER` (`0x00010041`; low byte `0x41` is alpha 1).
+
+
## Project Information
diff --git a/TODO.md b/TODO.md
index 9b0322b..4ad32da 100644
--- a/TODO.md
+++ b/TODO.md
@@ -11,8 +11,8 @@
## Functional improvements
* [x] ~~~SGR colour and reset codes~~~ - ✅;
-* [ ] TTY-conditional colour codes (process and per-stream);
-* [ ] Windows virtual-terminal gating (OS build + `GetConsoleMode`);
+* [x] ~~~TTY-conditional colour codes (process and per-stream)~~~ - ✅;
+* [ ] Windows virtual-terminal gating (OS build + `GetConsoleMode`) and handle currencies;
## Performance improvements
diff --git a/examples/colour_for/main.rs b/examples/colour_for/main.rs
new file mode 100644
index 0000000..fe5680a
--- /dev/null
+++ b/examples/colour_for/main.rs
@@ -0,0 +1,60 @@
+// Emits colour only when the destination warrants it.
+//
+// cargo run --example colour_for
+// cargo run --example colour_for | cat
+// WOAD_COLOUR=always cargo run --example colour_for | cat
+// WOAD_COLOUR=never cargo run --example colour_for
+
+fn main() {
+ let stdout = std::io::stdout();
+ let stderr = std::io::stderr();
+
+ println!(
+ "policy: {}; stdout is{} a tty; stderr is{} a tty",
+ woad::colour_policy(),
+ if woad::stream_is_tty(&stdout) { "" } else { " not" },
+ if woad::stream_is_tty(&stderr) { "" } else { " not" },
+ );
+
+ println!(
+ "{}this line is green if, and only if, stdout warrants colour{}",
+ woad::seq_for(woad::FG_GREEN, &stdout),
+ woad::reset_for(&stdout),
+ );
+
+ println!(
+ "{}this line is bold if, and only if, stdout warrants colour{}",
+ woad::bold_for(&stdout),
+ woad::reset_for(&stdout),
+ );
+
+ println!(
+ "{}this line blinks if, and only if, stdout warrants colour{}",
+ woad::blinking_for(&stdout),
+ woad::reset_for(&stdout),
+ );
+
+ eprintln!(
+ "{}{}this line is white-on-red if, and only if, stderr warrants colour{}",
+ woad::bg_for(woad::BG_RED, &stderr),
+ woad::fg_for(woad::FG_BRIGHT_WHITE, &stderr),
+ woad::reset_for(&stderr),
+ );
+
+ println!(
+ "{}this line is yellow if, and only if, stdout warrants colour{}",
+ woad::fg_for(woad::FG_YELLOW, &stdout),
+ woad::reset_for(&stdout),
+ );
+
+ #[cfg(unix)]
+ {
+ use std::os::fd::AsFd as _;
+
+ println!(
+ "{}this line is cyan if, and only if, stdout's descriptor warrants colour{}",
+ woad::seq_for_fd(woad::FG_CYAN, stdout.as_fd()),
+ woad::seq_for_fd(woad::RESET, stdout.as_fd()),
+ );
+ }
+}
diff --git a/examples/versions/main.rs b/examples/versions/main.rs
index 9886976..ad22cf8 100644
--- a/examples/versions/main.rs
+++ b/examples/versions/main.rs
@@ -1,3 +1,11 @@
fn main() {
- println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"),);
+ let stdout = std::io::stdout();
+
+ println!(
+ "{}v{}{} ({:#010x})",
+ woad::seq_for(woad::FG_GREEN, &stdout),
+ woad::version_string(),
+ woad::reset_for(&stdout),
+ woad::version(),
+ );
}
diff --git a/src/dynamic.rs b/src/dynamic.rs
new file mode 100644
index 0000000..b8065b1
--- /dev/null
+++ b/src/dynamic.rs
@@ -0,0 +1,727 @@
+//! Stream-conditional colour: policy, terminal attachment, and sequence
+//! selection.
+//!
+//! This is the dynamic half of **woad**. Fixed sequences live in the
+//! crate root. Windows console handles and virtual-terminal opt-in are
+//! not implemented; see [`console_supports_ansi`].
+
+use std::{
+ io::IsTerminal,
+ sync::Mutex,
+};
+
+#[cfg(unix)]
+use std::os::fd::AsFd;
+
+
+// The truey/falsey token sets come from to-be.Rust
+// (https://github.com/synesissoftware/to-be.Rust). Classification is local
+// so this crate stays dependency-free. woad adds `always` / `force` and
+// `never` / `none`, compares case-insensitively, and does not trim.
+
+
+const TRUEY : &[&str] = &["1", "always", "force", "on", "true", "yes"];
+
+const FALSEY : &[&str] = &["0", "false", "never", "no", "none", "off"];
+
+
+/// Prevailing colour policy, matching `WOAD_COLOUR_POLICY_*`.
+#[derive(Clone)]
+#[derive(Copy)]
+#[derive(Debug)]
+#[derive(Eq, PartialEq)]
+#[repr(i32)]
+pub enum ColourPolicy {
+ /// Defer to the destination: a terminal that is not `TERM=dumb`.
+ Auto = 0,
+ /// Emit colour for every destination.
+ Force = 1,
+ /// Emit colour for no destination.
+ Inhibit = 2,
+}
+
+impl ColourPolicy {
+ /// Stable policy name: `auto`, `force`, or `inhibit`.
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Auto => "auto",
+ Self::Force => "force",
+ Self::Inhibit => "inhibit",
+ }
+ }
+}
+
+impl std::fmt::Display for ColourPolicy {
+ fn fmt(
+ &self,
+ f : &mut std::fmt::Formatter<'_>,
+ ) -> std::fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+
+struct Cached {
+ policy : ColourPolicy,
+ term_is_dumb : bool,
+}
+
+static CACHE : Mutex