// Port of rich/segment.py.
///|
/// Non-printable control codes which typically translate to ANSI codes.
pub(all) enum ControlType {
Bell
CarriageReturn
Home
Clear
ShowCursor
HideCursor
EnableAltScreen
DisableAltScreen
CursorUp
CursorDown
CursorForward
CursorBackward
CursorMoveToColumn
CursorMoveTo
EraseInLine
SetWindowTitle
} derive(Eq, Hash, Debug)
///|
pub fn ControlType::to_int(self : ControlType) -> Int {
match self {
Bell => 1
CarriageReturn => 2
Home => 3
Clear => 4
ShowCursor => 5
HideCursor => 6
EnableAltScreen => 7
DisableAltScreen => 8
CursorUp => 9
CursorDown => 10
CursorForward => 11
CursorBackward => 12
CursorMoveToColumn => 13
CursorMoveTo => 14
EraseInLine => 15
SetWindowTitle => 16
}
}
///|
/// A control code with its parameters.
pub(all) enum ControlCode {
Code(ControlType)
CodeInt(ControlType, Int)
CodeStr(ControlType, String)
CodeXY(ControlType, Int, Int)
} derive(Eq, Hash, Debug)
///|
/// A piece of text with associated style. Segments are produced by the
/// Console render process and are ultimately converted in to strings to be
/// written to the terminal.
pub(all) struct Segment {
text : String
style : Style?
control : Array[ControlCode]?
} derive(Eq)
///|
pub fn Segment::new(
text : String,
style? : Style,
control? : Array[ControlCode],
) -> Segment {
{ text, style, control, }
}
///|
/// Upstream `repr(segment)`.
pub fn Segment::repr(self : Segment) -> String {
let parts = [py_repr(self.text)]
match self.control {
None => if self.style is Some(s) { parts.push(s.repr()) }
Some(control) => {
parts.push(
match self.style {
Some(s) => s.repr()
None => "None"
},
)
parts.push(control_codes_repr(control))
}
}
"Segment(" + parts.join(", ") + ")"
}
///|
fn control_codes_repr(codes : Array[ControlCode]) -> String {
let items = codes.map(code => {
match code {
Code(t) => "(,)"
CodeInt(t, n) =>
"(, \{n})"
CodeStr(t, s) =>
"(, \{py_repr(s)})"
CodeXY(t, x, y) =>
"(, \{x}, \{y})"
}
})
"[" + items.join(", ") + "]"
}
///|
fn control_type_name(t : ControlType) -> String {
match t {
Bell => "BELL"
CarriageReturn => "CARRIAGE_RETURN"
Home => "HOME"
Clear => "CLEAR"
ShowCursor => "SHOW_CURSOR"
HideCursor => "HIDE_CURSOR"
EnableAltScreen => "ENABLE_ALT_SCREEN"
DisableAltScreen => "DISABLE_ALT_SCREEN"
CursorUp => "CURSOR_UP"
CursorDown => "CURSOR_DOWN"
CursorForward => "CURSOR_FORWARD"
CursorBackward => "CURSOR_BACKWARD"
CursorMoveToColumn => "CURSOR_MOVE_TO_COLUMN"
CursorMoveTo => "CURSOR_MOVE_TO"
EraseInLine => "ERASE_IN_LINE"
SetWindowTitle => "SET_WINDOW_TITLE"
}
}
///|
pub impl Show for Segment with fn output(self, logger) {
logger.write_string(self.repr())
}
///|
pub impl Debug for Segment with fn to_repr(self) {
@debug.Repr(self.repr())
}
///|
/// The number of terminal cells required to display the text (0 for
/// control segments).
pub fn Segment::cell_length(self : Segment) -> Int {
if self.has_codes() {
0
} else {
@cells.cell_len(self.text)
}
}
///|
/// Check if the segment contains control codes.
pub fn Segment::is_control(self : Segment) -> Bool {
self.control is Some(_)
}
///|
/// Upstream `bool(segment.control)`: the segment has at least one control
/// code. Most of the segment helpers test this (rather than `is_control`).
pub fn Segment::has_codes(self : Segment) -> Bool {
match self.control {
Some(c) => !c.is_empty()
None => false
}
}
///|
/// Upstream `bool(segment)`: the segment contains text.
pub fn Segment::to_bool(self : Segment) -> Bool {
self.text != ""
}
///|
/// Split a segment in to two at a given cell position. If the cut point
/// falls in the middle of a 2-cell wide character then it is replaced by two
/// spaces, to preserve the display width.
pub fn Segment::split_cells(self : Segment, cut : Int) -> (Segment, Segment) {
let { text, style, control, } = self
if @cells.is_single_cell_widths(text) {
if cut >= text.length() {
return (self, { text: "", style, control, })
}
return (
{ text: text.view(end_offset=cut).to_owned(), style, control, },
{ text: text.view(start_offset=cut).to_owned(), style, control, },
)
}
split_cells_slow(self, cut)
}
///|
fn split_cells_slow(segment : Segment, cut : Int) -> (Segment, Segment) {
let { text, style, control, } = segment
let cell_length = segment.cell_length()
if cut >= cell_length {
return (segment, { text: "", style, control, })
}
// Work in code points as upstream does.
let chars = text.to_array()
let n = chars.length()
fn join(a : Int, b : Int) -> String {
String::from_array(chars[a:b])
}
let mut pos = (cut.to_double() / cell_length.to_double() * n.to_double()).to_int()
while true {
let before = join(0, pos)
let cell_pos = @cells.cell_len(before)
let out_by = cell_pos - cut
if out_by == 0 {
return (
{ text: before, style, control, },
{ text: join(pos, n), style, control, },
)
}
if out_by == -1 &&
pos < n &&
@cells.get_character_cell_size(chars[pos]) == 2 {
return (
{ text: join(0, pos) + " ", style, control, },
{ text: " " + join(pos + 1, n), style, control, },
)
}
if out_by == 1 &&
pos > 0 &&
@cells.get_character_cell_size(chars[pos - 1]) == 2 {
return (
{ text: join(0, pos - 1) + " ", style, control, },
{ text: " " + join(pos, n), style, control, },
)
}
if cell_pos < cut {
pos += 1
} else {
pos -= 1
}
}
panic()
}
///|
/// Make a new line segment.
pub fn Segment::line() -> Segment {
{ text: "\n", style: None, control: None, }
}
///|
/// Apply style(s) to segments: each style is replaced by
/// `style + segment.style + post_style`.
pub fn Segment::apply_style(
segments : Array[Segment],
style? : Style,
post_style? : Style,
) -> Array[Segment] {
let mut result = segments
if style is Some(style) && style.to_bool() {
result = result.map(seg => {
text: seg.text,
style: if seg.has_codes() {
None
} else {
Some(style.add_opt(seg.style))
},
control: seg.control,
})
}
if post_style is Some(post_style) && post_style.to_bool() {
result = result.map(seg => {
text: seg.text,
style: if seg.has_codes() {
None
} else {
match seg.style {
Some(s) if s.to_bool() => Some(s + post_style)
_ => Some(post_style)
}
},
control: seg.control,
})
}
result
}
///|
/// Filter segments by their `is_control` attribute.
pub fn Segment::filter_control(
segments : Array[Segment],
is_control? : Bool = false,
) -> Array[Segment] {
segments.filter(s => s.has_codes() == is_control)
}
///|
/// Split a sequence of segments in to a list of lines.
pub fn Segment::split_lines(segments : Array[Segment]) -> Array[Array[Segment]] {
Segment::split_lines_terminator(segments).map(x => x.0)
}
///|
/// Split segments in to lines, with a flag that indicates whether the line
/// was terminated by a new line.
pub fn Segment::split_lines_terminator(
segments : Array[Segment],
) -> Array[(Array[Segment], Bool)] {
let lines = []
let mut line = []
for segment in segments {
let text = segment.text
if !segment.has_codes() && text.contains_char('\n') {
let n = text.length()
let mut pos = 0
while pos < n {
let nl = match text.view(start_offset=pos).find("\n") {
Some(i) => pos + i
None => n
}
if nl > pos {
line.push({
text: text.unsafe_substring(start=pos, end=nl),
style: segment.style,
control: None,
})
}
if nl < n {
lines.push((line, true))
line = []
}
pos = nl + 1
}
} else {
line.push(segment)
}
}
if !line.is_empty() {
lines.push((line, false))
}
lines
}
///|
/// Python `str.partition(sep)`.
fn partition(s : String, sep : String) -> (String, Bool, String) {
match s.find(sep) {
Some(i) =>
(
s.view(end_offset=i).to_owned(),
true,
s.view(start_offset=i + sep.length()).to_owned(),
)
None => (s, false, "")
}
}
///|
/// Split segments in to lines, and crop lines greater than a given length.
pub fn Segment::split_and_crop_lines(
segments : Array[Segment],
length : Int,
style? : Style,
pad? : Bool = true,
include_new_lines? : Bool = true,
) -> Array[Array[Segment]] {
let lines = []
let mut line = []
let new_line_segment = Segment::line()
for segment in segments {
let text = segment.text
if !segment.has_codes() && text.contains_char('\n') {
// walk the newlines by index (slicing off the rest each time would be
// quadratic in the number of lines)
let n = text.length()
let mut pos = 0
while pos < n {
let nl = match text.view(start_offset=pos).find("\n") {
Some(i) => pos + i
None => n
}
if nl > pos {
let piece = if pos == 0 && nl == n {
text
} else {
text.unsafe_substring(start=pos, end=nl)
}
line.push({ text: piece, style: segment.style, control: None, })
}
if nl < n {
let cropped_line = adjust_line_length_owned(line, length, style, pad)
if include_new_lines {
cropped_line.push(new_line_segment)
}
lines.push(cropped_line)
line = []
}
pos = nl + 1
}
} else {
line.push(segment)
}
}
if !line.is_empty() {
lines.push(adjust_line_length_owned(line, length, style, pad))
}
lines
}
///|
/// Adjust a line to a given width (cropping or padding as required).
pub fn Segment::adjust_line_length(
line : Array[Segment],
length : Int,
style? : Style,
pad? : Bool = true,
) -> Array[Segment] {
adjust_line_length_owned(line.copy(), length, style, pad)
}
///|
/// `adjust_line_length` for a line the caller hands over: it is reused for
/// the result instead of being copied.
fn adjust_line_length_owned(
line : Array[Segment],
length : Int,
style : Style?,
pad : Bool,
) -> Array[Segment] {
let mut line_length = 0
for segment in line {
line_length += segment.cell_length()
}
if line_length < length {
if pad {
line.push({
text: space_string(length - line_length),
style,
control: None,
})
}
line
} else if line_length > length {
let new_line = []
line_length = 0
for segment in line {
let segment_length = segment.cell_length()
if line_length + segment_length < length || segment.has_codes() {
new_line.push(segment)
line_length += segment_length
} else {
let text = @cells.set_cell_size(segment.text, length - line_length)
new_line.push({ text, style: segment.style, control: None, })
break
}
}
new_line
} else {
line
}
}
///|
/// Get the length of a line of segments (assumes no new lines).
pub fn Segment::get_line_length(line : ArrayView[Segment]) -> Int {
let mut total = 0
for segment in line {
if !segment.has_codes() {
total += @cells.cell_len(segment.text)
}
}
total
}
///|
/// Get the shape (enclosing rectangle) of a list of lines.
pub fn Segment::get_shape(lines : Array[Array[Segment]]) -> (Int, Int) {
let mut max_width = 0
for line in lines {
max_width = @cmp.maximum(max_width, Segment::get_line_length(line))
}
(max_width, lines.length())
}
///|
/// Set the shape of a list of lines (enclosing rectangle).
pub fn Segment::set_shape(
lines : Array[Array[Segment]],
width : Int,
height? : Int,
style? : Style,
new_lines? : Bool = false,
) -> Array[Array[Segment]] {
let height = match height {
Some(h) if h != 0 => h
_ => lines.length()
}
let blank = if new_lines {
[Segment::new(space_string(width) + "\n", style?)]
} else {
[Segment::new(space_string(width), style?)]
}
// upstream adjusts *all* lines (the slice assignment replaces the
// truncated list), so `height` only ever adds lines
let shaped_lines = lines.map(line => {
Segment::adjust_line_length(line, width, style?)
})
while shaped_lines.length() < height {
shaped_lines.push(blank)
}
shaped_lines
}
///|
fn blank_line(width : Int, style : Style?, new_lines : Bool) -> Segment {
if new_lines {
{ text: space_string(width) + "\n", style, control: None, }
} else {
{ text: space_string(width), style, control: None, }
}
}
///|
/// Align lines to top (adds extra lines to bottom as required).
pub fn Segment::align_top(
lines : Array[Array[Segment]],
width : Int,
height : Int,
style : Style?,
new_lines? : Bool = false,
) -> Array[Array[Segment]] {
let extra_lines = height - lines.length()
if extra_lines == 0 {
return lines.copy()
}
let result = py_head(lines, height)
let blank = blank_line(width, style, new_lines)
for _ in 0.. Array[Array[Segment]] {
let extra_lines = height - lines.length()
if extra_lines == 0 {
return lines.copy()
}
let blank = blank_line(width, style, new_lines)
let result = []
for _ in 0.. Array[Array[Segment]] {
let extra_lines = height - lines.length()
if extra_lines == 0 {
return lines.copy()
}
let blank = blank_line(width, style, new_lines)
let top_lines = floor_div(extra_lines, 2)
let bottom_lines = extra_lines - top_lines
let result = []
for _ in 0.. Array[T] {
let len = xs.length()
let end = if n < 0 { @cmp.maximum(0, len + n) } else { @cmp.minimum(n, len) }
xs[0:end].to_owned()
}
///|
/// Python floor division.
fn floor_div(a : Int, b : Int) -> Int {
let q = a / b
if a % b != 0 && (a < 0) != (b < 0) {
q - 1
} else {
q
}
}
///|
/// Python modulo.
fn py_mod(a : Int, b : Int) -> Int {
let r = a % b
if r != 0 && (r < 0) != (b < 0) {
r + b
} else {
r
}
}
///|
/// Simplify segments by combining contiguous segments with the same style.
pub fn Segment::simplify(segments : Array[Segment]) -> Array[Segment] {
guard segments is [first, .. rest] else { return [] }
let result = []
let mut last_segment = first
for segment in rest {
if last_segment.style == segment.style && !segment.has_codes() {
last_segment = {
text: last_segment.text + segment.text,
style: last_segment.style,
control: None,
}
} else {
result.push(last_segment)
last_segment = segment
}
}
result.push(last_segment)
result
}
///|
/// Remove all links from segments.
pub fn Segment::strip_links(segments : Array[Segment]) -> Array[Segment] {
segments.map(segment => {
match segment.style {
_ if segment.has_codes() => segment
None => segment
Some(style) =>
{
text: segment.text,
style: if style.to_bool() {
Some(style.update_link(None))
} else {
None
},
control: None,
}
}
})
}
///|
/// Remove all styles from segments.
pub fn Segment::strip_styles(segments : Array[Segment]) -> Array[Segment] {
segments.map(segment => {
text: segment.text,
style: None,
control: segment.control,
})
}
///|
/// Remove all color from segments.
pub fn Segment::remove_color(segments : Array[Segment]) -> Array[Segment] {
let cache : Map[Style, Style] = Map([])
segments.map(segment => {
match segment.style {
Some(style) if style.to_bool() => {
let colorless = match cache.get(style) {
Some(s) => s
None => {
let s = style.without_color()
cache[style] = s
s
}
}
{
text: segment.text,
style: Some(colorless),
control: segment.control,
}
}
_ => { text: segment.text, style: None, control: segment.control, }
}
})
}
///|
/// Divide segments in to portions at the given cell positions.
pub fn Segment::divide(
segments : Array[Segment],
cuts : Array[Int],
) -> Array[Array[Segment]] {
let result = []
let split_segments = []
let mut cut_index = 0
fn next_cut() -> Int {
if cut_index < cuts.length() {
let c = cuts[cut_index]
cut_index += 1
c
} else {
-1
}
}
let mut cut = -1
while true {
cut = next_cut()
if cut == -1 {
return result
}
if cut != 0 {
break
}
result.push([])
}
let mut pos = 0
for seg in segments {
let mut segment = seg
while segment.text != "" {
let end_pos = if segment.has_codes() {
pos
} else {
pos + @cells.cell_len(segment.text)
}
if end_pos < cut {
split_segments.push(segment)
pos = end_pos
break
}
if end_pos == cut {
split_segments.push(segment)
result.push(split_segments.copy())
split_segments.clear()
pos = end_pos
cut = next_cut()
if cut == -1 {
if !split_segments.is_empty() {
result.push(split_segments.copy())
}
return result
}
break
} else {
let (before, after) = segment.split_cells(cut - pos)
segment = after
split_segments.push(before)
result.push(split_segments.copy())
split_segments.clear()
pos = cut
}
cut = next_cut()
if cut == -1 {
if !split_segments.is_empty() {
result.push(split_segments.copy())
}
return result
}
}
}
result.push(split_segments.copy())
result
}
///|
/// A simple renderable to render a list of segments.
pub struct Segments {
segments : Array[Segment]
new_lines : Bool
}
///|
/// With `new_lines`, a new line is added after each segment.
pub fn Segments::new(
segments : Array[Segment],
new_lines? : Bool = false,
) -> Segments {
{ segments: segments.copy(), new_lines, }
}
///|
pub impl Renderable for Segments with fn rich_console(self, _console, _options) {
let out = []
for segment in self.segments {
out.push(Seg(segment))
if self.new_lines {
out.push(Seg(Segment::line()))
}
}
out
}
///|
/// A renderable to render lines of segments.
pub struct SegmentLines {
lines : Array[Array[Segment]]
new_lines : Bool
}
///|
/// With `new_lines`, a new line is added after each line.
pub fn SegmentLines::new(
lines : Array[Array[Segment]],
new_lines? : Bool = false,
) -> SegmentLines {
{ lines: lines.copy(), new_lines, }
}
///|
pub impl Renderable for SegmentLines with fn rich_console(
self,
_console,
_options,
) {
let out = []
for line in self.lines {
for segment in line {
out.push(Seg(segment))
}
if self.new_lines {
out.push(Seg(Segment::line()))
}
}
out
}