Skip to content

Conversation

@tecosaur
Copy link
Member

@tecosaur tecosaur commented Jan 4, 2026

These changes give Faces primacy, replacing the use of Symbols to name faces. This change is capitalised on to address several long-standing pain points with face/style management in StyledStrings.

The supplants #99 and #110, and perhaps also #100.

This closes #87, and should also help with #61.

Motivation

The current system has several issues:

  • Piracy with dispatch on AnnotatedString printing
  • No way for other packages to define annotation types with custom display
  • Any value type in annotations
  • No namespacing for faces (all faces in one global dictionary)
  • Long package prefixes required (e.g. REPL_History_search_unselected)
  • Silent failures with misspelled face names (styled"{bluue:text}" renders unstyled)
  • Mixed use of Faces and Symbols throughout the API

Solution

Parameterised annotation values: AnnotatedString{S} becomes AnnotatedString{S, V}, allowing dispatch on the annotation value type without piracy (see: JuliaLang/julia#60527).

Face primacy: Named faces are now referenced by Face objects rather than Symbols. The face"" macro provides convenient access (face"red" instead of :red). Faces are hybrid-interned: created at compile time, registered at runtime.

Palettes: Three new macros for module-scoped face definitions:

  • @defpalette! - define a set of named faces
  • @registerpalette! - register faces at runtime (in __init__)
  • @usepalettes! - import faces from other modules

Face remapping: remapfaces substitutes faces at string construction time, complementing the existing withfaces which operates at display time.

Example

# Before
const FACES = (:MyPkg_header => Face(foreground=:blue, weight=:bold),)
__init__() = foreach(addface!, FACES)
styled"{MyPkg_header:Title}"

# After  
@defpalette! begin
    header = Face(foreground = blue, weight = :bold)
end
__init__() = @registerpalette!
styled"{header:Title}"

Breaking changes

None. The old Symbol-based API remains supported with deprecation warnings.

In preparation for performing a form of interning with `Face`s to make
"faces refer to faces" instead of using symbols, we want to end up with
a single pointer that's passed around for each Face. This can be done by
making `Face` a thin const mutable struct wrapper around an underlying
face type (`_Face`).

We also want to differentiate between "no value provided" and "attribute
explicitly unset". For this purpose we introduce special values that act
as hard and soft nothing-likes. This could be done with two singletons,
and expanding each field to be a `Union{hardnothing, softnothing,
<original type>}` but with the current compiler this produces worse
memory layout/assembly than if we introduce special values and go to the
effort of handling them appropriately, and so that's what I've done.

If we can replace this with union fields and two singletons and not have
to bother with the rest, with the compiler doing just as well as the
manually written code, I'll be overjoyed and happily delete this work.
@tecosaur tecosaur force-pushed the face-primacy branch 11 times, most recently from 09158f8 to 1340ce0 Compare January 7, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

withfaces doesn't apply to constructed StyledStrings

1 participant