// The console protocol (rich/abc.py, rich/protocol.py, parts of
// rich/console.py) and small shared types.
///|
/// Something that may be rendered by a `Console`: upstream's
/// `__rich_console__` (required) and `__rich_measure__` (optional).
///
/// `String` and `Text` implement it; any other type can implement
/// `rich_console`, returning segments and/or nested renderables.
pub(open) trait Renderable {
/// Render to segments and/or nested renderables.
fn rich_console(Self, Console, ConsoleOptions) -> Array[RenderItem] raise
/// Measure the minimum and maximum width. `None` means "not measurable",
/// which upstream treats as `Measurement(0, options.max_width)`.
fn rich_measure(Self, Console, ConsoleOptions) -> Measurement? raise = _
/// Whether this is a plain string or a `Text` (upstream `isinstance`
/// checks). Only `String` and `Text` should override this.
fn text_kind(Self) -> TextKind = _
/// Vertical alignment requested by the renderable (upstream reads a
/// `vertical` attribute from table cells, e.g. on `Align`).
fn vertical_align(Self) -> VerticalAlignMethod? = _
}
///|
impl Renderable with fn vertical_align(_self) {
None
}
///|
impl Renderable with fn rich_measure(_self, _console, _options) {
None
}
///|
impl Renderable with fn text_kind(_self) {
NotText
}
///|
/// Result of `Renderable::text_kind`.
pub(all) enum TextKind {
NotText
Str(String)
Text(Text)
}
///|
/// An item produced by `Renderable::rich_console`: a segment, or another
/// renderable to be rendered in turn.
pub(all) enum RenderItem {
Seg(Segment)
Child(&Renderable)
}
///|
/// Wrap segments as render items.
pub fn segments_to_items(segments : Array[Segment]) -> Array[RenderItem] {
segments.map(s => Seg(s))
}
///|
pub impl Renderable for String with fn rich_console(self, console, options) {
let text = console.render_str(
self,
highlight=options.highlight,
markup=options.markup,
)
text.rich_console(console, options)
}
///|
pub impl Renderable for String with fn rich_measure(self, console, options) {
let text = console.render_str(
self,
markup=options.markup,
highlight=Some(false),
)
text.rich_measure(console, options)
}
///|
pub impl Renderable for String with fn text_kind(self) {
Str(self)
}
///|
/// Justify methods.
pub(all) enum JustifyMethod {
Default
Left
Center
Right
Full
} derive(Eq, Hash, Debug)
///|
pub fn JustifyMethod::to_string(self : JustifyMethod) -> String {
match self {
Default => "default"
Left => "left"
Center => "center"
Right => "right"
Full => "full"
}
}
///|
/// Parse a justify method name (`"default"`, `"left"`, ...).
pub fn JustifyMethod::parse(s : String) -> JustifyMethod? {
match s {
"default" => Some(Default)
"left" => Some(Left)
"center" => Some(Center)
"right" => Some(Right)
"full" => Some(Full)
_ => None
}
}
///|
/// Overflow methods.
pub(all) enum OverflowMethod {
Fold
Crop
Ellipsis
Ignore
} derive(Eq, Hash, Debug)
///|
pub fn OverflowMethod::to_string(self : OverflowMethod) -> String {
match self {
Fold => "fold"
Crop => "crop"
Ellipsis => "ellipsis"
Ignore => "ignore"
}
}
///|
/// Parse an overflow method name.
pub fn OverflowMethod::parse(s : String) -> OverflowMethod? {
match s {
"fold" => Some(Fold)
"crop" => Some(Crop)
"ellipsis" => Some(Ellipsis)
"ignore" => Some(Ignore)
_ => None
}
}
///|
/// Horizontal alignment.
pub(all) enum AlignMethod {
Left
Center
Right
} derive(Eq, Hash, Debug)
///|
pub fn AlignMethod::to_string(self : AlignMethod) -> String {
match self {
Left => "left"
Center => "center"
Right => "right"
}
}
///|
/// Vertical alignment.
pub(all) enum VerticalAlignMethod {
Top
Middle
Bottom
} derive(Eq, Hash, Debug)
///|
pub fn VerticalAlignMethod::to_string(self : VerticalAlignMethod) -> String {
match self {
Top => "top"
Middle => "middle"
Bottom => "bottom"
}
}
///|
/// A style definition (looked up in the theme, then parsed, at render
/// time) or a `Style` (upstream `StyleType = Union[str, Style]`).
pub(all) enum StyleType {
Def(String)
Style(Style)
} derive(Eq, Hash)
///|
/// Upstream `bool(style)`: a non-empty definition or a non-null style.
pub fn StyleType::to_bool(self : StyleType) -> Bool {
match self {
Def(s) => s != ""
Style(s) => s.to_bool()
}
}
///|
/// Upstream `repr(style)` for a str or Style.
pub fn StyleType::repr(self : StyleType) -> String {
match self {
Def(s) => py_repr(s)
Style(s) => s.repr()
}
}
///|
/// Upstream `str(style)`.
pub impl Show for StyleType with fn output(self, logger) {
match self {
Def(s) => logger.write_string(s)
Style(s) => logger.write_string(s.to_definition())
}
}
///|
pub impl Debug for StyleType with fn to_repr(self) {
@debug.Repr(self.repr())
}
///|
/// Types accepted where upstream takes `Union[str, Style]`.
pub(open) trait IntoStyle {
fn into_style_type(Self) -> StyleType
}
///|
pub impl IntoStyle for String with fn into_style_type(self) {
Def(self)
}
///|
pub impl IntoStyle for Style with fn into_style_type(self) {
StyleType::Style(self)
}
///|
pub impl IntoStyle for StyleType with fn into_style_type(self) {
self
}
///|
fn style_or_empty(style : &IntoStyle?) -> StyleType {
match style {
Some(s) => s.into_style_type()
None => Def("")
}
}
///|
fn style_opt(style : &IntoStyle?) -> StyleType? {
match style {
Some(s) => Some(s.into_style_type())
None => None
}
}
///|
/// A plain string or a `Text` instance (upstream `TextType`).
pub(all) enum TextType {
Str(String)
Text(Text)
}
///|
/// Types accepted where upstream takes `Union[str, Text]`.
pub(open) trait IntoText {
fn into_text_type(Self) -> TextType
}
///|
pub impl IntoText for String with fn into_text_type(self) {
Str(self)
}
///|
pub impl IntoText for Text with fn into_text_type(self) {
TextType::Text(self)
}
///|
pub impl IntoText for TextType with fn into_text_type(self) {
self
}
///|
/// Upstream `bool(text)`.
pub fn TextType::to_bool(self : TextType) -> Bool {
match self {
Str(s) => s != ""
Text(t) => t.length() > 0
}
}
///|
/// Upstream `str(text)`.
pub fn TextType::plain(self : TextType) -> String {
match self {
Str(s) => s
Text(t) => t.plain()
}
}
///|
pub impl Renderable for TextType with fn rich_console(self, console, options) {
match self {
Str(s) => Renderable::rich_console(s, console, options)
Text(t) => Renderable::rich_console(t, console, options)
}
}
///|
pub impl Renderable for TextType with fn rich_measure(self, console, options) {
match self {
Str(s) => Renderable::rich_measure(s, console, options)
Text(t) => Renderable::rich_measure(t, console, options)
}
}
///|
pub impl Renderable for TextType with fn text_kind(self) {
match self {
Str(s) => Str(s)
Text(t) => Text(t)
}
}
///|
/// Stores the minimum and maximum widths (in cells) required to render an
/// object.
pub(all) struct Measurement {
minimum : Int
maximum : Int
} derive(Eq, Debug)
///|
pub fn Measurement::new(minimum : Int, maximum : Int) -> Measurement {
{ minimum, maximum, }
}
///|
pub impl Show for Measurement with fn output(self, logger) {
logger.write_string(
"Measurement(minimum=\{self.minimum}, maximum=\{self.maximum})",
)
}
///|
/// Difference between maximum and minimum.
pub fn Measurement::span(self : Measurement) -> Int {
self.maximum - self.minimum
}
///|
/// Ensure that minimum <= maximum and minimum >= 0.
pub fn Measurement::normalize(self : Measurement) -> Measurement {
let { minimum, maximum, } = self
let minimum = @cmp.minimum(@cmp.maximum(0, minimum), maximum)
{
minimum: @cmp.maximum(0, minimum),
maximum: @cmp.maximum(0, @cmp.maximum(minimum, maximum)),
}
}
///|
/// Get a measurement where the widths are <= width.
pub fn Measurement::with_maximum(
self : Measurement,
width : Int,
) -> Measurement {
{
minimum: @cmp.minimum(self.minimum, width),
maximum: @cmp.minimum(self.maximum, width),
}
}
///|
/// Get a measurement where the widths are >= width.
pub fn Measurement::with_minimum(
self : Measurement,
width : Int,
) -> Measurement {
let width = @cmp.maximum(0, width)
{
minimum: @cmp.maximum(self.minimum, width),
maximum: @cmp.maximum(self.maximum, width),
}
}
///|
/// Clamp a measurement within the specified range.
pub fn Measurement::clamp(
self : Measurement,
min_width? : Int,
max_width? : Int,
) -> Measurement {
let mut m = self
if min_width is Some(w) {
m = m.with_minimum(w)
}
if max_width is Some(w) {
m = m.with_maximum(w)
}
m
}
///|
/// Get a measurement for a renderable.
pub fn Measurement::get(
console : Console,
options : ConsoleOptions,
renderable : &Renderable,
) -> Measurement raise {
let max_width = options.max_width
if max_width < 1 {
return { minimum: 0, maximum: 0, }
}
match renderable.rich_measure(console, options) {
Some(m) => {
// `m.normalize().with_maximum(max_width).normalize()` on plain ints
let minimum = @cmp.minimum(@cmp.maximum(0, m.minimum), m.maximum)
let maximum = @cmp.maximum(0, @cmp.maximum(minimum, m.maximum))
let minimum = @cmp.minimum(@cmp.maximum(0, minimum), max_width)
let maximum = @cmp.minimum(maximum, max_width)
if maximum < 1 {
return { minimum: 0, maximum: 0, }
}
let minimum = @cmp.minimum(@cmp.maximum(0, minimum), maximum)
let maximum = @cmp.maximum(0, @cmp.maximum(minimum, maximum))
let minimum = @cmp.maximum(0, minimum)
if minimum == m.minimum && maximum == m.maximum {
m
} else {
{ minimum, maximum, }
}
}
None => { minimum: 0, maximum: max_width, }
}
}
///|
/// Get a measurement that would fit a number of renderables.
pub fn measure_renderables(
console : Console,
options : ConsoleOptions,
renderables : ArrayView[&Renderable],
) -> Measurement raise {
if renderables.length() == 0 {
return { minimum: 0, maximum: 0, }
}
let measurements = []
for r in renderables {
measurements.push(Measurement::get(console, options, r))
}
// max() with a key returns the first maximal item
let mut min_m = measurements[0]
let mut max_m = measurements[0]
for m in measurements {
if m.minimum > min_m.minimum {
min_m = m
}
if m.maximum > max_m.maximum {
max_m = m
}
}
{ minimum: min_m.minimum, maximum: max_m.maximum, }
}
///|
/// Size of the terminal.
pub(all) struct ConsoleDimensions {
width : Int
height : Int
} derive(Eq, Debug)
///|
/// Options for `Renderable::rich_console`.
pub(all) struct ConsoleOptions {
/// Size of console.
mut size : ConsoleDimensions
/// Flag for legacy windows.
mut legacy_windows : Bool
/// Minimum width of renderable.
mut min_width : Int
/// Maximum width of renderable.
mut max_width : Int
/// True if the target is a terminal.
mut is_terminal : Bool
/// Encoding of terminal.
mut encoding : String
/// Height of container (starts as terminal).
mut max_height : Int
/// Justify value override for renderable.
mut justify : JustifyMethod?
/// Overflow value override for renderable.
mut overflow : OverflowMethod?
/// Disable wrapping for text.
mut no_wrap : Bool?
/// Highlight override for render_str.
mut highlight : Bool?
/// Enable markup when rendering strings.
mut markup : Bool?
/// Height available, if fixed.
mut height : Int?
}
///|
/// Check if renderables should use ascii only.
pub fn ConsoleOptions::ascii_only(self : ConsoleOptions) -> Bool {
!self.encoding.has_prefix("utf")
}
///|
/// Return a copy of the options.
pub fn ConsoleOptions::copy(self : ConsoleOptions) -> ConsoleOptions {
{ ..self, }
}
///|
/// Update values, return a copy. Omitted arguments are left unchanged
/// (upstream `NO_CHANGE`); `Some(None)`-style values reset an option.
pub fn ConsoleOptions::update(
self : ConsoleOptions,
width? : Int,
min_width? : Int,
max_width? : Int,
justify? : JustifyMethod?,
overflow? : OverflowMethod?,
no_wrap? : Bool?,
highlight? : Bool?,
markup? : Bool?,
height? : Int?,
) -> ConsoleOptions {
let options = self.copy()
if width is Some(w) {
options.min_width = @cmp.maximum(0, w)
options.max_width = @cmp.maximum(0, w)
}
if min_width is Some(w) {
options.min_width = w
}
if max_width is Some(w) {
options.max_width = w
}
if justify is Some(j) {
options.justify = j
}
if overflow is Some(o) {
options.overflow = o
}
if no_wrap is Some(n) {
options.no_wrap = n
}
if highlight is Some(h) {
options.highlight = h
}
if markup is Some(m) {
options.markup = m
}
if height is Some(h) {
if h is Some(h) {
options.max_height = h
}
options.height = match h {
None => None
Some(h) => Some(@cmp.maximum(0, h))
}
}
options
}
///|
/// Update just the width (sets both min_width and max_width).
pub fn ConsoleOptions::update_width(
self : ConsoleOptions,
width : Int,
) -> ConsoleOptions {
let options = self.copy()
options.min_width = @cmp.maximum(0, width)
options.max_width = @cmp.maximum(0, width)
options
}
///|
/// Update the height.
pub fn ConsoleOptions::update_height(
self : ConsoleOptions,
height : Int,
) -> ConsoleOptions {
let options = self.copy()
options.max_height = height
options.height = Some(height)
options
}
///|
/// Return a copy of the options with height set to `None`.
pub fn ConsoleOptions::reset_height(self : ConsoleOptions) -> ConsoleOptions {
let options = self.copy()
options.height = None
options
}
///|
/// Update the width and height.
pub fn ConsoleOptions::update_dimensions(
self : ConsoleOptions,
width : Int,
height : Int,
) -> ConsoleOptions {
let options = self.copy()
options.min_width = @cmp.maximum(0, width)
options.max_width = @cmp.maximum(0, width)
options.height = Some(height)
options.max_height = height
options
}
///|
/// Upstream `_pick.pick_bool`: the first non-`None` value, or `false`.
pub fn pick_bool(values : Array[Bool?]) -> Bool {
for v in values {
if v is Some(b) {
return b
}
}
false
}
///|
/// Pair each item with a flag that is true for the last item
/// (upstream `_loop.loop_last`).
pub fn[T] loop_last(xs : ArrayView[T]) -> Array[(Bool, T)] {
let n = xs.length()
let out = []
for i, x in xs {
out.push((i == n - 1, x))
}
out
}
///|
/// Pair each item with a flag that is true for the first item
/// (upstream `_loop.loop_first`).
pub fn[T] loop_first(xs : ArrayView[T]) -> Array[(Bool, T)] {
let out = []
for i, x in xs {
out.push((i == 0, x))
}
out
}
///|
/// Pair each item with flags for first and last
/// (upstream `_loop.loop_first_last`).
pub fn[T] loop_first_last(xs : ArrayView[T]) -> Array[(Bool, Bool, T)] {
let n = xs.length()
let out = []
for i, x in xs {
out.push((i == 0, i == n - 1, x))
}
out
}