Skip to content

Commit

Permalink
rename Sampler to Translator
Browse files Browse the repository at this point in the history
  • Loading branch information
apparebit committed Aug 4, 2024
1 parent e3e9904 commit 2af5c80
Show file tree
Hide file tree
Showing 19 changed files with 416 additions and 226 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## v0.11.0 (2024-xx-xx)

### Changes

* Rename `Sampler` to `Translator`.
* Edit documentation for correctness and clarity.


## v0.10.0 (2024-07-12)

### New Features
Expand Down
10 changes: 5 additions & 5 deletions docs/src/deepdive/progressbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,17 +181,17 @@ in Python even more ergonomic and is well worth the extra engineering effort.
### 4. Adjust Styles to Reality

Once the terminal has been set up, the progress bar script uses the
[`current_sampler`]'s [`is_dark_theme`] to pick the attendant style and then
[`current_translator`]'s [`is_dark_theme`] to pick the attendant style and then
adjusts that style to the terminal's [`Terminal.fidelity`]:

```python
style = DARK_MODE_BAR if current_sampler().is_dark_theme() else LIGHT_MODE_BAR
style = DARK_MODE_BAR if current_translator().is_dark_theme() else LIGHT_MODE_BAR
style = style.prepare(term.fidelity)
```

Doing so once during startup means that the resulting styles are ready for
(repeated) display and incurs the overhead of color conversion only once.
Between [`Style.prepare`] and [`Sampler.cap`], updating styles and colors to
Between [`Style.prepare`] and [`Translator.cap`], updating styles and colors to
match a given fidelity level also is positively easy.

In other words, "reality," as far as this progress bar is concerned, has two
Expand Down Expand Up @@ -291,15 +291,15 @@ a complete change of direction.

</div>

[`current_sampler`]: https://apparebit.github.io/prettypretty/python/prettypretty/theme.html#prettypretty.theme.current_sampler
[`current_translator`]: https://apparebit.github.io/prettypretty/python/prettypretty/theme.html#prettypretty.theme.current_translator
[`is_dark_theme`]: https://apparebit.github.io/prettypretty/python/prettypretty/darkmode.html#prettypretty.darkmode.is_dark_theme
[`rich`]: https://apparebit.github.io/prettypretty/python/prettypretty/style.html#prettypretty.style.rich
[`RichText`]: https://apparebit.github.io/prettypretty/python/prettypretty/style.html#prettypretty.style.RichText
[`Sampler.cap`]: https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.cap
[`Style.prepare`]: https://apparebit.github.io/prettypretty/python/prettypretty/style.html#prettypretty.style.Style.prepare
[`Terminal`]: https://apparebit.github.io/prettypretty/python/prettypretty/terminal.html#prettypretty.terminal.Terminal
[`Terminal.alternate_screen`]: https://apparebit.github.io/prettypretty/python/prettypretty/terminal.html#prettypretty.terminal.Terminal.alternate_screen
[`Terminal.batched_output`]: https://apparebit.github.io/prettypretty/python/prettypretty/terminal.html#prettypretty.terminal.Terminal.batched_output
[`Terminal.bracketed_paste`]: https://apparebit.github.io/prettypretty/python/prettypretty/terminal.html#prettypretty.terminal.Terminal.bracketed_paste
[`Terminal.fidelity`]: https://apparebit.github.io/prettypretty/python/prettypretty/terminal.html#prettypretty.terminal.Terminal.fidelity
[`Terminal.window_title`]: https://apparebit.github.io/prettypretty/python/prettypretty/terminal.html#prettypretty.terminal.Terminal.window_title
[`Translator.cap`]: https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.cap
2 changes: 1 addition & 1 deletion docs/src/links.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
[`Layer`]: https://apparebit.github.io/prettypretty/prettypretty/enum.Layer.html
[`OkVersion`]: https://apparebit.github.io/prettypretty/prettypretty/enum.OkVersion.html
[`OutOfBoundsError`]: https://apparebit.github.io/prettypretty/prettypretty/struct.OutOfBoundsError.html
[`Sampler`]: https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html
[`TerminalColor`]: https://apparebit.github.io/prettypretty/prettypretty/enum.TerminalColor.html
[`ThemeEntry`]: https://apparebit.github.io/prettypretty/prettypretty/enum.ThemeEntry.html
[`ThemeEntryIterator`]: https://apparebit.github.io/prettypretty/prettypretty/struct.ThemeEntryIterator.html
[`Translator`]: https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html
[`TrueColor`]: https://apparebit.github.io/prettypretty/prettypretty/struct.TrueColor.html
[`VGA_COLORS`]: https://apparebit.github.io/prettypretty/prettypretty/constant.VGA_COLORS.html
103 changes: 52 additions & 51 deletions docs/src/overview/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,38 +35,39 @@ default and ANSI colors.
## Translation Is Necessarily Stateful

Since the default and ANSI colors are abstract, translation to high-resolution
colors necessarily requires some form of lookup table, i.e., the so-called color
theme. Prettypretty relies on the same abstraction to store that table as well
as the derived state for translating high-resolution colors to terminal colors
again:
colors necessarily requires some form of lookup table, i.e., a color theme.
Prettypretty relies on the same abstraction to store that table as well as the
derived state for translating high-resolution colors to terminal colors again:

* [`Sampler`] provides the logic and state for translating between
* [`Translator`] provides the logic and state for translating between
terminal and high-resolution colors.

There is ample precedent for the use of color themes to provide concrete values
for abstract colors. Most terminal emulators feature robust support for the
[plethora of such themes](https://gogh-co.github.io/Gogh/) readily available on
the web. However, asking users to configure theme colors yet again most
certainly is the wrong approach. Luckily, ANSI escape codes include sequences
for querying a terminal for its current theme colors, making it possible to
automatically and transparently adjust to the runtime environment.
for abstract colors. In fact, most terminal emulators feature robust support for
the [plethora of such themes](https://gogh-co.github.io/Gogh/) readily available
on the web. However, asking users to configure theme colors after they already
configured their terminals decidedly is the wrong approach. Luckily, ANSI escape
codes include sequences for querying a terminal for its current theme colors,
making it possible to automatically and transparently adjust to the runtime
environment.


## The Fall From High-Resolution

Theme colors turn the translation of terminal to high-resolution colors into a
simple lookup. The difficulty of translation in the other direction, from
high-resolution to terminal colors, very much depends on the target colors:
simple lookup. The level of difficulty when translating in the other direction,
from high-resolution to terminal colors, very much depends on the target colors:


### 24-Bit Colors

In the best case, when the source color is in-gamut for sRGB and the target are
24-bit "true" colors, a loss of numeric resolution is the only concern. It
probably is imperceptible as well. However, if the source color is out of sRGB
gamut, even when still targeting 24-bit colors and, like [`Sampler`], using
gamut-mapping, the difference between source and target colors becomes clearly
noticeable. It only becomes more obvious when targeting 8-bit or ANSI colors.
gamut, even when still targeting 24-bit colors and, like [`Translator`], using
gamut-mapping, the difference between source and target colors may become
clearly noticeable. It only becomes more obvious when targeting 8-bit or ANSI
colors.


### 8-Bit Colors
Expand Down Expand Up @@ -97,7 +98,7 @@ time*. But because there are so few candidates, the closest matching color may
just violate basic human expectations about what is a match, e.g., that warm
tones remain warm, cold tones remain cold, light tones remain light, dark tones
remain dark, and last but not least color remains color.
[`Sampler::to_closest_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_closest_ansi)'s
[`Translator::to_closest_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_closest_ansi)'s
documentation provides an example that violates the latter expectation, with a
light orange tone turning into a light gray. That is jarring, especially in
context of other colors that are *not* mapped to gray.
Expand All @@ -107,91 +108,91 @@ leverages not only uses color pragmatics, i.e., the coordinates of theme colors,
but also color semantics, i.e., their intended appearance. In other words, the
algorithm leverages the very fact that ANSI colors are abstract colors to
improve the quality of matches. As implemented by
[`Sampler::to_ansi_hue_lightness`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_ansi_hue_lightness),
[`Translator::to_ansi_hue_lightness`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_ansi_hue_lightness),
the algorithm first uses hue in Oklrch to find a pair of regular and bright
colors and second uses lightness to pick the closer one. In my evaluation so
far, it is indeed more robust than brute force search. But it also won't work if
the theme colors themselves are inconsistent with theme semantics. Since that is
detectable,
[`Sampler::to_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_ansi)
the theme colors themselves are inconsistent with theme semantics. Since that
can be automatically checked,
[`Translator::to_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_ansi)
transparently picks the best possible method.


## Sampler Methods
## Translator Methods

Now that we understand the challenges and the algorithms for overcoming them, we
turn to [`Sampler`]'s interface. We group its method by task:
turn to [`Translator`]'s interface. We group its method by task:

1. [`Sampler::resolve`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.resolve)
1. [`Translator::resolve`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.resolve)
translates terminal colors to high-resolution colors. Thanks to the
`Into<TerminalColor>` trait, Rust code can invoke the method with an
instance of `u8`, [`DefaultColor`], [`AnsiColor`], [`EmbeddedRgb`],
[`GrayGradient`], [`TrueColor`], or [`TerminalColor`]. Thanks to a custom
PyO3 conversion function, Python code can do the exact same.
2. [`Sampler::to_closest_8bit`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_closest_8bit)
2. [`Translator::to_closest_8bit`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_closest_8bit)
and
[`Sampler::to_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_ansi)
[`Translator::to_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_ansi)
translate high-resolution colors to low-resolution terminal colors.
Prettypretty does not support conversion to the default colors and
high-resolution colors can be directly converted to true colors, without
requiring mediation through [`Sampler`].
requiring mediation through [`Translator`].

The
[`Sampler::supports_hue_lightness`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.supports_hue_lightness),
[`Sampler::to_ansi_hue_lightness`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_ansi_hue_lightness),
[`Sampler::to_closest_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_closest_ansi),
[`Translator::supports_hue_lightness`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.supports_hue_lightness),
[`Translator::to_ansi_hue_lightness`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_ansi_hue_lightness),
[`Translator::to_closest_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_closest_ansi),
and
[`Sampler::to_ansi_rgb`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_ansi_rgb)
[`Translator::to_ansi_rgb`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_ansi_rgb)
methods provide direct access to individual algorithms for converting to
ANSI colors. For instance, I use these methods for comparing the
effectiveness of different approaches. But your code is probably better off
using
[`Sampler::to_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.to_ansi),
[`Translator::to_ansi`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.to_ansi),
which automatically picks `to_ansi_hue_lightness` or `to_closest_ansi`. In
any case, I strongly recommend avoiding `to_ansi_rgb`. It only exists to
evaluate the approach taken by the popular JavaScript library
[Chalk](https://github.com/chalk/chalk) and reliably produces subpar
results. Ironically, Chalk's tagline is "Terminal string styling done
right."
3. [`Sampler::cap`](https://apparebit.github.io/prettypretty/prettypretty/struct.Sampler.html#method.cap)
3. [`Translator::cap`](https://apparebit.github.io/prettypretty/prettypretty/struct.Translator.html#method.cap)
tanslates terminal colors to terminal colors. Under the hood, it may very
well translate a terminal color to a high-resolution color and then match
against that color to produce a terminal color again. This is the method to
use for adjusting terminal colors to the runtime environment and user
preferences, which can be concisely expressed by the [`Fidelity`] level.
4. [`Sampler::is_dark_theme`]() determines whether the color theme used by this
sampler instance is a dark theme.
against that color to produce a terminal color again. Use this method to
adjust terminal colors to the runtime environment and user preferences,
which can be concisely expressed by a [`Fidelity`] level.
4. [`Translator::is_dark_theme`]() determines whether the color theme used by this
translator instance is a dark theme.


[`Sampler`] eagerly creates the necessary tables with colors for brute force and
[`Translator`] eagerly creates the necessary tables with colors for brute force and
hue-lightness search in the constructor. Altogether, an instance of this struct
owns 306 colors, which take up 7,160 bytes on macOS. As long as the terminal
color theme doesn't change, a sampler need not be regenerated. That also means
color theme doesn't change, a translator need not be regenerated. That also means
that it can be used concurrently without locking—as long as threads have their
own references.


## Sampler Samples
## Translator

The example code below illustrates the use of each major entry point besides
`to_closest_8bit`, which isn't that different from `to_ansi`:

```rust
# extern crate prettypretty;
# use prettypretty::{AnsiColor, Color, ColorFormatError, Sampler, VGA_COLORS};
# use prettypretty::{AnsiColor, Color, ColorFormatError, Translator, VGA_COLORS};
# use prettypretty::{OkVersion, TrueColor, Fidelity, EmbeddedRgb};
# use std::str::FromStr;
let red = &VGA_COLORS[AnsiColor::BrightRed as usize + 2];
assert_eq!(red, &Color::srgb(1.0, 0.333333333333333, 0.333333333333333));

let sampler = Sampler::new(OkVersion::Revised, VGA_COLORS.clone());
let also_red = &sampler.resolve(AnsiColor::BrightRed);
let translator = Translator::new(OkVersion::Revised, VGA_COLORS.clone());
let also_red = &translator.resolve(AnsiColor::BrightRed);
assert_eq!(red, also_red);

let black = sampler.to_ansi(&Color::srgb(0.15, 0.15, 0.15));
let black = translator.to_ansi(&Color::srgb(0.15, 0.15, 0.15));
assert_eq!(black, AnsiColor::Black);

let maroon = sampler.cap(TrueColor::new(148, 23, 81), Fidelity::EightBit);
let maroon = translator.cap(TrueColor::new(148, 23, 81), Fidelity::EightBit);
assert_eq!(maroon, Some(EmbeddedRgb::new(2,0,1).unwrap().into()));
# Ok::<(), ColorFormatError>(())
```
Expand All @@ -206,19 +207,19 @@ assert_eq!(maroon, Some(EmbeddedRgb::new(2,0,1).unwrap().into()));
The Python version is a close match:

```python
~from prettypretty.color import AnsiColor, Color, OkVersion, Sampler, Fidelity
~from prettypretty.color import AnsiColor, Color, OkVersion, Translator, Fidelity
~from prettypretty.theme import VGA
red = VGA[AnsiColor.BrightRed.to_8bit() + 2]
assert red == Color.srgb(1.0, 0.333333333333333, 0.333333333333333)

sampler = Sampler(OkVersion.Revised, VGA)
also_red = sampler.resolve(AnsiColor.BrightRed)
translator = Translator(OkVersion.Revised, VGA)
also_red = translator.resolve(AnsiColor.BrightRed)
assert red == also_red

black = sampler.to_ansi(Color.srgb(0.15, 0.15, 0.15))
black = translator.to_ansi(Color.srgb(0.15, 0.15, 0.15))
assert black == AnsiColor.Black

maroon = sampler.cap(TrueColor(148, 23, 81), Fidelity.EightBit)
maroon = translator.cap(TrueColor(148, 23, 81), Fidelity.EightBit)
assert maroon == TerminalColor.Rgb6(EmbeddedRgb(2, 0, 1))
```
<div class=color-swatch>
Expand Down
8 changes: 4 additions & 4 deletions docs/src/overview/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ In more detail:
lossless and partial conversions between color representations
including, for example, conversion from EmbeddedRgb to `u8` index values
as well true, terminal, and high-resolution colors.
* [`Sampler`] performs the more difficult translation from ANSI to
high-resolution colors, from high-resolution to 8-bit or ANSI colors,
and the downgrading of terminal colors based on terminal capabilities
and user preferences.
* [`Translator`] performs the more difficult translation from ANSI to
high-resolution colors, from high-resolution to 8-bit or ANSI colors, and
the downgrading of terminal colors based on terminal capabilities and user
preferences.


{{#include ../links.md}}
20 changes: 8 additions & 12 deletions prettypretty/color.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -356,38 +356,34 @@ class ThemeEntryIterator:
def __next__(self) -> None | ThemeEntry: ...


class Sampler:
"""A color sampler for translating between terminal and high-resolution colors."""
class Translator:
"""A class for translating between terminal and high-resolution colors."""
@staticmethod
def theme_entries() -> Iterator[ThemeEntry]: ...
def __new__(cls, version: OkVersion, theme_colors: Sequence[Color]) -> Self: ...
def __repr__(self) -> str: ...

"""Interrogate color theme."""
# Interrogate the color theme
def is_dark_theme(self) -> bool: ...

"""Translate to high-resolution colors."""
# Translate terminal to high-resolution colors
def resolve(
self,
color: TerminalColor|DefaultColor|AnsiColor|EmbeddedRgb|GrayGradient|TrueColor|int,
) -> Color: ...

"""Translate to ANSI colors."""
def to_ansi(self, color: Color) -> Color:
"""
Translate high-resolution to ANSI colors using the best available
algorithm.
"""
# Translate high-resolution to ANSI colors
def to_ansi(self, color: Color) -> Color: ...
def supports_hue_lightness(self) -> bool: ...
def to_ansi_hue_lightness(self, color: Color) -> None | AnsiColor: ...
def to_closest_ansi(self, color: Color) -> AnsiColor: ...
def to_ansi_rgb(self, color: Color) -> AnsiColor: ...

"""Translate to 8-bit colors."""
# Translate high-resolution to 8-bit colors
def to_closest_8bit(self, color: Color) -> TerminalColor: ...
def to_closest_8bit_with_ansi(self, color: Color) -> TerminalColor: ...

"""Cap terminal colors."""
# Cap terminal colors
def cap(
self,
color: TerminalColor|DefaultColor|AnsiColor|EmbeddedRgb|GrayGradient|TrueColor|int,
Expand Down
Loading

0 comments on commit 2af5c80

Please sign in to comment.