Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_with_u

dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
# A const IS a field, so without this the rule demands `_nonceSize` for
# `private const int NonceSize` — PascalCase constants are correct .NET style and
# the codebase uses them throughout. Restricting the rule to instance fields keeps
# it aimed at what it was written for. Found when EnforceCodeStyleInBuild surfaced
# 76 IDE1006 violations, every one of them a constant.
dotnet_naming_symbols.private_fields.required_modifiers =

dotnet_naming_style.camel_case_with_underscore.capitalization = camel_case
dotnet_naming_style.camel_case_with_underscore.required_prefix = _
Expand Down
21 changes: 21 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## What changed

<!-- One or two sentences. What a reader of the changelog needs to know. -->

## Why

<!-- The motivating problem. Link an issue if there is one. -->

## Checklist

- [ ] Build is clean — no new warnings (`TreatWarningsAsErrors` is on)
- [ ] Tests pass on **every** shipped target framework
- [ ] Public API changes carry XML docs
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
- [ ] Dependency floors unchanged, or the consumer impact is described below

## Consumer impact

<!-- Delete if none. Note any raised dependency floor, changed public signature,
new target framework, or on-disk format change - including whether existing
stored data stays readable. -->
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ PublishScripts/
*.nupkg
# NuGet Symbol Packages
*.snupkg
# Stray "C:/" / "c:/" directory created on non-Windows when the csproj's
# Windows-style PackageOutputPath is interpreted as a relative path.
**/[Cc]:/

# Per-user Claude Code state — never commit
.claude/settings.local.json
.claude/projects/

# Pack output (matches CI's --output flag)
artifacts/
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Adopted the standards baseline docs, editor config and SDK pin.**
`SECURITY.md`, `CONTRIBUTING.md`, `CLAUDE.md` and a pull request template added;
`.gitignore` and `.editorconfig` replaced with the canonical copies; `global.json`
now pins the SDK band (`10.0.100`, `rollForward: latestFeature`) alongside the
existing Microsoft.Testing.Platform runner setting. An unpinned SDK gives a
contributor different analyzer results from CI, and `TreatWarningsAsErrors` turns
that into a build that fails for them and passes for everyone else.
- **README now states its target frameworks and dependency floors** (§5.3), and
explains why the floors are not per-target-framework.
- **`SplashTagline.RandomBuiltIn`'s XML doc corrected** from "~200 quotes" to
"~300": the built-in pool holds 313 entries. Documentation only; the pool itself
is unchanged.

- **Test suite migrated to xUnit.net v3 (`xunit.v3` 4.0.0)** from `xunit` 2.9.3.
Contributor-facing only — no library code, public API, or shipped package
contents changed. v3 test projects are self-executing console apps and run on
Expand Down
120 changes: 120 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# CLAUDE.md — NextIteration.SpectreConsole.Splash

## This package

A Figgle-and-Spectre.Console splash screen for .NET CLIs. A consumer calls
`SplashScreen.Show("my-cli")` for the defaults, or passes a `SplashOptions` to choose the
Figgle font, a `SplashColors` gradient palette of `#RRGGBB` stops, and a `SplashTagline`
strategy (`None`, `RandomBuiltIn` from a ~300-entry built-in pool, or `FromProvider` with a
callback). There is no DI registration and no command branch — it is one static entry point
with no state. Nothing here consumes another package in the estate, and nothing in the estate
consumes it.

## Things that are easy to get wrong here

- **The single `AnsiConsole.Markup` call is the whole point of the library.** `Renderer`
assembles logo and tagline into one markup string and `SplashScreen.Show` emits it with one
call. The precursor wrote one `AnsiConsole.Write(char)` per character — ~250 calls for a
typical Roman logo, each flushing its own ANSI colour escape — and measured ~60 ms against
under 10 ms for the batched path. Any change that writes incrementally, or that renders
through a Spectre widget instead of a markup string, gives that back. The figure is quoted
in the README and in the `SplashScreen` XML docs, so a rewrite has to re-measure both.
- **Markup escaping is load-bearing, not defensive tidiness.** Figgle glyphs contain `[` and
`]`, and Spectre parses those as markup control characters. This is not hypothetical in the
default font: in Roman, the glyphs for `w`, `K`, `2`, `3`, `5` and `6` all contain a
bracket, so an app called `wget` or `k8s` hits it immediately. `RenderLogo` doubles them
inline per character; `RenderTagline` escapes through `EscapeMarkup` *after* padding,
because padding can only add spaces but reordering the two would be easy and wrong.
Dropping either escape does not throw — it silently corrupts the logo or swallows part of
the tagline as a bogus colour tag.
- **Line-ending normalisation is implemented twice, and the two must agree.**
`SplashScreen.MaxLineLength` scans for `\n` and discounts a preceding `\r` to compute the
width the gradient is generated at; `Renderer.Render` independently splits on `\n` and
`TrimEnd('\r')`s to compute the width the logo is rendered at. Both handle `\r` because the
code treats Figgle's line endings as platform-dependent (on Linux with Figgle 0.6.6 the
output is `\n`-only, so the Windows leg of the matrix is what actually exercises the other
branch). If they ever disagree the gradient is
sampled to the wrong width and the sweep no longer reaches the end of the logo — a visual
bug with no exception and no failing test unless one is written for the pair together.
- **`Gradient` deliberately trusts its input; `SplashColors` is the only validator.**
`HexToRgb` and `CharToHex` do no checking at all — `CharToHex` maps anything unrecognised
to `0` with an "unreachable" comment — because `SplashColors`' constructor has already
rejected anything that is not `#RRGGBB`. That invariant is the contract between the two
types. Any new path that reaches `Gradient.Generate` without going through a constructed
`SplashColors` silently renders wrong colours instead of throwing.
- **Spaces skip the colour escape entirely.** `RenderLogo` appends a bare `' '` rather than
wrapping it, which saves ~14 markup characters per space and is a large fraction of a
Figgle logo. It is safe only because a space has no visible foreground; a change that gives
the splash a background colour makes it wrong.
- **The font renders on first `Show`, not at class load.** `FiggleFonts.Roman` is touched
lazily via the `SplashOptions` default. Hoisting it into a static initialiser moves that
cost onto every consumer that loads the assembly without showing a splash.

## Repository baseline

This repo conforms to
[NextIteration.Standards](https://github.com/StuartMeeks/NextIteration.Standards).
Build properties, test stack, CI shape, and branch protection are defined there, not
here. Before changing any of those, read `STANDARD.md`; if this repo needs to deviate,
that is an `EXCEPTIONS.md` entry in the standards repo, not a local difference.

## Non-negotiables

- **The build must be clean.** `TreatWarningsAsErrors` is on and analyzers run at
`latest`. A warning is a build failure.
- **Tests must pass on every shipped target framework** (`net8.0` and `net10.0`). A change
that only passes on one is not finished. Shipping a target you do not test is a defect,
not a scoping decision.
- **Dependency floors are deliberate.** A `PackageReference` version in a library is a
*minimum* NuGet forces on every consumer, so raising a floor is a consumer-visible change
even when nothing in the code needs it. Never raise one to silence a warning. This repo
has no *per-TFM* floors, and that is not an oversight: Figgle, Figgle.Fonts and
Spectre.Console are all pre-1.0 and version independently of the .NET runtime, so
`STANDARD.md` §1.5 gives them a single common floor at the version actually built and
tested against. §1.4's per-TFM rule applies only to runtime-aligned Microsoft platform
packages, of which this repo references none.
- **Public API changes need XML docs.** `GenerateDocumentationFile` is on and the public
surface is fully documented.
- **Update `CHANGELOG.md`** under `[Unreleased]`, saying what changed and why.

## Dependabot

Minor and patch updates auto-merge behind CI. Major updates stay open for a human — that
is deliberate, not a backlog to clear. `dependabot.yml` here has **no `ignore` block**, and
adding one would be wrong: §4.10 scopes that list to packages carrying per-TFM floors, and
this repo has none, so every major bump it files is genuinely reviewable. `audit-drift.sh`
checks the `ignore` set against the repo's actual per-TFM floors, so an inherited entry
fails the audit rather than passing quietly.

## After opening a pull request

Watch CI to completion, report the real check results, then **offer to merge** in the same
message. Do not stop silently and wait to be asked.

- If branch protection blocks the merge, say so and offer `gh pr merge --admin`. These
repos require a code-owner review only the maintainer can give, which is why `--admin` is
the tool — but that mechanic is not the reason the offer is wanted. The reason is simply
that the maintainer has grown comfortable delegating this to an agent, so treat the
latest instruction as authoritative over this file.
- **Merge only on an explicit yes.** The offer is pre-approved; the action is not.
- Never offer while checks are failing or still running. Report that state instead.
- Report the checks that actually ran. A skipped check is not a passing check, and branch
protection treats them differently from how they read in a summary.

## CI

The single required status check is `ci` — an aggregating gate over `build` and `test`.
Renaming those jobs is safe; the ruleset never names them. Do not make them required
checks directly.

**`ci.yml` is not yours to edit freely.** `STANDARD.md` §3.0.1 requires its non-comment
content to match `templates/.github/workflows/ci.yml` in the standards repo exactly, apart
from the tag glob and steps carrying an `EXCEPTIONS.md` entry. This repo has no such entry,
so the only permitted difference is `tags: [ 'v*' ]` and the header comment.
`audit-drift.sh` checks this. Change the template first, then every repo — never this file
alone.

The `test` matrix runs Linux, Windows and macOS. Nothing here touches the filesystem or an
OS store, so no platform has its own backend — but Figgle's line endings and Spectre's
console capability detection both differ by platform, and §3.1.1 removes platforms by
exception only. Do not drop a leg to save CI minutes.
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Contributing

Issues and pull requests are welcome.

## Before you open a PR

- **The build must be clean.** `TreatWarningsAsErrors` is on and analyzers run at
`latest`. A warning is a build failure, not a suggestion.
- **Tests run on every target framework** the project ships. `dotnet test` covers
`net8.0` and `net10.0`; a change that only passes on one is not finished.
- **Public API changes need XML docs.** `GenerateDocumentationFile` is on and the
public surface is fully documented — keep it that way.
- **Update `CHANGELOG.md`.** Keep a Changelog format, under `[Unreleased]`. Say what
changed and why; "bump dependency" without a reason is not useful six months later.

## Dependency changes

Dependency floors are deliberate. A `PackageReference` version in a library is a
*minimum* NuGet forces on every consumer, so raising a floor is a
consumer-visible change even when nothing in the code needs it. Read
`STANDARD.md` sections 1.4 and 1.5 in `NextIteration.Standards` before changing one.

This package has no *per-target-framework* floors, which is a consequence of
those two clauses rather than an omission. Figgle, Figgle.Fonts and
Spectre.Console are all pre-1.0 and version independently of the .NET runtime, so
section 1.5 gives them a single common floor at the version actually built and
tested against — per-TFM floors would be meaningless where breaking changes land
between minors. Section 1.4's per-TFM rule covers runtime-aligned Microsoft
platform packages, and this package references none.

Minor and patch bumps arrive automatically via Dependabot and merge behind CI.
Major bumps stay open for a human — that is deliberate, not a backlog.

## Repository conventions

These repositories share a baseline defined in
[NextIteration.Standards](https://github.com/StuartMeeks/NextIteration.Standards):
build properties, test stack, CI shape, and branch protection. If a change would
deviate from it, raise that there first — a per-repo exception is a documented
entry, not a quiet difference.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ A reusable Figgle-and-Spectre.Console splash screen for .NET CLIs.
- Performance-first: the entire splash is assembled as one markup string and
emitted via a single `AnsiConsole.Markup` call.

Targets `net8.0` and `net10.0`. Both targets carry an identical public surface and are
covered by the same test suite on Linux, Windows, and macOS.

## Install

```bash
Expand Down Expand Up @@ -76,6 +79,25 @@ Additional wins:
- Space characters skip the colour-escape wrapper entirely (saves ~14
markup chars per space).

## Requirements

- **.NET 8.0** or **.NET 10.0** (the package multi-targets `net8.0;net10.0`)
- **Spectre.Console** 0.57.2+
- **Figgle** and **Figgle.Fonts** 0.6.6+

These floors are the versions the package is built and tested against. All three are
pre-1.0 and version independently of the .NET runtime, so each takes a single common
floor rather than one per target framework — a per-TFM floor would be meaningless where
breaking changes land between minors.

Everything else is transitive.

## Contributing

Issues and PRs welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md). This repository
follows the baseline in
[NextIteration.Standards](https://github.com/StuartMeeks/NextIteration.Standards).

## License

MIT — see [LICENSE](./LICENSE).
56 changes: 56 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Security policy

## Reporting a vulnerability

Report privately through GitHub's **Report a vulnerability** button under this
repository's Security tab, which opens a private advisory visible only to the
maintainers. Please do not open a public issue for a suspected vulnerability.

Include the affected package and version, what an attacker can achieve, and a
reproduction if you have one.

You can expect an acknowledgement within 7 days, an assessment within 14, and
credit in the advisory and changelog unless you ask otherwise.

## Supported versions

Only the latest released minor of each package receives security fixes. These are
pre-1.0 libraries and there are no long-term support branches.

## Scope

This library reads no input it did not receive from its caller, opens no file,
network socket or OS store, and stores nothing. It renders a string to the
terminal. That makes the attack surface narrow, and worth stating precisely
rather than waving at.

The one real boundary is **terminal control sequences**. The rendered splash is a
Spectre.Console markup string emitted with a single `AnsiConsole.Markup` call, so
anything the caller supplies that reaches the output has to survive markup
escaping. Two paths carry caller-controlled text:

- **`AppName`**, which is rendered through a Figgle font first. Figgle glyphs
legitimately contain `[` and `]`, so the renderer doubles both — they become
markup literals rather than colour tags.
- **`SplashTagline.FromProvider`**, whose return value is escaped the same way
after word-wrapping and padding.

A caller-supplied string that escaped that handling and reached the terminal as a
live escape sequence would be in scope: on some terminals control sequences can
reposition the cursor, alter the window title, or in the worst historical cases
be replayed as input. If you can make either path emit an unescaped `[` that
Spectre interprets, or get a raw ESC through, that is a report worth filing.

Two things are explicitly **not** claimed:

- **The tagline provider is trusted code, not untrusted input.** `FromProvider`
takes a `Func<string?>` the consumer wrote. A consumer that pipes unvalidated
remote data into it owns that decision; the library escapes markup but does not
sanitise arbitrary control characters.
- **Nothing here is a security control.** There is no secret, no credential, no
persistence, and no privilege boundary being enforced. A splash screen cannot
protect anything, so no report should assume it was trying to.

Reports demonstrating a break *within* those stated boundaries are in scope and
welcome. Reports that only restate a documented limitation are not
vulnerabilities.
4 changes: 4 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
Expand Down
2 changes: 1 addition & 1 deletion src/NextIteration.SpectreConsole.Splash/SplashTagline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace NextIteration.SpectreConsole.Splash
/// Tagline strategy for the splash screen.
/// <list type="bullet">
/// <item><see cref="None"/> — render no tagline at all.</item>
/// <item><see cref="RandomBuiltIn"/> — pick a random quote from the library's built-in list (~200 quotes).</item>
/// <item><see cref="RandomBuiltIn"/> — pick a random quote from the library's built-in list (~300 quotes).</item>
/// <item><see cref="FromProvider"/> — supply your own provider callback.</item>
/// </list>
/// </summary>
Expand Down