use std::sync::Arc;
pub use crossterm::style::Color;
pub use tau_proto::MAX_HYPERLINK_TARGET_BYTES;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::TwoLineElision;
pub fn display_width(text: &str) -> usize {
UnicodeSegmentation::graphemes(text, true)
.map(screen_grapheme_width)
.sum()
}
pub fn truncate_to_width(text: &str, max_width: usize) -> String {
if max_width == 0 {
return String::new();
}
if display_width(text) <= max_width {
return text.to_owned();
}
if max_width == 1 {
return "…".to_owned();
}
let mut out = String::new();
let mut width = 0;
let prefix_width = max_width - 1;
for grapheme in UnicodeSegmentation::graphemes(text, true) {
let grapheme_width = screen_grapheme_width(grapheme);
if prefix_width < width + grapheme_width {
break;
}
width += grapheme_width;
out.push_str(grapheme);
}
out.push('…');
out
}
pub fn previous_grapheme_boundary(text: &str, pos: usize) -> usize {
let pos = pos.min(text.len());
UnicodeSegmentation::grapheme_indices(text, true)
.map(|(idx, _)| idx)
.take_while(|idx| *idx < pos)
.last()
.unwrap_or(0)
}
pub fn next_grapheme_boundary(text: &str, pos: usize) -> usize {
if text.len() <= pos {
return text.len();
}
for (idx, grapheme) in UnicodeSegmentation::grapheme_indices(text, true) {
let end = idx + grapheme.len();
if pos < end {
return end;
}
}
text.len()
}
pub(crate) fn is_line_break_grapheme(grapheme: &str) -> bool {
matches!(grapheme, "\n" | "\r\n" | "\r")
}
pub(crate) fn screen_grapheme_width(grapheme: &str) -> usize {
if is_line_break_grapheme(grapheme) {
0
} else if grapheme == "\t" || grapheme.chars().any(char::is_control) {
1
} else {
UnicodeWidthStr::width(grapheme)
}
}
pub(crate) fn push_grapheme_cells(
cells: &mut Vec<Cell>,
grapheme: &str,
style: Style,
hyperlink: Option<&Arc<str>>,
) {
if grapheme == "\t" {
cells.push(Cell::new(' ', style).with_hyperlink(hyperlink.cloned()));
return;
}
if grapheme.chars().any(char::is_control) {
cells.push(Cell::new('�', style).with_hyperlink(hyperlink.cloned()));
return;
}
let grapheme_width = screen_grapheme_width(grapheme);
for (idx, ch) in grapheme.chars().enumerate() {
let width = if idx == 0 { grapheme_width } else { 0 };
cells.push(
Cell::new(ch, style)
.with_width(width)
.with_hyperlink(hyperlink.cloned()),
);
}
}
pub(crate) fn visit_styled_graphemes(
spans: &[Span],
mut f: impl FnMut(&str, Style, Option<&Arc<str>>),
) {
let mut text = String::new();
let mut char_styles = Vec::new();
for span in spans {
for ch in span.text.chars() {
char_styles.push((text.len(), span.style, span.hyperlink.as_ref()));
text.push(ch);
}
}
let mut style_idx = 0;
for (byte, grapheme) in UnicodeSegmentation::grapheme_indices(text.as_str(), true) {
while style_idx + 1 < char_styles.len() && char_styles[style_idx + 1].0 <= byte {
style_idx += 1;
}
let style = char_styles
.get(style_idx)
.map(|(_, style, _)| *style)
.unwrap_or_default();
let hyperlink = char_styles.get(style_idx).and_then(|(_, _, link)| *link);
f(grapheme, style, hyperlink);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub underline: bool,
pub italic: bool,
pub strikethrough: bool,
}
impl Style {
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Cell {
pub ch: char,
pub style: Style,
pub width: usize,
pub hyperlink: Option<Arc<str>>,
}
impl Cell {
pub(crate) fn sanitized_char(ch: char) -> char {
if ch == '\t' {
' '
} else if ch.is_control() {
'�'
} else {
ch
}
}
pub fn new(ch: char, style: Style) -> Self {
let ch = Self::sanitized_char(ch);
Self {
ch,
style,
width: ch.width().unwrap_or(0),
hyperlink: None,
}
}
pub fn plain(ch: char) -> Self {
let ch = Self::sanitized_char(ch);
Self {
ch,
style: Style::default(),
width: ch.width().unwrap_or(0),
hyperlink: None,
}
}
pub(crate) fn normalized(&self) -> Self {
let ch = Self::sanitized_char(self.ch);
if ch == self.ch {
self.clone()
} else {
Self {
ch,
style: self.style,
width: ch.width().unwrap_or(0),
hyperlink: self.hyperlink.clone(),
}
}
}
pub fn with_width(mut self, width: usize) -> Self {
self.width = width;
self
}
pub fn with_hyperlink(mut self, hyperlink: Option<Arc<str>>) -> Self {
self.hyperlink = hyperlink.filter(|target| sanitize_hyperlink_target(target).is_some());
self
}
pub fn col_width(&self) -> usize {
self.width
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Span {
pub text: String,
pub style: Style,
pub hyperlink: Option<Arc<str>>,
}
impl Span {
pub fn new(text: impl Into<String>, style: Style) -> Self {
Self {
text: text.into(),
style,
hyperlink: None,
}
}
pub fn plain(text: impl Into<String>) -> Self {
Self {
text: text.into(),
style: Style::default(),
hyperlink: None,
}
}
pub fn hyperlink(mut self, target: impl AsRef<str>) -> Self {
self.hyperlink = sanitize_hyperlink_target(target.as_ref()).map(Arc::from);
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StyledText {
spans: Vec<Span>,
}
impl StyledText {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, span: Span) {
self.spans.push(span);
}
pub fn spans(&self) -> &[Span] {
&self.spans
}
pub fn spans_mut(&mut self) -> &mut [Span] {
&mut self.spans
}
pub fn char_count(&self) -> usize {
let mut text = String::new();
for span in &self.spans {
text.push_str(&span.text);
}
display_width(&text)
}
pub fn is_empty(&self) -> bool {
self.spans.iter().all(|s| s.text.is_empty())
}
pub fn to_cells(&self) -> Vec<Cell> {
let mut cells = Vec::new();
visit_styled_graphemes(&self.spans, |grapheme, style, hyperlink| {
if !is_line_break_grapheme(grapheme) {
push_grapheme_cells(&mut cells, grapheme, style, hyperlink);
}
});
cells
}
}
pub fn sanitize_hyperlink_target(target: &str) -> Option<&str> {
(!target.is_empty()
&& target.len() <= MAX_HYPERLINK_TARGET_BYTES
&& !target.chars().any(char::is_control))
.then_some(target)
}
impl From<&str> for StyledText {
fn from(s: &str) -> Self {
Self {
spans: vec![Span::plain(s)],
}
}
}
impl From<String> for StyledText {
fn from(s: String) -> Self {
Self {
spans: vec![Span::plain(s)],
}
}
}
impl From<Span> for StyledText {
fn from(span: Span) -> Self {
Self { spans: vec![span] }
}
}
impl From<Vec<Span>> for StyledText {
fn from(spans: Vec<Span>) -> Self {
Self { spans }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BlockId(
pub u64,
);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Align {
#[default]
Left,
Center,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum BlockLayout {
Ordinary,
Priority {
line: crate::PriorityLine,
body: StyledText,
},
TwoLineElision(TwoLineElision),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StyledBlock {
pub content: StyledText,
pub right_content: StyledText,
pub(crate) layout: BlockLayout,
pub bg: Option<Color>,
pub align: Align,
pub margin_left: u16,
pub margin_right: u16,
}
impl StyledBlock {
pub fn new(content: impl Into<StyledText>) -> Self {
Self {
content: content.into(),
right_content: StyledText::new(),
layout: BlockLayout::Ordinary,
bg: None,
align: Align::Left,
margin_left: 0,
margin_right: 0,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match &self.layout {
BlockLayout::Ordinary => self.content.is_empty(),
BlockLayout::Priority { line, .. } => line.is_empty(),
BlockLayout::TwoLineElision(elision) => elision.is_empty(),
}
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn align(mut self, align: Align) -> Self {
self.align = align;
self
}
pub fn right_content(mut self, content: impl Into<StyledText>) -> Self {
self.right_content = content.into();
self
}
pub fn two_line_elision(mut self, elision: TwoLineElision) -> Self {
self.layout = BlockLayout::TwoLineElision(elision);
self
}
pub fn priority_line(mut self, line: crate::PriorityLine) -> Self {
self.layout = BlockLayout::Priority {
line,
body: StyledText::new(),
};
self
}
pub fn priority_line_body(mut self, body: impl Into<StyledText>) -> Self {
if let BlockLayout::Priority {
body: current_body, ..
} = &mut self.layout
{
*current_body = body.into();
}
self
}
pub fn priority_line_content(&self) -> Option<&crate::PriorityLine> {
match &self.layout {
BlockLayout::Priority { line, .. } => Some(line),
BlockLayout::Ordinary | BlockLayout::TwoLineElision(_) => None,
}
}
pub fn priority_line_body_content(&self) -> Option<&StyledText> {
match &self.layout {
BlockLayout::Priority { body, .. } => Some(body),
BlockLayout::Ordinary | BlockLayout::TwoLineElision(_) => None,
}
}
pub fn margin_left(mut self, n: u16) -> Self {
self.margin_left = n;
self
}
pub fn margin_right(mut self, n: u16) -> Self {
self.margin_right = n;
self
}
pub fn margins(mut self, left: u16, right: u16) -> Self {
self.margin_left = left;
self.margin_right = right;
self
}
}
impl From<&str> for StyledBlock {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for StyledBlock {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<StyledText> for StyledBlock {
fn from(text: StyledText) -> Self {
Self::new(text)
}
}