use std::num::NonZeroU16;
use ratatui::buffer::{Buffer, CellDiffOption};
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Clear, Widget};
use ratatui::Frame;
use crate::view::palette;
pub(crate) const CUT: char = '…';
pub(crate) const GAP: usize = 2;
const OSC_8: &str = "\x1b]8;;";
const ST: &str = "\x1b\\";
pub(crate) fn openable(said: &str, to: &str) -> bool {
let holds_control = |text: &str| text.chars().any(char::is_control);
!holds_control(said) && !holds_control(to)
}
pub(crate) fn hyperlink(said: &str, to: &str) -> Option<String> {
openable(said, to).then(|| format!("{OSC_8}{to}{ST}{said}{OSC_8}{ST}"))
}
pub(crate) fn words_of(symbol: &str) -> String {
let mut words = String::new();
let mut rest = symbol;
while let Some(open) = rest.find(ESCAPE) {
words.push_str(&rest[..open]);
rest = match rest[open..].find(ST) {
Some(end) => &rest[open + end + ST.len()..],
None => "",
};
}
words.push_str(rest);
words
}
const ESCAPE: char = '\x1b';
pub(crate) struct Link {
pub(crate) at: usize,
pub(crate) to: String,
}
pub(crate) fn indent() -> String {
" ".repeat(GAP)
}
pub struct Fitted {
identity: Vec<Span<'static>>,
title: Vec<Span<'static>>,
state: Vec<Span<'static>>,
briefly: Option<Vec<Span<'static>>>,
links: Vec<Link>,
whole: Style,
title_or_nothing: bool,
state_or_nothing: bool,
}
impl Fitted {
pub(crate) fn new(
identity: Vec<Span<'static>>,
title: Vec<Span<'static>>,
state: Vec<Span<'static>>,
) -> Self {
Self {
identity,
title,
state,
briefly: None,
links: Vec::new(),
whole: Style::new(),
title_or_nothing: false,
state_or_nothing: false,
}
}
#[must_use]
pub(crate) fn linking(mut self, links: Vec<Link>) -> Self {
self.links = links;
self
}
#[must_use]
pub(crate) fn briefly(mut self, state: Vec<Span<'static>>) -> Self {
self.briefly = Some(state);
self
}
#[must_use]
pub(crate) fn title_or_nothing(mut self) -> Self {
self.title_or_nothing = true;
self
}
#[must_use]
pub(crate) fn state_or_nothing(mut self) -> Self {
self.state_or_nothing = true;
self
}
#[must_use]
pub fn selected(mut self) -> Self {
self.whole = self.whole.patch(palette::SELECTED);
self
}
#[must_use]
pub(crate) fn toned(mut self, tone: Style) -> Self {
self.whole = tone.patch(self.whole);
self
}
}
impl Widget for Fitted {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
let area = Rect { height: 1, ..area };
let width = area.width as usize;
let links = self.links;
let identity = columns(&self.identity);
let (spans, linked) = if identity >= width {
(cut_to(self.identity, width).0, Vec::new())
} else {
let room = width - identity;
let ((title, whole), state) = match self.briefly {
None => {
let (state, _) =
fit(self.state, room.saturating_sub(GAP), self.state_or_nothing);
let left = room - columns(&state) - if state.is_empty() { 0 } else { GAP };
(
fit(self.title, left.saturating_sub(GAP), self.title_or_nothing),
state,
)
}
Some(briefly) => {
let room_for_state = columns(&briefly).min(columns(&self.state)) + GAP;
let (title, whole) = fit(
self.title,
room.saturating_sub(GAP + room_for_state),
self.title_or_nothing,
);
let left = room - columns(&title) - if title.is_empty() { 0 } else { GAP };
let limit = left.saturating_sub(GAP);
let state = if columns(&self.state) <= limit {
self.state
} else {
fit(briefly, limit, self.state_or_nothing).0
};
((title, whole), state)
}
};
let linked = surviving(&links, &title, whole, identity + GAP);
let mut spans = self.identity;
if !title.is_empty() {
spans.push(Span::raw(" ".repeat(GAP)));
spans.extend(title);
}
if !state.is_empty() {
let pad = width.saturating_sub(columns(&spans) + columns(&state));
spans.push(Span::raw(" ".repeat(pad)));
spans.extend(state);
}
(spans, linked)
};
Line::from(spans).style(self.whole).render(area, buf);
for link in linked {
link.told_to(area, buf);
}
}
}
struct Kept {
at: usize,
width: usize,
said: String,
to: String,
}
impl Kept {
fn told_to(self, area: Rect, buf: &mut Buffer) {
let (Ok(at), Some(width)) = (
u16::try_from(self.at),
u16::try_from(self.width).ok().and_then(NonZeroU16::new),
) else {
return;
};
let Some(said) = hyperlink(&self.said, &self.to) else {
return;
};
if let Some(cell) = buf.cell_mut((area.x + at, area.y)) {
cell.set_symbol(&said)
.set_diff_option(CellDiffOption::ForcedWidth(width));
}
}
}
fn surviving(links: &[Link], title: &[Span<'static>], whole: usize, starts: usize) -> Vec<Kept> {
links
.iter()
.filter(|link| link.at < whole)
.map(|link| Kept {
at: starts + columns(&title[..link.at]),
width: title[link.at].width(),
said: title[link.at].content.to_string(),
to: link.to.clone(),
})
.collect()
}
pub(crate) fn cover(frame: &mut Frame, window: Rect) {
frame.render_widget(Clear, window);
let buf = frame.buffer_mut();
for y in window.top()..window.bottom() {
for x in buf.area.left()..window.left() {
let Some(cell) = buf.cell((x, y)) else {
continue;
};
let CellDiffOption::ForcedWidth(width) = cell.diff_option else {
continue;
};
if x.saturating_add(width.get()) <= window.left() {
continue;
}
let words = words_of(cell.symbol());
let style = cell.style();
let room = (window.left() - x) as usize;
if let Some(cell) = buf.cell_mut((x, y)) {
cell.reset();
}
buf.set_stringn(x, y, words, room, style);
}
}
}
pub(crate) fn columns(spans: &[Span<'static>]) -> usize {
spans.iter().map(Span::width).sum()
}
fn fit(spans: Vec<Span<'static>>, limit: usize, or_nothing: bool) -> (Vec<Span<'static>>, usize) {
if or_nothing && columns(&spans) > limit {
return (Vec::new(), 0);
}
cut_to(spans, limit)
}
fn cut_to(spans: Vec<Span<'static>>, limit: usize) -> (Vec<Span<'static>>, usize) {
if columns(&spans) <= limit {
let whole = spans.len();
return (spans, whole);
}
if limit == 0 {
return (Vec::new(), 0);
}
let room = limit - columns(&[Span::raw(CUT.to_string())]);
let mut kept: Vec<Span<'static>> = Vec::new();
let mut whole = 0;
let mut used = 0;
for span in spans {
let width = span.width();
if used + width <= room {
used += width;
whole += 1;
kept.push(span);
continue;
}
let head = head_of(&span.content, room - used);
if !head.is_empty() {
kept.push(Span::styled(head, span.style));
}
break;
}
kept.push(Span::raw(CUT.to_string()));
(kept, whole)
}
fn head_of(text: &str, limit: usize) -> String {
let mut head = String::new();
let mut used = 0;
for glyph in text.chars() {
let width = Span::raw(String::from(glyph)).width();
if used + width > limit {
break;
}
used += width;
head.push(glyph);
}
head
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use ratatui::backend::TestBackend;
use ratatui::widgets::Block;
use ratatui::Terminal;
use crate::view::painted::Painted;
fn a_row() -> Fitted {
Fitted::new(
vec![Span::raw("orb-7")],
vec![Span::raw("a title")],
vec![Span::raw("open")],
)
}
fn blank(width: usize, height: usize) -> Vec<String> {
vec![" ".repeat(width); height]
}
fn drawn(row: Fitted, width: u16) -> String {
Painted::of(row, width, 1).rows().remove(0)
}
#[test]
fn a_title_that_says_nothing_in_part_is_given_up_whole() {
let row = || {
Fitted::new(
vec![Span::raw("orb-7")],
vec![Span::raw("collected 10:22:14")],
vec![Span::raw("open")],
)
.title_or_nothing()
};
assert_eq!(drawn(row(), 31), "orb-7 collected 10:22:14 open");
assert_eq!(drawn(row(), 30), "orb-7 open");
}
#[test]
fn a_title_is_cut_like_any_other_block_unless_it_asks_not_to_be() {
let row = Fitted::new(
vec![Span::raw("orb-7")],
vec![Span::raw("collected 10:22:14")],
vec![Span::raw("open")],
);
assert_eq!(drawn(row, 30), "orb-7 collected 10:22:… open");
}
#[test]
fn a_state_that_says_nothing_in_part_is_given_up_whole() {
let row = || {
Fitted::new(
vec![Span::raw("orb-7")],
vec![Span::raw("a title")],
vec![Span::raw("Enter focus q quit")],
)
.state_or_nothing()
};
assert_eq!(drawn(row(), 27), "orb-7 Enter focus q quit");
assert_eq!(drawn(row(), 26), "orb-7 a title ");
}
#[test]
fn a_state_is_cut_like_any_other_block_unless_it_asks_not_to_be() {
let row = Fitted::new(
vec![Span::raw("orb-7")],
vec![Span::raw("a title")],
vec![Span::raw("Enter focus q quit")],
);
assert_eq!(drawn(row, 26), "orb-7 Enter focus q qu…");
}
#[test]
fn a_band_of_no_rows_is_left_alone() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
a_row().render(
Rect {
x: 0,
y: 1,
width: 20,
height: 0,
},
&mut buf,
);
assert_eq!(Painted::read(&buf).rows(), blank(20, 3));
}
#[test]
fn a_band_of_no_columns_is_left_alone() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
a_row().render(
Rect {
x: 0,
y: 1,
width: 0,
height: 1,
},
&mut buf,
);
assert_eq!(Painted::read(&buf).rows(), blank(20, 3));
}
const SOMEWHERE: &str = "https://forge.invalid/orbital/atlas/pull/12";
fn a_linked_row() -> Fitted {
a_row_linking("⇢ #12")
}
fn a_row_linking(badge: &str) -> Fitted {
Fitted::new(
vec![Span::raw("orb-7")],
vec![
Span::raw("a title"),
Span::raw(" "),
Span::raw(badge.to_string()),
Span::raw(" done"),
],
Vec::new(),
)
.linking(vec![Link {
at: 2,
to: SOMEWHERE.to_string(),
}])
}
fn symbols(buf: &Buffer) -> String {
(buf.area.left()..buf.area.right())
.map(|x| buf[(x, 0)].symbol())
.collect()
}
fn rendered(row: Fitted, width: u16) -> Buffer {
let area = Rect::new(0, 0, width, 1);
let mut buf = Buffer::empty(area);
row.render(area, &mut buf);
buf
}
#[test]
fn a_linked_span_is_wrapped_in_a_hyperlink_where_it_starts() {
let buf = rendered(a_linked_row(), 40);
assert!(
symbols(&buf).contains(
&hyperlink("⇢ #12", SOMEWHERE).expect("this vocabulary holds no control character")
),
"the link is not on the row: {:?}",
symbols(&buf)
);
}
#[test]
fn a_linked_cell_reports_the_width_the_link_takes_on_screen() {
let buf = rendered(a_linked_row(), 40);
let at = opened_at(&buf);
assert_eq!(
buf[(at, 0)].diff_option,
CellDiffOption::ForcedWidth(NonZeroU16::new(5).expect("⇢ #12 is five columns"))
);
}
#[test]
fn a_link_the_cut_stopped_short_of_is_opened_as_it_always_was() {
let said = symbols(&rendered(a_linked_row(), 21));
assert!(
said.contains(&CUT.to_string()),
"the row was not cut at all, so it says nothing about a link \
before the cut: {said:?}"
);
assert!(
said.contains(
&hyperlink("⇢ #12", SOMEWHERE).expect("this vocabulary holds no control character")
),
"a link the cut stopped short of was dropped: {said:?}"
);
}
#[test]
fn a_link_carrying_a_control_character_is_not_opened() {
for to in [
format!("https://forge.invalid{ST}\x1b]52;c;cGF5bG9hZA=={ST}"),
"https://forge.invalid/\nfoo".to_string(),
"https://forge.invalid/\rfoo".to_string(),
] {
let row = Fitted::new(
vec![Span::raw("orb-7")],
vec![Span::raw("a title"), Span::raw(" "), Span::raw("⇢ #12")],
Vec::new(),
)
.linking(vec![Link {
at: 2,
to: to.clone(),
}]);
let said = symbols(&rendered(row, 40));
assert!(
!said.contains(ESCAPE),
"a link naming {to:?} reached the terminal: {said:?}"
);
assert!(
said.contains("⇢ #12"),
"the badge stopped drawing as well as stopped linking: {said:?}"
);
}
}
#[test]
fn a_window_over_a_link_that_started_outside_it_still_draws_its_own_edge() {
const STARTS: u16 = 15;
let window = Rect::new(17, 0, 10, 3);
for (badge, glyph) in [("⇢ #12", "⇢"), ("🔗 #12", "🔗")] {
let mut terminal = Terminal::new(TestBackend::new(40, 3)).expect("a test backend");
let mut draw_row_and = |window: Option<Rect>| {
terminal
.draw(|frame| {
a_row_linking(badge).render(Rect::new(0, 0, 40, 1), frame.buffer_mut());
if let Some(window) = window {
cover(frame, window);
frame.render_widget(Block::bordered(), window);
}
})
.expect("a draw into memory");
};
draw_row_and(None);
draw_row_and(Some(window));
let screen = terminal.backend().buffer().clone();
assert_eq!(
screen[(window.left(), 0)].symbol(),
"┌",
"the window's left edge never reached the terminal, over {badge:?}"
);
assert_eq!(
screen[(STARTS, 0)].symbol(),
glyph,
"the columns the link handed back say nothing the reader can see"
);
}
}
#[test]
fn a_link_cut_for_width_is_not_opened_at_all() {
let said = symbols(&rendered(a_linked_row(), 20));
assert!(
!said.contains(OSC_8),
"a cut link opened a hyperlink: {said:?}"
);
}
#[test]
fn a_partial_redraw_cannot_send_an_opening_sequence_without_its_closer() {
let moved = || {
Fitted::new(
vec![Span::raw("orb-7")],
vec![Span::raw("a title"), Span::raw(" "), Span::raw("→ #12")],
Vec::new(),
)
.linking(vec![Link {
at: 2,
to: SOMEWHERE.to_string(),
}])
};
let before = rendered(a_linked_row(), 40);
let after = rendered(moved(), 40);
let sent: Vec<&str> = before
.diff(&after)
.into_iter()
.map(|(_, _, cell)| cell.symbol())
.collect();
let opened: Vec<&&str> = sent.iter().filter(|said| said.contains(OSC_8)).collect();
assert!(!opened.is_empty(), "the moved link was not sent: {sent:?}");
for said in opened {
assert!(
said.ends_with(&format!("{OSC_8}{ST}")),
"an opening sequence went without its closer: {said:?}"
);
}
}
fn opened_at(buf: &Buffer) -> u16 {
(buf.area.left()..buf.area.right())
.find(|&x| buf[(x, 0)].symbol().starts_with(OSC_8))
.expect("a cell opening a hyperlink")
}
#[test]
fn a_band_of_several_rows_is_given_one() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
a_row().render(Rect::new(0, 0, 20, 3), &mut buf);
assert_eq!(Painted::read(&buf).rows()[1..], blank(20, 2));
}
}