use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::symbols::{Marker, line};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::OnceLock;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy)]
pub struct Glyphs {
pub unicode: bool,
pub selector: &'static str,
pub selector_blank: &'static str,
pub cursor: &'static str,
pub prompt: &'static str,
pub rule: &'static str,
pub rule_broken: &'static str,
pub ellipsis: &'static str,
pub up: &'static str,
pub times: &'static str,
pub collapsed: &'static str,
pub expanded: &'static str,
pub updown: &'static str,
pub updown_lr: &'static str,
pub ctrl_updown: &'static str,
pub middot: &'static str,
pub dash: &'static str,
pub r_squared: &'static str,
pub rho: &'static str,
pub spinner: &'static [&'static str],
pub here: &'static str,
pub in_memory: &'static str,
pub over_network: &'static str,
pub in_object_store: &'static str,
pub place_unknown: &'static str,
pub null: &'static str,
pub absent: &'static str,
pub conflict: &'static str,
pub drift_mark: &'static str,
pub sort_asc: &'static str,
pub sort_desc: &'static str,
pub rail: &'static str,
pub rule_h: &'static str,
pub rule_h_focused: &'static str,
pub arrow_left: &'static str,
pub arrow_right: &'static str,
pub trail: &'static str,
pub choice_prev: &'static str,
pub choice_next: &'static str,
pub hidden_mark: &'static str,
pub diff_mark: &'static str,
pub checkbox_on: &'static str,
pub checkbox_off: &'static str,
pub radio_on: &'static str,
pub radio_off: &'static str,
pub dot_full: &'static str,
pub dot_half: &'static str,
pub dot_empty: &'static str,
pub score_marks: &'static [&'static str; 5],
pub check: &'static str,
pub warning: &'static str,
pub scroll_thumb: &'static str,
pub scroll_track: &'static str,
pub binary_stub: &'static str,
pub newline_mark: &'static str,
pub tab_mark: &'static str,
pub control_mark: &'static str,
pub hex_dot: &'static str,
pub mini_bars: &'static [&'static str; 8],
pub unsampled: &'static str,
pub pointer: &'static str,
pub bar_eighths: &'static [&'static str; 8],
pub wordmark: Option<&'static [&'static str]>,
pub border: ratatui::symbols::border::Set<'static>,
pub plot: PlotMarks,
}
#[derive(Debug, Clone, Copy)]
pub struct PlotMarks {
pub line: Marker,
pub point: Marker,
pub bar: Marker,
pub column_eighths: &'static [&'static str; 8],
pub axis: line::Set<'static>,
pub tick_x: &'static str,
pub tick_y: &'static str,
pub grid_across: &'static str,
pub grid_down: &'static str,
}
impl PlotMarks {
pub fn column_set(&self) -> ratatui::symbols::bar::Set<'static> {
let e = self.column_eighths;
ratatui::symbols::bar::Set {
full: e[7],
seven_eighths: e[6],
three_quarters: e[5],
five_eighths: e[4],
half: e[3],
three_eighths: e[2],
one_quarter: e[1],
one_eighth: e[0],
empty: " ",
}
}
pub fn is_mark(marker: Marker, symbol: &str) -> bool {
let mut chars = symbol.chars();
let (Some(c), None) = (chars.next(), chars.next()) else {
return false;
};
match marker {
Marker::Braille => ('\u{2800}'..='\u{28ff}').contains(&c),
Marker::Dot => symbol == ratatui::symbols::DOT,
Marker::Custom(mark) => c == mark,
_ => false,
}
}
pub fn redraw_axes(&self, area: Rect, buf: &mut Buffer) {
let (from, to) = (line::NORMAL, self.axis);
if from == to {
return;
}
let area = area.intersection(buf.area);
let at = |x: u16, y: u16| buf[(x, y)].symbol();
let run = |x: u16, y: u16, (dx, dy): (i32, i32), symbol: &str| {
let mut n = 0;
let (mut cx, mut cy) = (i32::from(x) + dx, i32::from(y) + dy);
while (i32::from(area.left())..i32::from(area.right())).contains(&cx)
&& (i32::from(area.top())..i32::from(area.bottom())).contains(&cy)
&& at(cx as u16, cy as u16) == symbol
{
n += 1;
cx += dx;
cy += dy;
}
n
};
let (up, down, right) = ((0, -1), (0, 1), (1, 0));
let mut frame = Vec::new();
let mut found_axes = false;
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
let symbol = at(x, y);
if symbol == from.bottom_left {
let (high, wide) = (
run(x, y, up, from.vertical),
run(x, y, right, from.horizontal),
);
let end = x + wide + 1;
let boxed = end < area.right() && at(end, y) == from.bottom_right;
if high > 0 && wide > 0 && !boxed {
found_axes = true;
frame.extend((y - high..=y).map(|y| (x, y)));
frame.extend((x + 1..end).map(|x| (x, y)));
}
} else if symbol == from.top_left {
let wide = run(x, y, right, from.horizontal);
let high = run(x, y, down, from.vertical);
let (r, b) = (x + wide + 1, y + high + 1);
let closed = r < area.right()
&& b < area.bottom()
&& at(r, y) == from.top_right
&& at(x, b) == from.bottom_left
&& at(r, b) == from.bottom_right
&& run(r, y, down, from.vertical) == high
&& run(x, b, right, from.horizontal) == wide;
if closed {
for i in x..=r {
frame.extend([(i, y), (i, b)]);
}
for j in y + 1..b {
frame.extend([(x, j), (r, j)]);
}
}
}
}
}
if !found_axes {
frame = (area.top()..area.bottom())
.flat_map(|y| (area.left()..area.right()).map(move |x| (x, y)))
.collect();
}
let pairs = [
(from.vertical, to.vertical),
(from.horizontal, to.horizontal),
(from.top_left, to.top_left),
(from.top_right, to.top_right),
(from.bottom_left, to.bottom_left),
(from.bottom_right, to.bottom_right),
];
for (x, y) in frame {
let cell = &mut buf[(x, y)];
if let Some((_, twin)) = pairs.iter().find(|(line, _)| cell.symbol() == *line) {
cell.set_symbol(twin);
}
}
}
}
const UNICODE: Glyphs = Glyphs {
unicode: true,
selector: "▎ ",
selector_blank: " ",
cursor: "▏",
prompt: "› ",
rule: "│",
rule_broken: "┆",
ellipsis: "…",
up: "..",
times: "×",
collapsed: "▸ ",
expanded: "▾ ",
updown: "↑↓",
updown_lr: "←→",
ctrl_updown: "^↑↓",
middot: "·",
dash: "—",
r_squared: "R²",
rho: "ρ",
spinner: &["⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾"],
here: "◦",
in_memory: "▪",
over_network: "↕",
in_object_store: "≈",
place_unknown: "◌",
null: "∅",
absent: "·",
conflict: "≠",
drift_mark: "*",
sort_asc: "▲",
sort_desc: "▼",
rail: "▎",
rule_h: "─",
rule_h_focused: "━",
arrow_left: "←",
arrow_right: "→",
trail: "›",
choice_prev: "‹",
choice_next: "›",
hidden_mark: "⊘",
diff_mark: "Δ",
checkbox_on: "■",
checkbox_off: "□",
radio_on: "●",
radio_off: "○",
dot_full: "●",
dot_half: "◔",
dot_empty: "○",
score_marks: &["○", "◔", "◕", "◉", "●"],
check: "✓",
warning: "▲",
scroll_thumb: "█",
scroll_track: "░",
binary_stub: "‹binary›",
newline_mark: "¶",
tab_mark: "»",
control_mark: "¤",
hex_dot: "·",
mini_bars: &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"],
unsampled: "·",
pointer: "▲",
bar_eighths: &["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"],
wordmark: Some(&[
"┌──╮ ╭──╮ ╶─┬─╴ ╷ ╷ ╶┬╴",
"│ │ ├──┤ │ │ │ │ ",
"└──╯ ╵ ╵ ╵ ╰──╯ ╶┴╴",
]),
border: ratatui::symbols::border::ROUNDED,
plot: PlotMarks {
line: Marker::Braille,
point: Marker::Dot,
bar: Marker::HalfBlock,
column_eighths: &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"],
axis: line::NORMAL,
tick_x: "┬",
tick_y: "┤",
grid_across: "·",
grid_down: "┊",
},
};
const ASCII: Glyphs = Glyphs {
unicode: false,
selector: "> ",
selector_blank: " ",
cursor: "_",
prompt: "> ",
rule: "|",
rule_broken: ":",
ellipsis: "...",
up: "..",
times: "x",
collapsed: "+ ",
expanded: "- ",
updown: "Up/Dn",
updown_lr: "Lt/Rt",
ctrl_updown: "^Up/Dn",
middot: "-",
dash: "-",
r_squared: "R^2",
rho: "rho",
spinner: &["|", "/", "-", "\\"],
here: ".",
in_memory: "*",
over_network: "~",
in_object_store: "@",
place_unknown: "?",
null: "~",
absent: ".",
conflict: "!",
drift_mark: "*",
sort_asc: "^",
sort_desc: "v",
rail: ">",
rule_h: "-",
rule_h_focused: "=",
arrow_left: "<",
arrow_right: ">",
trail: ">",
choice_prev: "<",
choice_next: ">",
hidden_mark: "x",
diff_mark: "*",
checkbox_on: "[x]",
checkbox_off: "[ ]",
radio_on: "(*)",
radio_off: "( )",
dot_full: "#",
dot_half: "+",
dot_empty: ".",
score_marks: &[".", "-", "+", "*", "#"],
check: "+",
warning: "!",
scroll_thumb: "#",
scroll_track: "|",
binary_stub: "<binary>",
newline_mark: "$",
tab_mark: ">",
control_mark: "?",
hex_dot: ".",
mini_bars: &[".", ":", "-", "=", "+", "*", "#", "@"],
unsampled: "?",
pointer: "^",
bar_eighths: &["-", "-", "-", "=", "=", "=", "=", "#"],
wordmark: None,
border: ratatui::symbols::border::Set {
top_left: "+",
top_right: "+",
bottom_left: "+",
bottom_right: "+",
vertical_left: "|",
vertical_right: "|",
horizontal_top: "-",
horizontal_bottom: "-",
},
plot: PlotMarks {
line: Marker::Custom('*'),
point: Marker::Custom('o'),
bar: Marker::Custom('#'),
column_eighths: &["_", "_", "-", "-", "-", "#", "#", "#"],
axis: line::Set {
vertical: "|",
horizontal: "-",
top_right: "+",
top_left: "+",
bottom_right: "+",
bottom_left: "+",
vertical_left: "+",
vertical_right: "+",
horizontal_down: "+",
horizontal_up: "+",
cross: "+",
},
tick_x: "+",
tick_y: "+",
grid_across: ".",
grid_down: ":",
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UnicodeMode {
#[default]
Auto,
Always,
Never,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum SlotOverride {
One(String),
Many(Vec<String>),
}
macro_rules! with_string_slots {
($callback:ident) => {
$callback!(
selector,
selector_blank,
cursor,
prompt,
rule,
rule_broken,
ellipsis,
up,
times,
collapsed,
expanded,
updown,
updown_lr,
ctrl_updown,
middot,
dash,
r_squared,
rho,
here,
in_memory,
over_network,
in_object_store,
place_unknown,
null,
absent,
conflict,
drift_mark,
sort_asc,
sort_desc,
rail,
rule_h,
rule_h_focused,
arrow_left,
arrow_right,
trail,
choice_prev,
choice_next,
hidden_mark,
diff_mark,
checkbox_on,
checkbox_off,
radio_on,
radio_off,
dot_full,
dot_half,
dot_empty,
check,
warning,
scroll_thumb,
scroll_track,
binary_stub,
newline_mark,
tab_mark,
control_mark,
hex_dot,
unsampled,
pointer
)
};
}
pub fn asciify_instructions(text: &str) -> std::borrow::Cow<'_, str> {
if get().unicode || text.is_ascii() {
return std::borrow::Cow::Borrowed(text);
}
std::borrow::Cow::Owned(instructions_in_ascii(text))
}
pub fn instructions_in_ascii(text: &str) -> String {
let mut out = String::with_capacity(text.len() + text.len() / 8);
let mut section: Vec<&str> = Vec::new();
for line in text.split('\n') {
if line.trim().is_empty() {
push_section(§ion, &mut out);
section.clear();
push_ascii(line, &mut out);
out.push('\n');
} else {
section.push(line);
}
}
push_section(§ion, &mut out);
out.pop();
out
}
fn push_section(lines: &[&str], out: &mut String) {
let rows: Vec<Option<(usize, usize, usize)>> = lines
.iter()
.map(|line| key_gap(line).map(|(key, desc)| (key, desc, display_width(&line[..desc]))))
.collect();
let shift = lines
.iter()
.zip(&rows)
.filter_map(|(line, row)| {
row.map(|(key, _, column)| (ascii_width(&line[..key]) + 2).saturating_sub(column))
})
.max()
.unwrap_or(0);
for (line, row) in lines.iter().zip(&rows) {
match *row {
Some((key, desc, column)) => {
push_ascii(&line[..key], out);
let pad = column + shift - ascii_width(&line[..key]);
out.extend(std::iter::repeat_n(' ', pad));
push_ascii(&line[desc..], out);
}
None => {
let indent = line.len() - line.trim_start_matches(' ').len();
if shift > 0 && rows.iter().flatten().any(|&(_, _, c)| c == indent) {
out.extend(std::iter::repeat_n(' ', shift));
}
push_ascii(line, out);
}
}
out.push('\n');
}
}
fn ascii_width(text: &str) -> usize {
let mut ascii = String::new();
push_ascii(text, &mut ascii);
ascii.len()
}
pub(crate) fn key_gap(line: &str) -> Option<(usize, usize)> {
let lead = line.len() - line.trim_start_matches(' ').len();
let key_end = lead + line[lead..].find(" ")?;
let desc_start = line.len() - line[key_end..].trim_start_matches(' ').len();
(desc_start < line.len()).then_some((key_end, desc_start))
}
fn push_ascii(text: &str, out: &mut String) {
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
let pair = match (c, chars.peek()) {
('↑', Some('↓')) => Some(ASCII.updown),
('←', Some('→')) => Some(ASCII.updown_lr),
_ => None,
};
if let Some(pair) = pair {
chars.next();
out.push_str(pair);
continue;
}
match ascii_twin(c) {
Some(twin) => out.push_str(twin),
None if c.is_ascii() => out.push(c),
None => out.push('?'),
}
}
}
pub fn display_width(text: &str) -> usize {
UnicodeWidthStr::width(text)
}
pub fn take_columns(text: &str, width: usize) -> &str {
let mut used = 0usize;
for (i, c) in text.char_indices() {
let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
if used + w > width {
return &text[..i];
}
used += w;
}
text
}
pub fn take_columns_end(text: &str, width: usize) -> &str {
let mut used = 0usize;
let mut start = text.len();
for (i, c) in text.char_indices().rev() {
let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
if used + w > width {
break;
}
used += w;
start = i;
}
&text[start..]
}
fn plain_ascii(text: &str) -> bool {
text.bytes().all(|b| (0x20..0x7f).contains(&b))
}
fn drawn_graphemes<'a>(span: &'a ratatui::text::Span<'a>) -> impl Iterator<Item = &'a str> {
span.styled_graphemes(ratatui::style::Style::default())
.map(|g| g.symbol)
}
pub fn cell_width(text: &str) -> usize {
use ratatui::buffer::CellWidth;
if plain_ascii(text) {
return text.len();
}
let span = ratatui::text::Span::raw(text);
drawn_graphemes(&span)
.map(|g| usize::from(g.cell_width()))
.sum()
}
pub fn fit_cells<'a>(text: &'a str, width: usize, marker: &str) -> Cow<'a, str> {
use ratatui::buffer::CellWidth;
let marker_width = cell_width(marker);
let head_len = text.len().min(width.saturating_add(1));
let head = text.get(..head_len).unwrap_or(text);
if head.len() == head_len && plain_ascii(head) {
if text.len() <= width {
return Cow::Borrowed(text);
}
if width <= marker_width {
return Cow::Owned(take_columns(marker, width).to_string());
}
return Cow::Owned(format!("{}{marker}", &text[..width - marker_width]));
}
let span = ratatui::text::Span::raw(text);
let mut total = 0usize;
let mut drawn_bytes = 0usize;
for g in drawn_graphemes(&span) {
total += usize::from(g.cell_width());
if total > width {
break;
}
drawn_bytes += g.len();
}
if total <= width {
return if drawn_bytes == text.len() {
Cow::Borrowed(text)
} else {
Cow::Owned(drawn_graphemes(&span).collect())
};
}
if width <= marker_width {
return Cow::Owned(take_columns(marker, width).to_string());
}
let budget = width - marker_width;
let mut used = 0usize;
let mut out = String::with_capacity(budget + marker.len());
for g in drawn_graphemes(&span) {
let w = usize::from(g.cell_width());
if used + w > budget {
break;
}
used += w;
out.push_str(g);
}
out.push_str(marker);
Cow::Owned(out)
}
fn ascii_twin(c: char) -> Option<&'static str> {
match c {
'↑' => Some("Up"),
'↓' => Some("Dn"),
'←' => Some("Lt"),
'→' => Some("Rt"),
'↔' => Some("Lt/Rt"),
'—' => Some("-"),
'…' => Some("..."),
'×' => Some("x"),
'≤' => Some("<="),
'≥' => Some(">="),
'∅' => Some("~"),
'·' => Some("."),
'≠' => Some("!"),
'ρ' => Some("rho"),
'│' => Some("|"),
'┆' => Some(":"),
'¶' => Some("$"),
'»' => Some(">"),
'¤' => Some("?"),
'Δ' => Some("*"),
_ => None,
}
}
fn unicode_default(slot: &str) -> Option<&'static str> {
macro_rules! lookup {
($($name:ident),*) => {
match slot {
$(stringify!($name) => Some(UNICODE.$name),)*
_ => None,
}
};
}
with_string_slots!(lookup)
}
pub fn validate_overrides(overrides: &BTreeMap<String, SlotOverride>) -> Result<(), String> {
let same_width = |slot: &str, text: &str, default: &str| -> Result<(), String> {
if text.width() == default.width() {
Ok(())
} else {
Err(format!(
"glyph override for `{slot}` is {} columns wide; {default:?} is {} — \
an override must keep the width of the glyph it replaces",
text.width(),
default.width(),
))
}
};
for (slot, value) in overrides {
match (unicode_default(slot), value) {
(Some(default), SlotOverride::One(text)) => same_width(slot, text, default)?,
(Some(_), SlotOverride::Many(_)) => {
return Err(format!(
"glyph slot `{slot}` takes a single string, not a list"
));
}
(None, _) => {
let (len, default) = match slot.as_str() {
"spinner" => (None, UNICODE.spinner),
"score_marks" => (Some(5), &UNICODE.score_marks[..]),
"mini_bars" => (Some(8), &UNICODE.mini_bars[..]),
"bar_eighths" => (Some(8), &UNICODE.bar_eighths[..]),
_ => return Err(format!("unknown glyph slot `{slot}`")),
};
let SlotOverride::Many(entries) = value else {
return Err(format!("glyph slot `{slot}` takes a list of strings"));
};
match len {
Some(len) if entries.len() != len => {
return Err(format!(
"glyph slot `{slot}` takes exactly {len} entries, got {}",
entries.len()
));
}
None if entries.is_empty() => {
return Err(format!("glyph slot `{slot}` takes at least one entry"));
}
_ => {}
}
for entry in entries {
same_width(slot, entry, default[0])?;
}
}
}
}
Ok(())
}
fn leak(text: &str) -> &'static str {
Box::leak(text.to_string().into_boxed_str())
}
fn apply_overrides(set: &mut Glyphs, overrides: &BTreeMap<String, SlotOverride>) {
for (slot, value) in overrides {
match (slot.as_str(), value) {
("spinner", SlotOverride::Many(frames)) => {
set.spinner = Box::leak(
frames
.iter()
.map(|f| leak(f))
.collect::<Vec<_>>()
.into_boxed_slice(),
);
}
("score_marks", SlotOverride::Many(marks)) if marks.len() == 5 => {
set.score_marks = Box::leak(Box::new(std::array::from_fn(|i| leak(&marks[i]))));
}
("mini_bars", SlotOverride::Many(bars)) if bars.len() == 8 => {
set.mini_bars = Box::leak(Box::new(std::array::from_fn(|i| leak(&bars[i]))));
}
("bar_eighths", SlotOverride::Many(bars)) if bars.len() == 8 => {
set.bar_eighths = Box::leak(Box::new(std::array::from_fn(|i| leak(&bars[i]))));
}
(name, SlotOverride::One(text)) => {
let text = leak(text);
macro_rules! assign {
($($slot:ident),*) => {
match name {
$(stringify!($slot) => set.$slot = text,)*
_ => {}
}
};
}
with_string_slots!(assign)
}
_ => {}
}
}
}
static GLYPHS: OnceLock<Glyphs> = OnceLock::new();
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct Environment {
locale: Option<String>,
windows: bool,
}
impl Environment {
fn current() -> Self {
Self {
locale: ["LC_ALL", "LC_CTYPE", "LANG"]
.into_iter()
.find_map(|key| std::env::var(key).ok().filter(|v| !v.is_empty())),
windows: cfg!(windows),
}
}
fn is_utf8(&self) -> bool {
match &self.locale {
Some(value) => {
let lower = value.to_ascii_lowercase();
lower.contains("utf-8") || lower.contains("utf8")
}
None => self.windows,
}
}
}
pub fn environment_is_utf8() -> bool {
Environment::current().is_utf8()
}
pub fn init_with_overrides(mode: UnicodeMode, overrides: &BTreeMap<String, SlotOverride>) {
let mut chosen = match mode {
UnicodeMode::Always => UNICODE,
UnicodeMode::Never => ASCII,
UnicodeMode::Auto => {
if environment_is_utf8() {
UNICODE
} else {
ASCII
}
}
};
if chosen.unicode && !overrides.is_empty() {
apply_overrides(&mut chosen, overrides);
}
let _ = GLYPHS.set(chosen);
}
pub fn get() -> &'static Glyphs {
GLYPHS.get_or_init(|| {
if environment_is_utf8() {
UNICODE
} else {
ASCII
}
})
}
pub fn active_is_unicode() -> bool {
get().unicode
}
pub fn unicode() -> &'static Glyphs {
&UNICODE
}
#[cfg(test)]
pub(crate) fn frame_corners(rows: &[String]) -> Vec<(usize, usize)> {
let b = get().border;
let grid: Vec<Vec<String>> = rows
.iter()
.map(|r| r.chars().map(String::from).collect())
.collect();
let at = |x: usize, y: usize| grid.get(y).and_then(|r| r.get(x)).map(String::as_str);
let glyphs = [b.top_left, b.top_right, b.bottom_left, b.bottom_right];
if (1..4).all(|i| !glyphs[..i].contains(&glyphs[i])) {
let opened: usize = rows.iter().map(|r| r.matches(b.top_left).count()).sum();
let closed: Vec<(usize, usize)> = grid
.iter()
.enumerate()
.flat_map(|(y, row)| {
row.iter()
.enumerate()
.filter(|(_, cell)| cell.as_str() == b.bottom_left)
.map(move |(x, _)| (x, y))
})
.collect();
assert_eq!(opened, closed.len(), "every frame closes: {rows:#?}");
return closed;
}
let mut corners = Vec::new();
for (y, row) in grid.iter().enumerate().skip(1) {
for x in 0..row.len() {
if at(x, y) == Some(b.bottom_left)
&& at(x, y - 1) == Some(b.vertical_left)
&& at(x + 1, y) == Some(b.horizontal_bottom)
&& (x == 0 || at(x - 1, y) != Some(b.horizontal_bottom))
{
corners.push((x, y));
}
}
}
corners
}
pub fn ascii() -> &'static Glyphs {
&ASCII
}
pub fn fit(text: &str, width: usize) -> String {
fit_cells(text, width, get().ellipsis).into_owned()
}
pub fn fit_start(text: &str, width: usize) -> String {
if display_width(text) <= width {
return text.to_string();
}
let ellipsis = get().ellipsis;
let ellipsis_width = display_width(ellipsis);
if width <= ellipsis_width {
return take_columns_end(text, width).to_string();
}
format!(
"{ellipsis}{}",
take_columns_end(text, width - ellipsis_width)
)
}
pub fn fit_middle(text: &str, width: usize) -> String {
if display_width(text) <= width {
return text.to_string();
}
let mark = get().ellipsis;
let mark_w = display_width(mark);
if width <= mark_w {
return take_columns(mark, width).to_string();
}
let room = width - mark_w;
let head = take_columns(text, room.div_ceil(2));
let tail = take_columns_end(text, room - display_width(head));
format!("{head}{mark}{tail}")
}
pub fn dotted(text: &str) -> String {
text.replace('·', get().middot)
}
#[cfg(test)]
mod tests;