1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Duration;
6
7use bevy_app::App;
8use bevy_ecs::prelude::*;
9use bevy_ecs::system::SystemParam;
10use bevy_log::warn;
11use ratatui::style::{Style, Stylize};
12use ratatui::text::{Line, Span};
13use rig_core::completion::{AssistantContent, Message};
14use rig_core::message::{Audio, Document, Image, ToolCall, ToolResult, UserContent, Video};
15use rig_core::tool::{ExitStatus, KeptEnd, OutputCut, ToolResultContext};
16use rig_ecs::prelude::{Aside, AsideId, Author, AuthorKind, MessageId};
17use rig_harness::rig_core::conversation::Note;
18
19use super::{diff, markdown};
20use crate::Theme;
21
22pub struct ToolCallView<'a> {
24 pub call: &'a ToolCall,
27 pub result: Option<&'a ToolResult>,
29 pub state: CallState,
31 pub theme: Theme,
33 pub facts: Option<&'a ToolResultContext>,
37 pub took: Option<Duration>,
39 pub output: Option<&'a str>,
46 pub children: &'a [ChildCall],
53}
54
55#[derive(Clone, Debug, PartialEq)]
58pub struct ChildCall {
59 pub depth: usize,
61 pub call: Option<ToolCall>,
63 pub name: String,
66 pub agent: Option<String>,
68 pub state: CallState,
71 pub failed: bool,
73 pub answer: Option<String>,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Hash)]
81pub enum CallState {
82 Streaming,
84 Waiting,
87 Held(Vec<String>),
90 Running(Option<String>),
94 Done,
96}
97
98pub(crate) const UNNAMED_HOLD: &str = "unnamed hold";
100
101impl<'a> ToolCallView<'a> {
102 pub fn new(call: &'a ToolCall, result: Option<&'a ToolResult>, state: CallState) -> Self {
105 Self {
106 call,
107 result,
108 state,
109 theme: Theme::default(),
110 facts: None,
111 took: None,
112 output: None,
113 children: &[],
114 }
115 }
116
117 pub fn name(&self) -> &str {
119 &self.call.function.name
120 }
121
122 pub fn argument(&self, key: &str) -> Option<&str> {
124 self.call.function.arguments.get(key)?.as_str()
125 }
126
127 pub fn result_text(&self) -> Option<String> {
129 self.result.map(result_text)
130 }
131
132 pub fn failed(&self) -> bool {
135 self.result.is_some_and(|result| result.is_error)
136 || self.exit().is_some_and(|exit| !exit.success())
137 }
138
139 fn exit(&self) -> Option<ExitStatus> {
141 self.facts?.get::<ExitStatus>().ok().flatten()
142 }
143
144 pub fn header(&self, title: impl Into<String>, detail: impl Into<String>) -> Line<'static> {
149 let detail = detail.into();
150 let detail = (!detail.is_empty()).then(|| Span::from(detail).dim());
151 self.header_spans(title, detail.into_iter().collect())
152 }
153
154 pub fn header_spans(
157 &self,
158 title: impl Into<String>,
159 detail: Vec<Span<'static>>,
160 ) -> Line<'static> {
161 let failure = || match self.exit() {
162 Some(ExitStatus::Exited(code)) if code != 0 => format!("exit {code}"),
163 Some(ExitStatus::Signaled) => "killed by a signal".to_owned(),
164 Some(ExitStatus::TimedOut) => "timed out".to_owned(),
165 Some(ExitStatus::Killed) => "killed".to_owned(),
166 _ => "failed".to_owned(),
167 };
168 let (band, state) = match (self.result, &self.state) {
169 (Some(_), _) if self.failed() => (self.theme.call_failed, failure()),
170 (_, CallState::Held(holds)) => (
171 self.theme.call_waiting,
172 format!("held: {}", holds.join(", ")),
173 ),
174 (Some(_), _) => (self.theme.call_done, String::new()),
175 (None, CallState::Running(_)) => (self.theme.call_running, "running".to_owned()),
176 (None, _) => (self.theme.call_waiting, String::new()),
177 };
178 let mut spans = vec![Span::from(" "), Span::from(title.into()).bold()];
179 if !detail.is_empty() {
180 spans.push(Span::from(" "));
181 spans.extend(detail);
182 }
183 if !state.is_empty() {
184 spans.push(Span::from(format!(" · {state}")).italic());
185 }
186 Line::from(spans).bg(band)
187 }
188
189 pub fn result_lines(&self) -> Vec<Line<'static>> {
194 let Some(text) = self.result_text() else {
195 return self.progress_lines();
196 };
197 if !self.failed()
198 && let Some(lines) = diff::unified_lines(&text, &self.theme)
199 {
200 return lines;
201 }
202 let (output, marker) = self.cut_marker(&text);
203 result_rows(output.into_iter(), marker, result_style(self.failed()))
204 }
205
206 fn cut_marker<'t>(&self, text: &'t str) -> (Vec<&'t str>, Option<(&'t str, KeptEnd)>) {
211 let mut lines: Vec<&str> = text.lines().collect();
212 let cut = self
213 .facts
214 .and_then(|facts| facts.get::<OutputCut>().ok().flatten());
215 let marker = cut.and_then(|cut| {
216 let line = match cut.kept {
217 KeptEnd::Ends if lines.is_empty() => None,
219 KeptEnd::Ends => Some(lines.remove(0)),
220 KeptEnd::Start => lines.pop(),
221 };
222 Some((line?, cut.kept))
223 });
224 (lines, marker)
225 }
226
227 pub fn progress_lines(&self) -> Vec<Line<'static>> {
231 let CallState::Running(Some(progress)) = &self.state else {
232 return Vec::new();
233 };
234 result_rows(progress.lines(), None, Style::new().yellow())
235 }
236
237 pub fn default_lines(&self) -> Vec<Line<'static>> {
240 let arguments = serde_json::Value::Object(self.call.function.arguments.clone()).to_string();
241 let mut lines = vec![self.header(self.name().to_owned(), arguments)];
242 lines.extend(self.result_lines());
243 lines
244 }
245}
246
247pub struct EntryView<'a> {
251 pub plugin: &'a str,
254 pub title: &'a str,
257 pub text: &'a str,
259}
260
261impl EntryView<'_> {
262 pub fn default_lines(&self) -> Vec<Line<'static>> {
265 let mut lines = vec![Line::styled(
266 format!("⤶ {}", self.title),
267 Style::new().magenta().bold(),
268 )];
269 lines.extend(text_rows(self.text, Style::new().dim()));
270 lines
271 }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
276pub enum ContentKind {
277 Image,
279 Audio,
281 Video,
283 Document,
285}
286
287impl ContentKind {
288 pub fn name(self) -> &'static str {
290 match self {
291 Self::Image => "image",
292 Self::Audio => "audio",
293 Self::Video => "video",
294 Self::Document => "document",
295 }
296 }
297}
298
299#[derive(Clone, Copy, Debug)]
302pub enum ContentBlock<'a> {
303 Image(&'a Image),
305 Audio(&'a Audio),
307 Video(&'a Video),
309 Document(&'a Document),
311}
312
313impl<'a> ContentBlock<'a> {
314 pub(crate) fn of_user(content: &'a UserContent) -> Option<Self> {
316 match content {
317 UserContent::Image(image) => Some(Self::Image(image)),
318 UserContent::Audio(audio) => Some(Self::Audio(audio)),
319 UserContent::Video(video) => Some(Self::Video(video)),
320 UserContent::Document(document) => Some(Self::Document(document)),
321 UserContent::Text(_) | UserContent::ToolResult(_) => None,
322 }
323 }
324
325 pub(crate) fn of_assistant(content: &'a AssistantContent) -> Option<Self> {
327 match content {
328 AssistantContent::Image(image) => Some(Self::Image(image)),
329 _ => None,
330 }
331 }
332
333 pub fn kind(self) -> ContentKind {
335 match self {
336 Self::Image(_) => ContentKind::Image,
337 Self::Audio(_) => ContentKind::Audio,
338 Self::Video(_) => ContentKind::Video,
339 Self::Document(_) => ContentKind::Document,
340 }
341 }
342}
343
344pub struct ContentView<'a> {
353 pub message: Option<MessageId>,
355 pub index: usize,
357 pub block: ContentBlock<'a>,
359 pub assistant: bool,
361}
362
363impl ContentView<'_> {
364 pub fn default_lines(&self) -> Vec<Line<'static>> {
367 let name = self.block.kind().name();
368 vec![Line::from(format!(" [{name}]")).dim()]
369 }
370}
371
372pub(crate) fn plugin_name(author: &Author) -> &str {
374 match &author.kind {
375 AuthorKind::Plugin(name) => name,
376 _ => "",
377 }
378}
379
380pub(crate) fn result_text(result: &ToolResult) -> String {
382 result
383 .content
384 .iter()
385 .filter_map(|content| content.as_text())
386 .collect::<Vec<_>>()
387 .join("\n")
388}
389
390pub(crate) fn result_style(failed: bool) -> Style {
392 if failed {
393 Style::new().red()
394 } else {
395 Style::new().dim()
396 }
397}
398
399pub const OUTPUT_INDENT: &str = " ";
402
403pub fn text_rows(text: &str, style: Style) -> Vec<Line<'static>> {
405 result_rows(text.lines(), None, style)
406}
407
408fn result_rows<'t>(
412 lines: impl Iterator<Item = &'t str>,
413 marker: Option<(&'t str, KeptEnd)>,
414 style: Style,
415) -> Vec<Line<'static>> {
416 let (before, after) = match marker {
417 Some((marker, KeptEnd::Ends)) => (Some(marker), None),
418 Some((marker, KeptEnd::Start)) => (None, Some(marker)),
419 None => (None, None),
420 };
421 let rows = before.into_iter().chain(lines).chain(after);
422 rows.map(|line| {
423 let line = line.replace('\t', " ");
424 Line::styled(format!("{OUTPUT_INDENT}{line}"), style)
425 })
426 .collect()
427}
428
429pub type RenderToolCall = dyn Fn(&ToolCallView<'_>) -> Vec<Line<'static>> + Send + Sync;
431
432#[derive(Component, Clone)]
434pub struct ToolRenderer {
435 pub tool: String,
437 pub render: Arc<RenderToolCall>,
439}
440
441impl ToolRenderer {
442 pub fn new(
444 tool: &str,
445 render: impl Fn(&ToolCallView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
446 ) -> Self {
447 let (tool, render) = (tool.to_owned(), Arc::new(render));
448 Self { tool, render }
449 }
450}
451
452pub type RenderEntry = dyn Fn(&EntryView<'_>) -> Vec<Line<'static>> + Send + Sync;
454
455#[derive(Component, Clone)]
458pub struct EntryRenderer {
459 pub plugin: String,
462 pub render: Arc<RenderEntry>,
464}
465
466impl EntryRenderer {
467 pub fn new(
469 plugin: &str,
470 render: impl Fn(&EntryView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
471 ) -> Self {
472 let (plugin, render) = (plugin.to_owned(), Arc::new(render));
473 Self { plugin, render }
474 }
475}
476
477pub type RenderContent = dyn Fn(&ContentView<'_>) -> Vec<Line<'static>> + Send + Sync;
479
480#[derive(Component, Clone)]
482pub struct ContentRenderer {
483 pub kind: ContentKind,
485 pub render: Arc<RenderContent>,
487}
488
489impl ContentRenderer {
490 pub fn new(
492 kind: ContentKind,
493 render: impl Fn(&ContentView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
494 ) -> Self {
495 let render = Arc::new(render);
496 Self { kind, render }
497 }
498}
499
500pub type RenderAside = dyn Fn(&AsideView<'_>) -> Vec<Line<'static>> + Send + Sync;
502
503#[derive(Component, Clone)]
505pub struct AsideRenderer {
506 pub kind: String,
508 pub render: Arc<RenderAside>,
510}
511
512impl AsideRenderer {
513 pub fn new(
515 kind: &str,
516 render: impl Fn(&AsideView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
517 ) -> Self {
518 let (kind, render) = (kind.to_owned(), Arc::new(render));
519 Self { kind, render }
520 }
521}
522
523pub(crate) type Decorated = Vec<(String, std::ops::Range<usize>)>;
526
527#[derive(Component, Clone)]
530pub struct CallDecorator {
531 pub name: String,
533 pub render: Arc<RenderToolCall>,
535}
536
537impl CallDecorator {
538 pub fn new(
540 name: &str,
541 render: impl Fn(&ToolCallView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
542 ) -> Self {
543 let (name, render) = (name.to_owned(), Arc::new(render));
544 Self { name, render }
545 }
546}
547
548pub type DecorateMessage = dyn Fn(&DecoratedMessage<'_>) -> Vec<Line<'static>> + Send + Sync;
550
551#[derive(Component, Clone)]
554pub struct MessageDecorator {
555 pub name: String,
557 pub render: Arc<DecorateMessage>,
559}
560
561impl MessageDecorator {
562 pub fn new(
564 name: &str,
565 render: impl Fn(&DecoratedMessage<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
566 ) -> Self {
567 let (name, render) = (name.to_owned(), Arc::new(render));
568 Self { name, render }
569 }
570}
571
572pub struct AsideView<'a> {
574 pub id: AsideId,
576 pub aside: &'a Aside,
578 pub theme: Theme,
580}
581
582#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
584pub enum Role {
585 User,
587 Assistant,
589 Reasoning,
591}
592
593impl Role {
594 pub fn name(self) -> &'static str {
596 match self {
597 Self::User => "user",
598 Self::Assistant => "assistant",
599 Self::Reasoning => "reasoning",
600 }
601 }
602}
603
604pub struct MessageView<'a> {
606 pub role: Role,
608 pub message: Option<MessageId>,
610 pub block: usize,
612 pub text: &'a str,
614 pub marks: &'a [(usize, Span<'static>)],
617 pub width: usize,
620 pub theme: Theme,
622}
623
624impl MessageView<'_> {
625 pub fn default_lines(&self) -> Vec<Line<'static>> {
628 match self.role {
629 Role::User => {
630 let lines = self.text.lines().enumerate();
631 let rows = lines.map(|(index, line)| {
632 let prefix = if index == 0 { "› " } else { " " };
633 let text = line.replace('\t', " ");
634 Line::from(vec![Span::from(prefix).dim(), Span::from(text).bold()])
635 });
636 rows.collect()
637 }
638 Role::Assistant => {
639 markdown::render_marked(self.text, self.marks, &self.theme, Some(self.width))
640 }
641 Role::Reasoning => {
642 let lines = self.text.lines();
643 let style = Style::new().dim().italic();
644 let rows = lines.map(|line| Line::styled(line.replace('\t', " "), style));
645 rows.collect()
646 }
647 }
648 }
649}
650
651pub struct DecoratedMessage<'a> {
653 pub message: &'a Message,
655 pub id: Option<MessageId>,
657 pub author: Option<&'a Author>,
659}
660
661pub trait AppToolRenderersExt {
694 fn add_tool_renderer(
696 &mut self,
697 tool: &str,
698 render: impl Fn(&ToolCallView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
699 ) -> &mut Self;
700
701 fn add_entry_renderer(
705 &mut self,
706 plugin: &str,
707 render: impl Fn(&EntryView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
708 ) -> &mut Self;
709
710 fn add_content_renderer(
714 &mut self,
715 kind: ContentKind,
716 render: impl Fn(&ContentView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
717 ) -> &mut Self;
718
719 fn add_aside_renderer(
725 &mut self,
726 kind: &str,
727 render: impl Fn(&AsideView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
728 ) -> &mut Self;
729
730 fn add_call_decorator(
736 &mut self,
737 name: &str,
738 render: impl Fn(&ToolCallView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
739 ) -> &mut Self;
740}
741
742fn add_look<T: Component>(app: &mut App, name: String, look: T, same: impl Fn(&T) -> bool) {
746 let world = app.world_mut();
747 let mut looks = world.query_filtered::<&T, Without<ChildOf>>();
748 if looks.iter(world).any(same) {
749 warn!("look not added: `{name}` has one; insert `Disabled` on it to replace it");
750 return;
751 }
752 world.spawn((Name::new(name), look));
753}
754
755impl AppToolRenderersExt for App {
756 fn add_tool_renderer(
757 &mut self,
758 tool: &str,
759 render: impl Fn(&ToolCallView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
760 ) -> &mut Self {
761 let look = ToolRenderer::new(tool, render);
762 add_look(self, format!("renderer:{tool}"), look, |other| {
763 other.tool == tool
764 });
765 self
766 }
767
768 fn add_entry_renderer(
769 &mut self,
770 plugin: &str,
771 render: impl Fn(&EntryView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
772 ) -> &mut Self {
773 let look = EntryRenderer::new(plugin, render);
774 add_look(self, format!("entry:{plugin}"), look, |other| {
775 other.plugin == plugin
776 });
777 self
778 }
779
780 fn add_content_renderer(
781 &mut self,
782 kind: ContentKind,
783 render: impl Fn(&ContentView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
784 ) -> &mut Self {
785 let look = ContentRenderer::new(kind, render);
786 let name = format!("content:{}", kind.name());
787 add_look(self, name, look, |other| other.kind == kind);
788 self
789 }
790
791 fn add_aside_renderer(
792 &mut self,
793 kind: &str,
794 render: impl Fn(&AsideView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
795 ) -> &mut Self {
796 let look = AsideRenderer::new(kind, render);
797 add_look(self, format!("aside:{kind}"), look, |other| {
798 other.kind == kind
799 });
800 self
801 }
802
803 fn add_call_decorator(
804 &mut self,
805 name: &str,
806 render: impl Fn(&ToolCallView<'_>) -> Vec<Line<'static>> + Send + Sync + 'static,
807 ) -> &mut Self {
808 let look = CallDecorator::new(name, render);
809 add_look(self, format!("decorator:{name}"), look, |other| {
810 other.name == name
811 });
812 self
813 }
814}
815
816type Scoped<T> = (Entity, Ref<'static, T>, Option<&'static ChildOf>);
819
820#[derive(SystemParam)]
822pub(crate) struct Looks<'w, 's> {
823 tools: Query<'w, 's, Scoped<ToolRenderer>>,
824 entries: Query<'w, 's, Scoped<EntryRenderer>>,
825 contents: Query<'w, 's, Scoped<ContentRenderer>>,
826 asides: Query<'w, 's, Scoped<AsideRenderer>>,
827 decorators: Query<'w, 's, Scoped<CallDecorator>>,
828 message_decorators: Query<'w, 's, Scoped<MessageDecorator>>,
829}
830
831fn scoped<'a, T: Component, K, V>(
835 looks: impl Iterator<Item = (Entity, Ref<'a, T>, Option<&'a ChildOf>)>,
836 pane: Option<Entity>,
837 key: impl Fn(&'a T) -> (K, V),
838) -> Vec<(K, V)> {
839 let mut found: Vec<(bool, Entity, &'a T)> = looks
840 .filter_map(|(entity, look, parent)| match parent.map(ChildOf::parent) {
841 None => Some((true, entity, look.into_inner())),
842 Some(of) if Some(of) == pane => Some((false, entity, look.into_inner())),
843 Some(_) => None,
844 })
845 .collect();
846 found.sort_by_key(|(global, entity, _)| (*global, *entity));
847 found.into_iter().map(|(.., look)| key(look)).collect()
848}
849
850fn first_of<K: Eq + std::hash::Hash, V>(looks: Vec<(K, V)>) -> HashMap<K, V> {
852 let mut map = HashMap::new();
853 for (key, look) in looks {
854 map.entry(key).or_insert(look);
855 }
856 map
857}
858
859impl Looks<'_, '_> {
860 pub(crate) fn by_kind(&self, pane: Option<Entity>) -> Renderers<'_> {
864 let tools = scoped(self.tools.iter(), pane, |look| {
865 (look.tool.as_str(), &look.render)
866 });
867 let entries = scoped(self.entries.iter(), pane, |look| {
868 (look.plugin.as_str(), &look.render)
869 });
870 let contents = scoped(self.contents.iter(), pane, |look| (look.kind, &look.render));
871 let asides = scoped(self.asides.iter(), pane, |look| {
872 (look.kind.as_str(), &look.render)
873 });
874 let decorators = scoped(self.decorators.iter(), pane, |look| {
875 (look.name.as_str(), &look.render)
876 });
877 let message_decorators = scoped(self.message_decorators.iter(), pane, |look| {
878 (look.name.as_str(), &look.render)
879 });
880 Renderers {
881 tools: first_of(tools),
882 entries: first_of(entries),
883 contents: first_of(contents),
884 asides: first_of(asides),
885 decorators,
886 message_decorators,
887 }
888 }
889
890 pub(crate) fn changed(&self) -> bool {
892 self.tools.iter().any(|(_, look, _)| look.is_changed())
893 || self.entries.iter().any(|(_, look, _)| look.is_changed())
894 || self.contents.iter().any(|(_, look, _)| look.is_changed())
895 || self.asides.iter().any(|(_, look, _)| look.is_changed())
896 || self.decorators.iter().any(|(_, look, _)| look.is_changed())
897 || (self.message_decorators.iter()).any(|(_, look, _)| look.is_changed())
898 }
899}
900
901#[derive(Default)]
903pub(crate) struct Renderers<'a> {
904 pub(crate) tools: HashMap<&'a str, &'a Arc<RenderToolCall>>,
906 pub(crate) entries: HashMap<&'a str, &'a Arc<RenderEntry>>,
908 pub(crate) contents: HashMap<ContentKind, &'a Arc<RenderContent>>,
910 pub(crate) asides: HashMap<&'a str, &'a Arc<RenderAside>>,
912 pub(crate) decorators: Vec<(&'a str, &'a Arc<RenderToolCall>)>,
914 pub(crate) message_decorators: Vec<(&'a str, &'a Arc<DecorateMessage>)>,
916}
917
918impl Renderers<'_> {
919 pub(crate) fn call(&self, view: &ToolCallView<'_>) -> Vec<Line<'static>> {
922 self.call_decorated(view).0
923 }
924
925 pub(crate) fn call_decorated(
928 &self,
929 view: &ToolCallView<'_>,
930 ) -> (Vec<Line<'static>>, Decorated) {
931 let mut lines = match self.tools.get(view.name()) {
932 Some(render) => render(view),
933 None => view.default_lines(),
934 };
935 let mut decorated = Vec::new();
936 for (name, decorate) in &self.decorators {
937 let start = lines.len();
938 lines.extend(decorate(view));
939 if lines.len() > start {
940 decorated.push(((*name).to_owned(), start..lines.len()));
941 }
942 }
943 (lines, decorated)
944 }
945
946 pub(crate) fn entry(&self, view: &EntryView<'_>) -> Vec<Line<'static>> {
949 match self.entries.get(view.plugin) {
950 Some(render) => render(view),
951 None => view.default_lines(),
952 }
953 }
954
955 pub(crate) fn content(&self, view: &ContentView<'_>) -> Vec<Line<'static>> {
958 match self.contents.get(&view.block.kind()) {
959 Some(render) => render(view),
960 None => view.default_lines(),
961 }
962 }
963
964 pub(crate) fn aside(&self, view: &AsideView<'_>) -> Option<Vec<Line<'static>>> {
967 if let Some(note) = Note::of(view.aside) {
968 let entry = EntryView {
969 plugin: plugin_name(&view.aside.author),
970 title: ¬e.title,
971 text: ¬e.text,
972 };
973 return Some(self.entry(&entry));
974 }
975 let render = self.asides.get(view.aside.kind.as_str())?;
976 Some(render(view))
977 }
978
979 pub(crate) fn draws(&self, aside: &Aside) -> bool {
981 aside.kind == Note::KIND || self.asides.contains_key(aside.kind.as_str())
982 }
983
984 pub(crate) fn message_decorations(
987 &self,
988 message: &DecoratedMessage<'_>,
989 ) -> (Vec<Line<'static>>, Decorated) {
990 let (mut lines, mut decorated) = (Vec::new(), Vec::new());
991 for (name, decorate) in &self.message_decorators {
992 let start = lines.len();
993 lines.extend(decorate(message));
994 if lines.len() > start {
995 decorated.push(((*name).to_owned(), start..lines.len()));
996 }
997 }
998 (lines, decorated)
999 }
1000}
1001
1002#[cfg(test)]
1003mod tests;