///|
pub(all) struct Frame {
lines : Array[String]
} derive(Eq, Debug)
///|
pub fn Frame::from_node(node : Node, size : Size) -> Frame {
{ lines: render_plain(node, size) }
}
///|
pub fn Frame::to_string(self : Frame) -> String {
join_lines(self.lines)
}
///|
priv struct StyledLine {
plain : String
ansi : String
style : Style
}
///|
priv struct StyledFrame {
lines : Array[StyledLine]
}
///|
pub fn ansi_style(style : Style) -> String {
let codes : Array[String] = []
if style.bold {
codes.push("1")
}
if style.dim {
codes.push("2")
}
if style.italic {
codes.push("3")
}
if style.underline {
codes.push("4")
}
if style.blink {
codes.push("5")
}
if style.reverse {
codes.push("7")
}
if style.strikethrough {
codes.push("9")
}
write_color_codes(codes, style.fg, foreground=true)
write_color_codes(codes, style.bg, foreground=false)
if codes.is_empty() {
""
} else {
let joined = join_with(codes, ";")
"\u{1b}[\{joined}m"
}
}
///|
pub fn ansi_reset() -> String {
"\u{1b}[0m"
}
///|
pub fn ansi_enter_alternate_screen() -> String {
"\u{1b}[?1049h"
}
///|
pub fn ansi_leave_alternate_screen() -> String {
"\u{1b}[?1049l"
}
///|
pub fn ansi_hide_cursor() -> String {
"\u{1b}[?25l"
}
///|
pub fn ansi_show_cursor() -> String {
"\u{1b}[?25h"
}
///|
pub fn ansi_clear_screen() -> String {
"\u{1b}[2J"
}
///|
pub fn ansi_clear_line() -> String {
"\u{1b}[K"
}
///|
pub fn ansi_disable_autowrap() -> String {
"\u{1b}[?7l"
}
///|
pub fn ansi_enable_autowrap() -> String {
"\u{1b}[?7h"
}
///|
pub fn ansi_move_cursor(row : Int, column : Int) -> String {
"\u{1b}[\{row};\{column}H"
}
///|
pub fn ansi_move_cursor_up(rows : Int) -> String {
if rows <= 0 {
""
} else {
"\u{1b}[\{rows}A"
}
}
///|
pub fn ansi_enable_mouse(mode : MouseMode) -> String {
match mode {
MouseOff => ansi_disable_mouse()
MouseCellMotion => "\u{1b}[?1000h\u{1b}[?1006h"
MouseButtonMotion => "\u{1b}[?1002h\u{1b}[?1006h"
MouseAllMotion => "\u{1b}[?1003h\u{1b}[?1006h"
}
}
///|
pub fn ansi_disable_mouse() -> String {
"\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1006l"
}
///|
pub fn ansi_enable_bracketed_paste() -> String {
"\u{1b}[?2004h"
}
///|
pub fn ansi_disable_bracketed_paste() -> String {
"\u{1b}[?2004l"
}
///|
pub fn ansi_enable_focus_events() -> String {
"\u{1b}[?1004h"
}
///|
pub fn ansi_disable_focus_events() -> String {
"\u{1b}[?1004l"
}
///|
pub fn ansi_set_window_title(title : String) -> String {
"\u{1b}]0;\{title}\u{07}"
}
///|
pub fn ansi_start_program(
options : ProgramOptions,
node : Node,
size : Size,
) -> String {
let out = StringBuilder::new()
if options.alternate_screen {
out.write_string(ansi_enter_alternate_screen())
}
out.write_string(ansi_disable_autowrap())
if options.hide_cursor {
out.write_string(ansi_hide_cursor())
}
if options.mouse_mode != MouseOff {
out.write_string(ansi_enable_mouse(options.mouse_mode))
}
if options.bracketed_paste {
out.write_string(ansi_enable_bracketed_paste())
}
if options.focus_events {
out.write_string(ansi_enable_focus_events())
}
out.write_string(ansi_clear_screen())
out.write_string(paint_styled_frame_ansi(StyledFrame::from_node(node, size)))
out.to_string()
}
///|
pub fn ansi_stop_program(options : ProgramOptions) -> String {
let out = StringBuilder::new()
out.write_string(ansi_enable_autowrap())
if options.focus_events {
out.write_string(ansi_disable_focus_events())
}
if options.bracketed_paste {
out.write_string(ansi_disable_bracketed_paste())
}
if options.mouse_mode != MouseOff {
out.write_string(ansi_disable_mouse())
}
if options.hide_cursor {
out.write_string(ansi_show_cursor())
}
if options.alternate_screen {
out.write_string(ansi_leave_alternate_screen())
}
out.to_string()
}
///|
pub fn ansi_terminal_command(command : TerminalCommand) -> String {
match command {
QuitProgram | Repaint => ""
Print(value) => value
PrintLine(value) => "\{value}\r\n"
PrintErr(value) => value
PrintErrLine(value) => "\{value}\r\n"
ClearScreen => "\{ansi_clear_screen()}\{ansi_move_cursor(1, 1)}"
EnterAlternateScreen => ansi_enter_alternate_screen()
LeaveAlternateScreen => ansi_leave_alternate_screen()
HideCursor => ansi_hide_cursor()
ShowCursor => ansi_show_cursor()
EnableMouse(mode) => ansi_enable_mouse(mode)
DisableMouse => ansi_disable_mouse()
EnableBracketedPaste => ansi_enable_bracketed_paste()
DisableBracketedPaste => ansi_disable_bracketed_paste()
EnableFocusEvents => ansi_enable_focus_events()
DisableFocusEvents => ansi_disable_focus_events()
SetWindowTitle(title) => ansi_set_window_title(title)
}
}
///|
pub fn render_ansi(node : Node, size : Size) -> String {
let frame = StyledFrame::from_node(node, size)
let lines : Array[String] = []
for line in frame.lines {
lines.push(line.ansi)
}
join_lines(lines)
}
///|
pub fn full_render_ansi(node : Node, size : Size) -> String {
ansi_start_program(ProgramOptions::default(), node, size)
}
///|
pub fn diff_ansi(previous : Frame?, next : Frame) -> String {
match previous {
None => paint_frame_ansi(next)
Some(prev) => diff_lines(prev.lines, next.lines)
}
}
///|
pub fn diff_node_ansi(
previous : Frame?,
node : Node,
size : Size,
) -> (Frame, String) {
let next = StyledFrame::from_node(node, size)
let plain = next.plain_frame()
let output = match previous {
None => paint_styled_frame_ansi(next)
Some(prev) => diff_styled_lines(prev.lines, next.lines)
}
(plain, output)
}
///|
pub fn paint_frame_ansi(frame : Frame) -> String {
let out = StringBuilder::new()
for index, line in frame.lines {
out.write_string(ansi_move_cursor(index + 1, 1))
out.write_string(screen_line(line))
}
out.to_string()
}
///|
fn paint_styled_frame_ansi(frame : StyledFrame) -> String {
let out = StringBuilder::new()
for index, line in frame.lines {
out.write_string(ansi_move_cursor(index + 1, 1))
out.write_string(screen_styled_line(line))
}
out.to_string()
}
///|
fn paint_styled_frame_inline(frame : StyledFrame) -> String {
let out = StringBuilder::new()
for index, line in frame.lines {
if index > 0 {
out.write_string("\r\n")
}
out.write_string(screen_styled_line(line))
}
out.to_string()
}
///|
fn repaint_styled_frame_inline(
previous_height : Int,
frame : StyledFrame,
) -> String {
let out = StringBuilder::new()
out.write_string(ansi_move_cursor_up(previous_height - 1))
out.write_string("\r")
let limit = Int::max(previous_height, frame.lines.length())
for index in 0.. 0 {
out.write_string("\r\n")
}
let line = if index < frame.lines.length() {
frame.lines[index]
} else {
styled_line("", Style::default())
}
out.write_string(screen_styled_line(line))
}
out.to_string()
}
///|
fn diff_styled_frame_inline(
previous : StyledFrame,
next : StyledFrame,
) -> String {
let out = StringBuilder::new()
if previous.lines.length() > 0 {
out.write_string(ansi_move_cursor_up(previous.lines.length() - 1))
}
out.write_string("\r")
let limit = Int::max(previous.lines.length(), next.lines.length())
for index in 0.. 0 {
out.write_string("\r\n")
}
let old_line = if index < previous.lines.length() {
previous.lines[index]
} else {
styled_line("", Style::default())
}
let new_line = if index < next.lines.length() {
next.lines[index]
} else {
styled_line("", Style::default())
}
if old_line.plain != new_line.plain || old_line.ansi != new_line.ansi {
out.write_string(screen_styled_line(new_line))
}
}
out.to_string()
}
///|
fn diff_styled_frame(previous : StyledFrame, next : StyledFrame) -> String {
let out = StringBuilder::new()
let limit = Int::max(previous.lines.length(), next.lines.length())
for index in 0.. String {
let out = StringBuilder::new()
let limit = Int::max(previous.length(), next.length())
for index in 0.. String {
let out = StringBuilder::new()
let limit = Int::max(previous.length(), next.length())
for index in 0.. String {
"\{trim_trailing_spaces(line)}\{ansi_clear_line()}"
}
///|
fn screen_styled_line(line : StyledLine) -> String {
let plain = trim_trailing_spaces(line.plain)
if line.style == Style::default() {
if line.ansi == line.plain {
"\{plain}\{ansi_clear_line()}"
} else {
"\{trim_trailing_spaces(line.ansi)}\{ansi_clear_line()}"
}
} else if line.style.bg != Default {
"\{ansi_style(line.style)}\{plain}\{ansi_clear_line()}\{ansi_reset()}"
} else {
"\{styled_text(plain, line.style)}\{ansi_clear_line()}"
}
}
///|
fn trim_trailing_spaces(line : String) -> String {
let mut end = line.length()
while end > 0 && line[end - 1] == ' ' {
end -= 1
}
line[:end].to_owned()
}
///|
fn StyledFrame::from_node(node : Node, size : Size) -> StyledFrame {
{
lines: fit_styled_lines(
render_styled_lines(node, size, Style::default()),
size,
),
}
}
///|
fn StyledFrame::plain_frame(self : StyledFrame) -> Frame {
let lines : Array[String] = []
for line in self.lines {
lines.push(line.plain)
}
{ lines, }
}
///|
fn render_styled_lines(
node : Node,
size : Size,
inherited : Style,
) -> Array[StyledLine] {
match node {
Empty => []
Text(value, style) => {
let out : Array[StyledLine] = []
let text_style = inherited.merge(style)
for line in split_ansi_lines(value) {
out.push(styled_line(line, text_style))
}
out
}
Fragment(children) => {
let out : Array[StyledLine] = []
for child in children {
for line in render_styled_lines(child, size, inherited) {
out.push(line)
}
}
out
}
VStack(children, gap) =>
render_styled_vstack(children, gap, size, inherited)
HStack(children, gap) =>
render_styled_hstack(children, gap, size, inherited)
Pad(child, edge) => render_styled_padded(child, edge, size, inherited)
Styled(child, style) =>
render_styled_lines(child, size, inherited.merge(style))
Bordered(child, border, style) =>
render_styled_border(
child,
border,
inherited.merge(style),
size,
inherited,
)
Clip(child, clip_size) =>
fit_styled_lines(
render_styled_lines(child, clip_size, inherited),
clip_size,
)
Sized(child, fixed) => {
let target = {
width: if fixed.width >= 0 {
fixed.width
} else {
size.width
},
height: if fixed.height >= 0 {
fixed.height
} else {
size.height
},
}
fit_styled_block(
render_styled_lines(child, target, inherited),
target,
inherited,
)
}
Aligned(child, horizontal, vertical) =>
render_styled_aligned(child, size, horizontal, vertical, inherited)
Placed(child, place_size, horizontal, vertical) =>
render_styled_aligned(child, place_size, horizontal, vertical, inherited)
Fill(value, style) => {
let out : Array[StyledLine] = []
let fill_style = inherited.merge(style)
for _ in 0.. Array[StyledLine] {
let out : Array[StyledLine] = []
for index, child in children {
if index > 0 {
for _ in 0.. Array[StyledLine] {
if children.is_empty() {
return []
}
let child_width = Int::max(
1,
(size.width - gap * (children.length() - 1)) / children.length(),
)
let rendered : Array[Array[StyledLine]] = []
let mut height = 0
for child in children {
let child_lines = fit_styled_lines(
render_styled_lines(
child,
{ width: child_width, height: size.height },
inherited,
),
{ width: child_width, height: size.height },
)
if child_lines.length() > height {
height = child_lines.length()
}
rendered.push(child_lines)
}
let out : Array[StyledLine] = []
let spacer = repeat_to_width(" ", gap)
let styled_spacer = styled_text(spacer, inherited)
for row in 0.. 0 {
plain.write_string(spacer)
ansi.write_string(styled_spacer)
}
let part = if row < child_lines.length() {
pad_styled_right(child_lines[row], child_width)
} else {
styled_line(repeat_to_width(" ", child_width), inherited)
}
plain.write_string(part.plain)
ansi.write_string(part.ansi)
}
out.push(raw_styled_line(plain.to_string(), ansi.to_string()))
}
out
}
///|
fn render_styled_padded(
child : Node,
edge : Edge,
size : Size,
inherited : Style,
) -> Array[StyledLine] {
let inner = {
width: Int::max(0, size.width - edge.left - edge.right),
height: Int::max(0, size.height - edge.top - edge.bottom),
}
let out : Array[StyledLine] = []
let blank = styled_line(repeat_to_width(" ", size.width), inherited)
for _ in 0.. Array[StyledLine] {
if kind is NoBorder || size.width < 2 || size.height < 2 {
return render_styled_lines(child, size, inherited)
}
let (tl, tr, bl, br, horizontal, vertical) = match kind {
Normal => ("┌", "┐", "└", "┘", "─", "│")
Rounded => ("╭", "╮", "╰", "╯", "─", "│")
Square => ("+", "+", "+", "+", "-", "|")
Double => ("╔", "╗", "╚", "╝", "═", "║")
Thick => ("┏", "┓", "┗", "┛", "━", "┃")
NoBorder => ("", "", "", "", "", "")
}
let inner = { width: size.width - 2, height: size.height - 2 }
let inner_lines = fit_styled_lines(
render_styled_lines(child, inner, inherited),
inner,
)
let out : Array[StyledLine] = []
let horizontal_line = repeat_to_width(horizontal, inner.width)
out.push(styled_line("\{tl}\{horizontal_line}\{tr}", border_style))
for line in inner_lines {
let vertical_line = styled_text(vertical, border_style)
out.push(
raw_styled_line(
"\{vertical}\{line.plain}\{vertical}",
"\{vertical_line}\{line.ansi}\{vertical_line}",
),
)
}
while out.length() < size.height - 1 {
let blank = repeat_to_width(" ", inner.width)
let styled_vertical = styled_text(vertical, border_style)
let styled_blank = styled_text(blank, inherited)
out.push(
raw_styled_line(
"\{vertical}\{blank}\{vertical}",
"\{styled_vertical}\{styled_blank}\{styled_vertical}",
),
)
}
out.push(styled_line("\{bl}\{horizontal_line}\{br}", border_style))
out
}
///|
fn render_styled_aligned(
child : Node,
size : Size,
horizontal : Align,
vertical : Align,
inherited : Style,
) -> Array[StyledLine] {
let raw = render_styled_lines(child, size, inherited)
let lines : Array[StyledLine] = []
let limit = Int::min(raw.length(), size.height)
for index in 0.. Array[StyledLine] {
let out : Array[StyledLine] = []
let limit = Int::min(lines.length(), size.height)
for index in 0.. Array[StyledLine] {
let out = fit_styled_lines(lines, size)
while out.length() < size.height {
out.push(styled_line(repeat_to_width(" ", size.width), inherited))
}
out
}
///|
fn fit_styled_line(line : StyledLine, width : Int) -> StyledLine {
let plain = fit_line(line.plain, width)
if line.style != Style::default() {
styled_line(plain, line.style)
} else if line.ansi == line.plain {
styled_line(plain, Style::default())
} else if display_width(line.plain) <= width {
pad_styled_right(line, width)
} else {
styled_line(plain, Style::default())
}
}
///|
fn pad_styled_right(line : StyledLine, width : Int) -> StyledLine {
let padding = repeat_to_width(
" ",
Int::max(0, width - display_width(line.plain)),
)
raw_styled_line("\{line.plain}\{padding}", "\{line.ansi}\{padding}")
}
///|
fn align_styled_line(
line : StyledLine,
width : Int,
alignment : Align,
) -> StyledLine {
let clipped = fit_styled_line(
line,
Int::min(width, display_width(line.plain)),
)
let remaining = Int::max(0, width - display_width(clipped.plain))
match alignment {
Start => pad_styled_right(clipped, width)
Center => {
let left = remaining / 2
let left_pad = repeat_to_width(" ", left)
let right_pad = repeat_to_width(" ", remaining - left)
raw_styled_line(
"\{left_pad}\{clipped.plain}\{right_pad}",
"\{left_pad}\{clipped.ansi}\{right_pad}",
)
}
End => {
let pad = repeat_to_width(" ", remaining)
raw_styled_line("\{pad}\{clipped.plain}", "\{pad}\{clipped.ansi}")
}
}
}
///|
fn styled_line(plain : String, style : Style) -> StyledLine {
{ plain, ansi: styled_text(plain, style), style }
}
///|
fn raw_styled_line(plain : String, ansi : String) -> StyledLine {
{ plain, ansi, style: Style::default() }
}
///|
fn styled_text(value : String, style : Style) -> String {
if style == Style::default() || value == "" {
value
} else {
"\{ansi_style(style)}\{value}\{ansi_reset()}"
}
}
///|
fn split_ansi_lines(value : String) -> Array[String] {
let out : Array[String] = []
let mut start = 0
let mut index = 0
while index < value.length() {
if value[index] == '\n' {
out.push(value[start:index].to_owned())
start = index + 1
}
index += 1
}
out.push(value[start:].to_owned())
out
}
///|
fn write_color_codes(
codes : Array[String],
color : Color,
foreground~ : Bool,
) -> Unit {
let base = if foreground { 30 } else { 40 }
match color {
Default => ()
Ansi(index) =>
if index >= 0 && index < 8 {
codes.push((base + index).to_string())
} else if index >= 8 && index < 16 {
codes.push((base + 60 + index - 8).to_string())
} else {
codes.push(if foreground { "38" } else { "48" })
codes.push("5")
codes.push(index.to_string())
}
Rgb(r, g, b) => {
codes.push(if foreground { "38" } else { "48" })
codes.push("2")
codes.push(clamp_color(r).to_string())
codes.push(clamp_color(g).to_string())
codes.push(clamp_color(b).to_string())
}
}
}
///|
fn clamp_color(value : Int) -> Int {
if value < 0 {
0
} else if value > 255 {
255
} else {
value
}
}
///|
fn join_lines(lines : Array[String]) -> String {
join_with(lines, "\n")
}
///|
fn join_with(items : Array[String], sep : String) -> String {
let out = StringBuilder::new()
for index, item in items {
if index > 0 {
out.write_string(sep)
}
out.write_string(item)
}
out.to_string()
}