From fd30b1ed80fe81306d30d0e61ac4e3085a900d83 Mon Sep 17 00:00:00 2001 From: Erik Kundt Date: Mon, 3 Jun 2024 16:22:10 +0200 Subject: [PATCH] issue: Adjust new API --- bin/commands/issue/select.rs | 320 ++++++++++++++++++++- bin/commands/issue/select/ui.rs | 485 +++----------------------------- 2 files changed, 338 insertions(+), 467 deletions(-) diff --git a/bin/commands/issue/select.rs b/bin/commands/issue/select.rs index 87e9ad94..6bd6682c 100644 --- a/bin/commands/issue/select.rs +++ b/bin/commands/issue/select.rs @@ -5,6 +5,12 @@ use std::str::FromStr; use anyhow::Result; +use termion::event::Key; + +use ratatui::layout::Constraint; +use ratatui::style::Stylize; +use ratatui::text::{Line, Span, Text}; + use radicle::issue::IssueId; use radicle::storage::git::Repository; use radicle::Profile; @@ -15,12 +21,15 @@ use tui::cob::issue; use tui::store; use tui::store::StateValue; use tui::ui::items::{Filter, IssueItem, IssueItemFilter}; -use tui::ui::widget::window::{Window, WindowProps}; -use tui::ui::widget::ToWidget; +use tui::ui::span; +use tui::ui::widget::container::{Column, Container, Footer, FooterProps, Header, HeaderProps}; +use tui::ui::widget::text::{Paragraph, ParagraphProps}; +use tui::ui::widget::window::{Page, PageProps, Shortcuts, ShortcutsProps, Window, WindowProps}; +use tui::ui::widget::{ToWidget, Widget}; use tui::{BoxedAny, Channel, Exit, PageStack}; -use self::ui::{BrowserPage, HelpPage}; +use self::ui::{Browser, BrowserProps}; use super::common::Mode; @@ -38,8 +47,8 @@ pub struct App { } #[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum Page { - Browse, +pub enum AppPage { + Browser, Help, } @@ -72,7 +81,7 @@ pub struct HelpState { #[derive(Clone, Debug)] pub struct State { mode: Mode, - pages: PageStack, + pages: PageStack, browser: BrowserState, help: HelpState, } @@ -96,7 +105,7 @@ impl TryFrom<&Context> for State { Ok(Self { mode: context.mode.clone(), - pages: PageStack::new(vec![Page::Browse]), + pages: PageStack::new(vec![AppPage::Browser]), browser: BrowserState { items, selected: Some(0), @@ -176,7 +185,7 @@ impl store::State for State { None } Message::OpenHelp => { - self.pages.push(Page::Help); + self.pages.push(AppPage::Help); None } Message::LeavePage => { @@ -204,15 +213,12 @@ impl App { let tx = channel.tx.clone(); let window = Window::default() - .page( - Page::Browse, - BrowserPage::new(tx.clone()).to_widget(tx.clone()), - ) - .page(Page::Help, HelpPage::new(tx.clone()).to_widget(tx.clone())) + .page(AppPage::Browser, browser_page(&state, &channel)) + .page(AppPage::Help, help_page(&state, &channel)) .to_widget(tx.clone()) .on_update(|state| { WindowProps::default() - .current_page(state.pages.peek().unwrap_or(&Page::Browse).clone()) + .current_page(state.pages.peek().unwrap_or(&AppPage::Browser).clone()) .to_boxed_any() .into() }); @@ -220,3 +226,289 @@ impl App { tui::run(channel, state, window).await } } + +fn browser_page(_state: &State, channel: &Channel) -> Widget { + let tx = channel.tx.clone(); + + let content = Browser::new(tx.clone()) + .to_widget(tx.clone()) + .on_update(|state| BrowserProps::from(state).to_boxed_any().into()); + + let shortcuts = Shortcuts::default() + .to_widget(tx.clone()) + .on_update(|state: &State| { + let shortcuts = if state.browser.show_search { + vec![("esc", "cancel"), ("enter", "apply")] + } else { + match state.mode { + Mode::Id => vec![("enter", "select"), ("/", "search")], + Mode::Operation => vec![ + ("enter", "show"), + ("e", "edit"), + ("/", "search"), + ("?", "help"), + ], + } + }; + + ShortcutsProps::default() + .shortcuts(&shortcuts) + .to_boxed_any() + .into() + }); + + Page::default() + .content(content) + .shortcuts(shortcuts) + .to_widget(tx.clone()) + .on_event(|key, _, props| { + let default = PageProps::default(); + let props = props + .and_then(|props| props.inner_ref::()) + .unwrap_or(&default); + + if props.handle_keys { + match key { + Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }), + Key::Char('?') => Some(Message::OpenHelp), + _ => None, + } + } else { + None + } + }) + .on_update(|state: &State| { + PageProps::default() + .page_size(state.browser.page_size) + .handle_keys(!state.browser.show_search) + .to_boxed_any() + .into() + }) + .on_render(|props, render| { + let default = PageProps::default(); + let props = props + .and_then(|props| props.inner_ref::()) + .unwrap_or(&default); + let page_size = render.area.height.saturating_sub(6) as usize; + + if page_size != props.page_size { + return Some(Message::BrowserPageSize(page_size)); + } + None + }) +} + +fn help_page(_state: &State, channel: &Channel) -> Widget { + let tx = channel.tx.clone(); + + let content = Container::default() + .header(Header::default().to_widget(tx.clone()).on_update(|_| { + HeaderProps::default() + .columns([Column::new(" Help ", Constraint::Fill(1))].to_vec()) + .to_boxed_any() + .into() + })) + .content( + Paragraph::default() + .to_widget(tx.clone()) + .on_event(|_, s, _| { + Some(Message::ScrollHelp { + progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(), + }) + }) + .on_update(|state: &State| { + ParagraphProps::default() + .text(&help_text()) + .page_size(state.help.page_size) + .to_boxed_any() + .into() + }), + ) + .footer( + Footer::default() + .to_widget(tx.clone()) + .on_update(|state: &State| { + FooterProps::default() + .columns( + [ + Column::new(Text::raw(""), Constraint::Fill(1)), + Column::new( + span::default(&format!("{}%", state.help.progress)).dim(), + Constraint::Min(4), + ), + ] + .to_vec(), + ) + .to_boxed_any() + .into() + }), + ) + .to_widget(tx.clone()); + + let shortcuts = Shortcuts::default().to_widget(tx.clone()).on_update(|_| { + ShortcutsProps::default() + .shortcuts(&[("?", "close")]) + .to_boxed_any() + .into() + }); + + Page::default() + .content(content) + .shortcuts(shortcuts) + .to_widget(tx.clone()) + .on_event(|key, _, _| match key { + Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }), + Key::Char('?') => Some(Message::LeavePage), + _ => None, + }) + .on_update(|state: &State| { + PageProps::default() + .page_size(state.help.page_size) + .handle_keys(true) + .to_boxed_any() + .into() + }) + .on_render(|props, render| { + let default = PageProps::default(); + let props = props + .and_then(|props| props.inner_ref::()) + .unwrap_or(&default); + let page_size = render.area.height.saturating_sub(6) as usize; + + if page_size != props.page_size { + return Some(Message::HelpPageSize(page_size)); + } + None + }) +} + +fn help_text() -> Text<'static> { + Text::from( + [ + Line::from(Span::raw("Generic keybindings").cyan()), + Line::raw(""), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "↑,k")).gray(), + Span::raw(" "), + Span::raw("move cursor one line up").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "↓,j")).gray(), + Span::raw(" "), + Span::raw("move cursor one line down").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "PageUp")).gray(), + Span::raw(" "), + Span::raw("move cursor one page up").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "PageDown")).gray(), + Span::raw(" "), + Span::raw("move cursor one page down").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "Home")).gray(), + Span::raw(" "), + Span::raw("move cursor to the first line").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "End")).gray(), + Span::raw(" "), + Span::raw("move cursor to the last line").gray().dim(), + ] + .to_vec(), + ), + Line::raw(""), + Line::from(Span::raw("Specific keybindings").cyan()), + Line::raw(""), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "enter")).gray(), + Span::raw(" "), + Span::raw("Select issue (if --mode id)").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "enter")).gray(), + Span::raw(" "), + Span::raw("Show issue").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "e")).gray(), + Span::raw(" "), + Span::raw("Edit patch").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "/")).gray(), + Span::raw(" "), + Span::raw("Search").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "?")).gray(), + Span::raw(" "), + Span::raw("Show help").gray().dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "Esc")).gray(), + Span::raw(" "), + Span::raw("Quit / cancel").gray().dim(), + ] + .to_vec(), + ), + Line::raw(""), + Line::from(Span::raw("Searching").cyan()), + Line::raw(""), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "Pattern")).gray(), + Span::raw(" "), + Span::raw("is: | is:authored | is:assigned | authors:[, ...] | assignees:[, ...] | ") + .gray() + .dim(), + ] + .to_vec(), + ), + Line::from( + [ + Span::raw(format!("{key:>10}", key = "Example")).gray(), + Span::raw(" "), + Span::raw("is:solved is:authored alias").gray().dim(), + ] + .to_vec(), + ), + Line::raw(""), + Line::raw(""), + ] + .to_vec()) +} diff --git a/bin/commands/issue/select/ui.rs b/bin/commands/issue/select/ui.rs index 8c8a1dad..e1a54369 100644 --- a/bin/commands/issue/select/ui.rs +++ b/bin/commands/issue/select/ui.rs @@ -10,21 +10,19 @@ use termion::event::Key; use ratatui::layout::{Constraint, Layout}; use ratatui::style::Stylize; -use ratatui::text::{Line, Span, Text}; +use ratatui::text::{Line, Text}; use radicle_tui as tui; use tui::ui::items::{IssueItem, IssueItemFilter}; use tui::ui::span; +use tui::ui::widget; use tui::ui::widget::container::{ - Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, SectionGroup, - SectionGroupProps, + Column, Container, ContainerProps, Footer, FooterProps, Header, HeaderProps, }; use tui::ui::widget::input::{TextField, TextFieldProps}; use tui::ui::widget::list::{Table, TableProps, TableUtils}; -use tui::ui::widget::text::{Paragraph, ParagraphProps}; -use tui::ui::widget::window::{Shortcuts, ShortcutsProps}; -use tui::ui::widget::{self, ViewProps}; +use tui::ui::widget::ViewProps; use tui::ui::widget::{RenderProps, ToWidget, View}; use tui::{BoxedAny, Selection}; @@ -37,7 +35,7 @@ use super::{Message, State}; type Widget = widget::Widget; #[derive(Clone, Default)] -struct BrowserProps<'a> { +pub struct BrowserProps<'a> { /// Application mode: openation and id or id only. mode: Mode, /// Filtered issues. @@ -137,15 +135,18 @@ pub struct Browser { } impl Browser { - fn new(tx: UnboundedSender) -> Self { + pub fn new(tx: UnboundedSender) -> Self { Self { issues: Container::default() - .header( - Header::default() - // .columns(props.header.clone()) - // .cutoff(props.cutoff, props.cutoff_after) - .to_widget(tx.clone()), - ) + .header(Header::default().to_widget(tx.clone()).on_update(|state| { + // TODO: remove and use state directly + let props = BrowserProps::from(state); + HeaderProps::default() + .columns(props.header.clone()) + .cutoff(props.cutoff, props.cutoff_after) + .to_boxed_any() + .into() + })) .content( Table::::default() .to_widget(tx.clone()) @@ -183,7 +184,23 @@ impl Browser { .to_boxed_any() .into() }), - search: Search::new(tx.clone()).to_widget(tx.clone()), + search: TextField::default() + .to_widget(tx.clone()) + .on_event(|key, s, _| match key { + Key::Esc => Some(Message::CloseSearch), + Key::Char('\n') => Some(Message::ApplySearch), + _ => Some(Message::UpdateSearch { + value: s.and_then(|i| i.unwrap_string()).unwrap_or_default(), + }), + }) + .on_update(|state: &State| { + TextFieldProps::default() + .text(&state.browser.search.read().to_string()) + .title("Search") + .inline(true) + .to_boxed_any() + .into() + }), } } } @@ -263,313 +280,6 @@ impl View for Browser { } } -#[derive(Clone, Default)] -struct BrowserPageProps<'a> { - /// Current page size (height of table content). - page_size: usize, - /// If this pages' keys should be handled (`false` if search is shown). - handle_keys: bool, - /// This pages' shortcuts. - shortcuts: Vec<(&'a str, &'a str)>, -} - -impl<'a> From<&State> for BrowserPageProps<'a> { - fn from(state: &State) -> Self { - Self { - page_size: state.browser.page_size, - handle_keys: !state.browser.show_search, - shortcuts: if state.browser.show_search { - vec![("esc", "cancel"), ("enter", "apply")] - } else { - match state.mode { - Mode::Id => vec![("enter", "select"), ("/", "search")], - Mode::Operation => vec![ - ("enter", "show"), - ("e", "edit"), - ("/", "search"), - ("?", "help"), - ], - } - }, - } - } -} - -pub struct BrowserPage<'a> { - /// Internal props - props: BrowserPageProps<'a>, - /// Sections widget - sections: Widget, - /// Shortcut widget - shortcuts: Widget, -} - -impl<'a: 'static> BrowserPage<'a> { - pub fn new(tx: UnboundedSender) -> Self { - Self { - props: BrowserPageProps::default(), - sections: SectionGroup::default() - .section(Browser::new(tx.clone()).to_widget(tx.clone())) - .to_widget(tx.clone()) - .on_update(|state| { - let props = BrowserPageProps::from(state); - SectionGroupProps::default() - .handle_keys(props.handle_keys) - .to_boxed_any() - .into() - }), - shortcuts: Shortcuts::default() - .to_widget(tx.clone()) - .on_update(|state| { - ShortcutsProps::default() - .shortcuts(&BrowserPageProps::from(state).shortcuts) - .to_boxed_any() - .into() - }), - } - } -} - -impl<'a: 'static> View for BrowserPage<'a> { - type Message = Message; - type State = State; - - fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option { - self.sections.handle_event(key); - - if self.props.handle_keys { - return match key { - Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }), - Key::Char('?') => Some(Message::OpenHelp), - _ => None, - }; - } - - None - } - - fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) { - self.sections.update(state); - self.shortcuts.update(state); - } - - fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) { - let page_size = render.area.height.saturating_sub(6) as usize; - - let [content_area, shortcuts_area] = - Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area); - - self.sections.render( - RenderProps::from(content_area) - .layout(Layout::horizontal([Constraint::Min(1)])) - .focus(true), - frame, - ); - self.shortcuts - .render(RenderProps::from(shortcuts_area), frame); - - // TODO: Find better solution - if page_size != self.props.page_size { - self.sections.send(Message::BrowserPageSize(page_size)); - } - } -} - -#[derive(Clone)] -pub struct SearchProps {} - -pub struct Search { - /// Internal props - props: SearchProps, - /// Search input field - input: Widget, -} - -impl Search { - fn new(tx: UnboundedSender) -> Self - where - Self: Sized, - { - Self { - props: SearchProps {}, - input: TextField::default() - .to_widget(tx.clone()) - .on_event(|_, s, _| { - Some(Message::UpdateSearch { - value: s.and_then(|i| i.unwrap_string()).unwrap_or_default(), - }) - }) - .on_update(|state: &State| { - TextFieldProps::default() - .text(&state.browser.search.read().to_string()) - .title("Search") - .inline(true) - .to_boxed_any() - .into() - }), - } - } -} - -impl View for Search { - type Message = Message; - type State = State; - - fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option { - match key { - Key::Esc => Some(Message::CloseSearch), - Key::Char('\n') => Some(Message::ApplySearch), - _ => { - self.input.handle_event(key); - None - } - } - } - - fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) { - self.input.update(state); - } - - fn render(&self, _props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) { - let layout = Layout::horizontal(Constraint::from_mins([0])) - .horizontal_margin(1) - .split(render.area); - - self.input.render(RenderProps::from(layout[0]), frame); - } -} - -#[derive(Clone, Default)] -struct HelpPageProps<'a> { - /// Current page size (height of table content). - page_size: usize, - /// Scroll progress of help paragraph. - help_progress: usize, - /// This pages' shortcuts. - shortcuts: Vec<(&'a str, &'a str)>, -} - -impl<'a> From<&State> for HelpPageProps<'a> { - fn from(state: &State) -> Self { - Self { - page_size: state.help.page_size, - help_progress: state.help.progress, - shortcuts: vec![("?", "close")], - } - } -} - -pub struct HelpPage { - /// Content widget - content: Widget, - /// Shortcut widget - shortcuts: Widget, -} - -impl HelpPage { - pub fn new(tx: UnboundedSender) -> Self - where - Self: Sized, - { - Self { - content: Container::default() - .header(Header::default().to_widget(tx.clone()).on_update(|_| { - HeaderProps::default() - .columns([Column::new(" Help ", Constraint::Fill(1))].to_vec()) - .to_boxed_any() - .into() - })) - .content( - Paragraph::default() - .to_widget(tx.clone()) - .on_event(|_, s, _| { - Some(Message::ScrollHelp { - progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(), - }) - }) - .on_update(|state| { - let props = HelpPageProps::from(state); - - ParagraphProps::default() - .text(&help_text()) - .page_size(props.page_size) - .to_boxed_any() - .into() - }), - ) - .footer(Footer::default().to_widget(tx.clone()).on_update(|state| { - let props = HelpPageProps::from(state); - - FooterProps::default() - .columns( - [ - Column::new(Text::raw(""), Constraint::Fill(1)), - Column::new( - span::default(&format!("{}%", props.help_progress)).dim(), - Constraint::Min(4), - ), - ] - .to_vec(), - ) - .to_boxed_any() - .into() - })) - .to_widget(tx.clone()), - shortcuts: Shortcuts::default() - .to_widget(tx.clone()) - .on_update(|state| { - ShortcutsProps::default() - .shortcuts(&HelpPageProps::from(state).shortcuts) - .to_boxed_any() - .into() - }), - } - } -} - -impl View for HelpPage { - type Message = Message; - type State = State; - - fn handle_event(&mut self, _props: Option<&ViewProps>, key: Key) -> Option { - match key { - Key::Esc | Key::Ctrl('c') => Some(Message::Exit { selection: None }), - Key::Char('?') => Some(Message::LeavePage), - _ => { - self.content.handle_event(key); - None - } - } - } - - fn update(&mut self, _props: Option<&ViewProps>, state: &Self::State) { - self.content.update(state); - self.shortcuts.update(state); - } - - fn render(&self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) { - let default = HelpPageProps::default(); - let props = props - .and_then(|props| props.inner_ref::()) - .unwrap_or(&default); - - let page_size = render.area.height.saturating_sub(6) as usize; - - let [content_area, shortcuts_area] = - Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(render.area); - - self.content - .render(RenderProps::from(content_area).focus(true), frame); - self.shortcuts - .render(RenderProps::from(content_area).focus(true), frame); - - // TODO: Find better solution - if page_size != props.page_size { - self.content.send(Message::HelpPageSize(page_size)); - } - } -} - fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option) -> Vec> { let search = Line::from(vec![ span::default(" Search ").cyan().dim().reversed(), @@ -644,134 +354,3 @@ fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option) -> Vec Text<'static> { - Text::from( - [ - Line::from(Span::raw("Generic keybindings").cyan()), - Line::raw(""), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "↑,k")).gray(), - Span::raw(" "), - Span::raw("move cursor one line up").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "↓,j")).gray(), - Span::raw(" "), - Span::raw("move cursor one line down").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "PageUp")).gray(), - Span::raw(" "), - Span::raw("move cursor one page up").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "PageDown")).gray(), - Span::raw(" "), - Span::raw("move cursor one page down").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "Home")).gray(), - Span::raw(" "), - Span::raw("move cursor to the first line").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "End")).gray(), - Span::raw(" "), - Span::raw("move cursor to the last line").gray().dim(), - ] - .to_vec(), - ), - Line::raw(""), - Line::from(Span::raw("Specific keybindings").cyan()), - Line::raw(""), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "enter")).gray(), - Span::raw(" "), - Span::raw("Select issue (if --mode id)").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "enter")).gray(), - Span::raw(" "), - Span::raw("Show issue").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "e")).gray(), - Span::raw(" "), - Span::raw("Edit patch").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "/")).gray(), - Span::raw(" "), - Span::raw("Search").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "?")).gray(), - Span::raw(" "), - Span::raw("Show help").gray().dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "Esc")).gray(), - Span::raw(" "), - Span::raw("Quit / cancel").gray().dim(), - ] - .to_vec(), - ), - Line::raw(""), - Line::from(Span::raw("Searching").cyan()), - Line::raw(""), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "Pattern")).gray(), - Span::raw(" "), - Span::raw("is: | is:authored | is:assigned | authors:[, ...] | assignees:[, ...] | ") - .gray() - .dim(), - ] - .to_vec(), - ), - Line::from( - [ - Span::raw(format!("{key:>10}", key = "Example")).gray(), - Span::raw(" "), - Span::raw("is:solved is:authored alias").gray().dim(), - ] - .to_vec(), - ), - Line::raw(""), - Line::raw(""), - ] - .to_vec()) -}