Skip to content

Commit

Permalink
Update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
schneems committed Oct 13, 2024
1 parent 929f36d commit febfe4d
Showing 1 changed file with 21 additions and 22 deletions.
43 changes: 21 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ Bulletproof printing for bullet point text

## What

An opinonated logger aimed at streaming text output (of scripts or buildpacks) to users. The format is loosely based on markdown headers and bulletpoints, hence the name.
An opinionated logger aimed at streaming text output (of scripts or buildpacks) to users. The format is loosely based on markdown headers and bullet points, hence the name.

## Why

This work started as a shared output format for Heroku's Cloud Native Buildpack (CNB) efforts, which are written in Rust. You can learn more about Heroku's [Cloud Native Buildpacks here](https://github.com/heroku/buildpacks).

# Use

Add bullet_stream to your project:
Add `bullet_stream` to your project:

```ignore
$ cargo add bullet_stream
Expand All @@ -37,7 +37,7 @@ output.done();

## Living style guide

To view the output format and read a living style guide you can run:
To view the output format and read a living style guide, you can run:

```ignore
$ git clone <repo-url>
Expand All @@ -46,32 +46,31 @@ $ cargo run --example style_guide

## Colors

In nature, colors and contrasts are used to emphasize differences and danger. [`Output`]
In nature, colors and contrasts are used to emphasize differences and danger. [`Print`]
utilizes common ANSI escape characters to highlight what's important and deemphasize what's not.
The output experience is designed from the ground up to be streamed to a user's terminal correctly.

## Consistent indentation and newlines

Help your users focus on what's happening, not on inconsistent formatting. The [`Output`]
is a consuming, stateful design. That means you can use Rust's powerful type system to ensure
Help your users focus on what's happening rather than on inconsistent formatting. The [`Print`] is a consuming, stateful design. That means you can use Rust's powerful type system to ensure
only the output you expect, in the style you want, is emitted to the screen. See the documentation
in the [`state`] module for more information.

## Requirements

The project has some unique requirements that might not be obvious at first glance:

- Assume screen clearing is not available: Text UI tools such as progress bars rely on ANSI excape codes to clear and re-draw lines which simulates animation. A primary goal of this project is to be used in contexts like a git hook where each line is prefixed via `remote >`. The library provides tooling for alternative append-only "spinners" that denote the passage of time without requiring a screen re-draw.
- Atomic ANSI: Bullet stream uses ANSI codes to colorize output, but it cannot predict if/when the stream will be disconnected. In that event, we don't want to leave the user's screen accidentally blue (or some other color) and so the library favors always writing an ANSI reset code for every line of output. This also ensures any wrapped prefixes like a `remote >` are not accidentally colorized.
- Accessability over style: While the project uses ANSI codes to colorize output, it relies on the most common colors likely to be supported by most shells/terminals/command-prompts.
- Distinguish between owned and unowned output: Any messages a script author emits are "owned" while calling another process and streaming the output (like `bundle install`) are "unowned". Bullet stream uses leader characters and color to denote "owned" output while unowned output carries no markers and is generally indented.
- Favor ease of use over runtime performance: It's assumed that the script/buildpack will call commands and perform network or system IO that should dwarf the cost of allocating a String. It's not that this project aims to be needlessly expensive, however if raw streaming performance is your goal, this project is not for you.
- Assume screen clearing is not available: Text UI tools such as progress bars rely on ANSI escape codes to clear and redraw lines, which simulates animation. A primary goal of this project is to be used in contexts like a git hook, where each line is prefixed via `remote >`. The library provides tooling for alternative append-only "spinners" that denote the passage of time without requiring a screen redraw.
- Atomic ANSI: Bullet stream uses ANSI codes to colorize output, but it cannot predict if/when the stream will be disconnected. In that event, we don't want to leave the user's screen accidentally blue (or some other color), so the library favors always writing an ANSI reset code for every line of output. This also ensures that any wrapped prefixes like a `remote >` are not accidentally colorized.
- Accessibility over style: While the project uses ANSI codes to colorize output, it relies on the most common colors likely to be supported by most shells, terminals, and command prompts.
- Distinguish between owned and unowned output: Any messages a script author emits are "owned" while calling another process and streaming the output (like `bundle install`) are "unowned". Bullet stream uses leader characters and color to denote "owned" output, while unowned output carries no markers and is generally indented.
- Favor ease of use over runtime performance: It's assumed that the script/buildpack will call commands and perform network or system IO that should dwarf the cost of allocating a String. It's not that this project aims to be needlessly expensive; however, if raw streaming performance is your goal, this project is not for you.

## Usage

### Ricochet

The library design relies on a consuming struct design to guarantee output consistency. That means that you'll end up needing to assign the `bullet_stream` result just about every time you use it for example:
The library design relies on a consuming struct design to guarantee output consistency. That means that you'll end up needing to assign the `bullet_stream` result just about every time you use it, for example:

```rust
use bullet_stream::{Print, state::{Bullet, Header, SubBullet}};
Expand All @@ -97,15 +96,15 @@ log.done();

### Push logic down, bubble information up

Any state you send to a function, you must retrieve. There are examples in:
Any state you send to a function must be retrieved. There are examples in:

- [`state::Header`]
- [`state::Bullet`]
- [`state::SubBullet`]
- [`state::Stream`]
- [`state::Background`]

In general, we recommend pushing business logic down into functions, and rather than trying to thread logging state throughout every possible function, rely on functions to bubble up information to log.
In general, we recommend breaking business logic down into functions. Rather than threading the logging state throughout every possible function, rely on functions to bubble up information to log.

Here's an example of logging by passing the output state into functions:

Expand Down Expand Up @@ -143,9 +142,9 @@ where
}
```

In this above example the `install_ruby` function both performs logic and logs information. It results in a very larget function signature. If the function also needed to return information it would need to use a tuple to return both the logger and the information.
In the above example, the `install_ruby` function both performs logic and logs information, resulting in a very large function signature. If the function also needed to return information, it would need to use a tuple to return both the logger and the information.

Here's an alternative where the all information needed to log is brought up to the same top level, and the functions don't need to have massive type signatures:
Here's an alternative where the all information needed to log is brought up to the same top-level, and the functions don't need to have massive type signatures:

```rust
// Example of bubbling up information to the logger
Expand Down Expand Up @@ -180,9 +179,9 @@ It's not **bad** if you want to pass your output around to functions, but it is

### Async support

> Status: Experimental/WIP, if you've got a better suggestion let us know.
> Status: Experimental/WIP; if you've got a better suggestion, let us know.
Because the logger is stateful and consuming logging from within an async or parallel execution context is tricky. We recommend to use the same pattern as above to bubble up information that can be logged between syncronization points in the program.
Because the logger is stateful, consuming logging from within an async or parallel execution context is tricky. We recommend using the same pattern as above to bubble up information that can be logged between synchronization points in the program.

For example, here's some hand-rolled output from code that uses async:

Expand All @@ -207,7 +206,7 @@ For example, here's some hand-rolled output from code that uses async:
[GET] http://archive.ubuntu.com/ubuntu/dists/jammy-security/main/binary-amd64/by-hash/SHA256/9943ee3b3104b37d0ee219fae65f261d0c61c96bb3978fe92e6573c9dcd88862
```

In this example the get and cached lines are logged from within an async context. Here's an example of a refactor that could use the bullet stream library:
In this example, the get and cached lines are logged within an async context. Here's an example of a refactor that could use the bullet stream library:

```text
# Heroku Debian Packages Buildpack (v0.0.1)
Expand All @@ -227,13 +226,13 @@ In this example the get and cached lines are logged from within an async context
- Processing ..... (Done 4s)
```

In this example the output states what it's going to do by listing the package source locations and then after it downloads them there's a syncronization point before it has enough information to output the which archives were downloaded and their SHAs and begins processing them (again asyncronously).
In this example, the output states what it's going to do by listing the package source locations. After it downloads them, there's a synchronization point before it has enough information to output which archives were downloaded and their SHAs and begin processing them (again asynchronously).

Alternatively you could wrap a `SubBullet` state struct in an Arc and try passing it around, or use bullet_stream for top level printing while printing inside of an async context could happen via `println`.
Alternatively, you could wrap a `SubBullet` state struct in an Arc and try passing it around, or use `bullet_stream` for top-level printing. Printing inside an async context could happen via `println`.

### Generics

Bullet stream works with anything that is `Write + Send + Sync + 'static` but most people will use `std::io::Stdout` or `std::io::Stderr`. If you know a specific type you want to output to, then you can simplify your method definitions.
Bullet stream works with anything that is `Write + Send + Sync + 'static,` but most people will use `std::io::Stdout` or `std::io::Stderr`. If you know a specific type you want to output to, you can simplify your method definitions.

For example:

Expand Down

0 comments on commit febfe4d

Please sign in to comment.