use std::cmp::Reverse;
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';
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Block {
Identity,
Title,
State,
}
pub(crate) struct Link {
pub(crate) block: Block,
pub(crate) at: usize,
pub(crate) to: String,
}
pub(crate) struct Shorter {
pub(crate) block: Block,
pub(crate) at: usize,
pub(crate) said: String,
}
struct Briefly {
spans: Vec<Span<'static>>,
links: Vec<Link>,
shorter: Vec<Shorter>,
}
pub(crate) fn indent() -> String {
" ".repeat(GAP)
}
pub struct Fitted {
identity: Vec<Span<'static>>,
title: Vec<Span<'static>>,
state: Vec<Span<'static>>,
briefly: Option<Briefly>,
shorter: Vec<Shorter>,
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,
shorter: Vec::new(),
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>>,
links: Vec<Link>,
shorter: Vec<Shorter>,
) -> Self {
self.briefly = Some(Briefly {
spans: state,
links,
shorter,
});
self
}
#[must_use]
pub(crate) fn shortening(mut self, shorter: Vec<Shorter>) -> Self {
self.shorter = shorter;
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 shorter = self.shorter;
let identity = shortened(self.identity, &shorter, Block::Identity, width);
let (spans, linked) = if columns(&identity) >= width {
let (identity, drawn) = cut_to(identity, width);
let linked = surviving(&links, Block::Identity, &identity, drawn, 0);
(identity, linked)
} else {
let room = width - columns(&identity);
let state_links = |state: &[Span<'static>], drawn: usize, links: &[Link]| {
surviving(links, Block::State, state, drawn, width - columns(state))
};
let ((title, title_drawn), (state, state_linked)) = match self.briefly {
None => {
let limit = room.saturating_sub(GAP);
let (state, state_drawn) = fit(
shortened(self.state, &shorter, Block::State, limit),
limit,
self.state_or_nothing,
);
let left = room - columns(&state) - if state.is_empty() { 0 } else { GAP };
let limit = left.saturating_sub(GAP);
let linked = state_links(&state, state_drawn, &links);
(
fit(
shortened(self.title, &shorter, Block::Title, limit),
limit,
self.title_or_nothing,
),
(state, linked),
)
}
Some(briefly) => {
let shortest = shortened(self.state.clone(), &shorter, Block::State, 0);
let briefest =
shortened(briefly.spans.clone(), &briefly.shorter, Block::State, 0);
let room_for_state = columns(&briefest).min(columns(&shortest)) + GAP;
let limit = room.saturating_sub(GAP + room_for_state);
let (title, title_drawn) = fit(
shortened(self.title, &shorter, Block::Title, limit),
limit,
self.title_or_nothing,
);
let left = room - columns(&title) - if title.is_empty() { 0 } else { GAP };
let limit = left.saturating_sub(GAP);
let state = shortened(self.state, &shorter, Block::State, limit);
let state = if columns(&state) <= limit {
let linked = state_links(&state, state.len(), &links);
(state, linked)
} else {
let (state, drawn) = fit(
shortened(briefly.spans, &briefly.shorter, Block::State, limit),
limit,
self.state_or_nothing,
);
let linked = state_links(&state, drawn, &briefly.links);
(state, linked)
};
((title, title_drawn), state)
}
};
let mut linked = surviving(&links, Block::Identity, &identity, identity.len(), 0);
linked.extend(surviving(
&links,
Block::Title,
&title,
title_drawn,
columns(&identity) + GAP,
));
linked.extend(state_linked);
let mut spans = 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],
block: Block,
spans: &[Span<'static>],
drawn: usize,
starts: usize,
) -> Vec<Kept> {
links
.iter()
.filter(|link| link.block == block && link.at < drawn)
.map(|link| Kept {
at: starts + columns(&spans[..link.at]),
width: spans[link.at].width(),
said: spans[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 shortened(
mut spans: Vec<Span<'static>>,
shorter: &[Shorter],
block: Block,
limit: usize,
) -> Vec<Span<'static>> {
let saving = |swap: &Shorter| {
spans.get(swap.at).map_or(0, |span| {
span.width()
.saturating_sub(Span::raw(swap.said.as_str()).width())
})
};
let mut order: Vec<&Shorter> = shorter.iter().filter(|swap| swap.block == block).collect();
order.sort_by_key(|swap| (Reverse(saving(swap)), Reverse(swap.at)));
for swap in order {
if columns(&spans) <= limit {
break;
}
if let Some(span) = spans.get_mut(swap.at) {
*span = Span::styled(swap.said.clone(), span.style);
}
}
spans
}
fn cut_to(spans: Vec<Span<'static>>, limit: usize) -> (Vec<Span<'static>>, usize) {
if columns(&spans) <= limit {
let drawn = spans.len();
return (spans, drawn);
}
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 drawn = 0;
let mut used = 0;
for span in spans {
let width = span.width();
if used + width <= room {
used += width;
drawn += 1;
kept.push(span);
continue;
}
let head = head_of(&span.content, room - used);
if !head.is_empty() {
drawn += 1;
kept.push(Span::styled(head, span.style));
}
break;
}
kept.push(Span::raw(CUT.to_string()));
(kept, drawn)
}
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 as Bordered;
use ratatui::Terminal;
use crate::view::painted::Painted;
fn a_row() -> Fitted {
Fitted::new(
vec![Span::raw("dun-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("dun-7")],
vec![Span::raw("collected 10:22:14")],
vec![Span::raw("open")],
)
.title_or_nothing()
};
assert_eq!(drawn(row(), 31), "dun-7 collected 10:22:14 open");
assert_eq!(drawn(row(), 30), "dun-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("dun-7")],
vec![Span::raw("collected 10:22:14")],
vec![Span::raw("open")],
);
assert_eq!(drawn(row, 30), "dun-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("dun-7")],
vec![Span::raw("a title")],
vec![Span::raw("Enter focus q quit")],
)
.state_or_nothing()
};
assert_eq!(drawn(row(), 27), "dun-7 Enter focus q quit");
assert_eq!(drawn(row(), 26), "dun-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("dun-7")],
vec![Span::raw("a title")],
vec![Span::raw("Enter focus q quit")],
);
assert_eq!(drawn(row, 26), "dun-7 Enter focus q qu…");
}
fn a_row_saying(title: &str) -> Fitted {
Fitted::new(
vec![Span::raw("dun-7")],
vec![Span::raw(title.to_string())],
vec![Span::raw("working on the parser")],
)
.briefly(vec![Span::raw("working")], Vec::new(), Vec::new())
}
#[test]
fn a_state_with_no_room_for_its_long_form_is_said_in_its_short_one() {
assert_eq!(
drawn(a_row_saying("a title"), 37),
"dun-7 a title working on the parser"
);
assert_eq!(
drawn(a_row_saying("a title"), 36),
"dun-7 a title working"
);
}
#[test]
fn a_title_cut_for_width_leaves_the_short_form_the_room_kept_for_it() {
assert_eq!(
drawn(a_row_saying("teach the elided run to fold back open"), 40),
"dun-7 teach the elided run to… working"
);
}
fn a_shortenable_row() -> Fitted {
Fitted::new(
vec![Span::raw("dun-7")],
vec![
Span::raw("a title"),
Span::raw(" "),
Span::raw("⇢ awaiting review"),
],
Vec::new(),
)
.shortening(vec![Shorter {
block: Block::Title,
at: 2,
said: "⇢ #12".to_string(),
}])
}
#[test]
fn a_span_with_no_room_for_its_long_form_is_said_in_its_short_one() {
assert_eq!(
drawn(a_shortenable_row(), 33),
"dun-7 a title ⇢ awaiting review"
);
assert_eq!(
drawn(a_shortenable_row(), 32),
"dun-7 a title ⇢ #12 "
);
}
#[test]
fn a_span_offering_no_short_form_is_cut_as_it_always_was() {
let row = Fitted::new(
vec![Span::raw("dun-7")],
vec![
Span::raw("a title"),
Span::raw(" "),
Span::raw("⇢ awaiting review"),
],
Vec::new(),
);
assert_eq!(drawn(row, 32), "dun-7 a title ⇢ awaiting revi…");
}
#[test]
fn a_span_said_in_its_short_form_keeps_the_link_the_long_one_had() {
let row = a_shortenable_row().linking(vec![Link {
block: Block::Title,
at: 2,
to: SOMEWHERE.to_string(),
}]);
let said = symbols(&rendered(row, 32));
assert!(
said.contains(
&hyperlink("⇢ #12", SOMEWHERE).expect("this vocabulary holds no control character")
),
"the short form was drawn without the link the long one had: {said:?}"
);
}
fn a_row_of_two_shortenable_spans() -> Fitted {
Fitted::new(
vec![Span::raw("dun-7")],
vec![
Span::raw("a title"),
Span::raw(" "),
Span::raw("awaiting review"),
Span::raw(" "),
Span::raw("blocked on the tracker"),
],
Vec::new(),
)
.shortening(vec![
Shorter {
block: Block::Title,
at: 2,
said: "#12".to_string(),
},
Shorter {
block: Block::Title,
at: 4,
said: "blocked".to_string(),
},
])
}
#[test]
fn no_more_spans_shorten_than_the_row_has_to_shorten_to_fit() {
assert_eq!(
drawn(a_row_of_two_shortenable_spans(), 55),
"dun-7 a title awaiting review blocked on the tracker"
);
assert_eq!(
drawn(a_row_of_two_shortenable_spans(), 40),
"dun-7 a title awaiting review blocked"
);
assert_eq!(
drawn(a_row_of_two_shortenable_spans(), 39),
"dun-7 a title #12 blocked "
);
}
fn a_shortenable_row_saying_briefly() -> Fitted {
Fitted::new(
vec![Span::raw("dun-7")],
vec![
Span::raw("a title"),
Span::raw(" "),
Span::raw("⇢ awaiting review"),
],
vec![Span::raw("working on the parser")],
)
.briefly(vec![Span::raw("working")], Vec::new(), Vec::new())
.shortening(vec![Shorter {
block: Block::Title,
at: 2,
said: "⇢ #12".to_string(),
}])
}
#[test]
fn a_span_keeps_its_long_form_where_the_state_block_can_swap_instead() {
assert_eq!(
drawn(a_shortenable_row_saying_briefly(), 56),
"dun-7 a title ⇢ awaiting review working on the parser"
);
assert_eq!(
drawn(a_shortenable_row_saying_briefly(), 55),
"dun-7 a title ⇢ awaiting review working"
);
}
#[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/dunwich/arkham/pull/12";
fn a_linked_row() -> Fitted {
a_row_linking("⇢ #12")
}
fn a_row_linking(badge: &str) -> Fitted {
Fitted::new(
vec![Span::raw("dun-7")],
vec![
Span::raw("a title"),
Span::raw(" "),
Span::raw(badge.to_string()),
Span::raw(" done"),
],
Vec::new(),
)
.linking(vec![Link {
block: Block::Title,
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("dun-7")],
vec![Span::raw("a title"), Span::raw(" "), Span::raw("⇢ #12")],
Vec::new(),
)
.linking(vec![Link {
block: Block::Title,
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(Bordered::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_opened_round_the_head_it_kept() {
let buf = rendered(a_linked_row(), 20);
let said = symbols(&buf);
assert!(
said.contains(CUT),
"the row was not cut at all, so it says nothing about a cut \
link: {said:?}"
);
assert!(
said.contains(
&hyperlink("⇢ #1", SOMEWHERE).expect("this vocabulary holds no control character")
),
"a link the row cut was dropped: {said:?}"
);
assert_eq!(
buf[(opened_at(&buf), 0)].diff_option,
CellDiffOption::ForcedWidth(NonZeroU16::new(4).expect("⇢ #1 is four columns"))
);
}
#[test]
fn a_link_the_cut_left_no_room_at_all_is_not_opened() {
let said = symbols(&rendered(a_linked_row(), 16));
assert!(
!said.contains(OSC_8),
"a link the row cut to nothing opened a hyperlink: {said:?}"
);
}
#[test]
fn a_partial_redraw_cannot_send_an_opening_sequence_without_its_closer() {
let moved = || {
Fitted::new(
vec![Span::raw("dun-7")],
vec![Span::raw("a title"), Span::raw(" "), Span::raw("→ #12")],
Vec::new(),
)
.linking(vec![Link {
block: Block::Title,
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:?}"
);
}
}
#[test]
fn a_link_in_the_identity_is_opened_at_the_column_it_starts_on() {
let row = Fitted::new(
vec![Span::raw("dun-7"), Span::raw(" "), Span::raw("⇢ #12")],
vec![Span::raw("a title")],
Vec::new(),
)
.linking(vec![Link {
block: Block::Identity,
at: 2,
to: SOMEWHERE.to_string(),
}]);
let buf = rendered(row, 40);
assert_eq!(drawn_of(&buf), "dun-7 ⇢ #12 a title ");
assert_eq!(opened_at(&buf), 6);
}
#[test]
fn a_link_in_a_right_justified_state_is_opened_where_it_was_drawn() {
let row = Fitted::new(
vec![Span::raw("dun-7")],
vec![Span::raw("a title")],
vec![Span::raw("open"), Span::raw(" "), Span::raw("⇢ #12")],
)
.linking(vec![Link {
block: Block::State,
at: 2,
to: SOMEWHERE.to_string(),
}]);
let buf = rendered(row, 40);
assert_eq!(drawn_of(&buf), "dun-7 a title open ⇢ #12");
assert_eq!(opened_at(&buf), 35);
}
#[test]
fn a_span_in_the_identity_with_no_room_for_its_long_form_is_said_in_its_short_one() {
let row = || {
Fitted::new(
vec![
Span::raw("dun-7"),
Span::raw(" "),
Span::raw("⇢ awaiting review"),
],
vec![Span::raw("a title")],
Vec::new(),
)
.shortening(vec![Shorter {
block: Block::Identity,
at: 2,
said: "⇢ #12".to_string(),
}])
};
assert_eq!(drawn(row(), 23), "dun-7 ⇢ awaiting review");
assert_eq!(drawn(row(), 22), "dun-7 ⇢ #12 a title ");
}
#[test]
fn a_span_in_the_state_with_no_room_for_its_long_form_is_said_in_its_short_one() {
let row = || {
Fitted::new(
vec![Span::raw("dun-7")],
vec![Span::raw("a title")],
vec![
Span::raw("open"),
Span::raw(" "),
Span::raw("⇢ awaiting review"),
],
)
.shortening(vec![Shorter {
block: Block::State,
at: 2,
said: "⇢ #12".to_string(),
}])
};
assert_eq!(drawn(row(), 29), "dun-7 open ⇢ awaiting review");
assert_eq!(drawn(row(), 28), "dun-7 a title open ⇢ #12");
}
fn drawn_of(buf: &Buffer) -> String {
Painted::read(buf).rows().remove(0)
}
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));
}
}