vtcode_ui/tui/core_tui/session/
header.rs1use std::fmt::Write;
2use std::sync::Arc;
3
4use ratatui::{
5 style::{Color, Modifier, Style},
6 text::{Line, Span},
7 widgets::{Block, Paragraph, Wrap},
8};
9use unicode_segmentation::UnicodeSegmentation;
10
11use crate::tui::config::constants::ui;
12use crate::tui::core_tui::blocked_status;
13use crate::tui::core_tui::style::convert_style;
14
15use super::super::types::{InlineHeaderContext, InlineHeaderHighlight};
16use super::terminal_capabilities;
17use super::utils::line_truncation::truncate_line_with_ellipsis_if_overflow;
18use super::{Session, ratatui_color_from_ansi, ratatui_style_from_inline};
19
20fn clean_reasoning_text(text: &str) -> String {
21 vtcode_commons::formatting::clean_reasoning_text(text)
22}
23
24fn capitalize_first_letter(s: &str) -> String {
25 let mut chars = s.chars();
26 match chars.next() {
27 None => String::new(),
28 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
29 }
30}
31
32fn format_model_summary_label(model: &str) -> String {
33 model.split('-').map(capitalize_first_letter).collect::<Vec<_>>().join("-")
34}
35
36pub(crate) fn primary_agent_header_label(name: Option<&str>) -> String {
37 let Some(name) = name.map(str::trim).filter(|name| !name.is_empty()) else {
38 return "Build".to_string();
39 };
40 format_model_summary_label(name)
41}
42
43fn compact_context_window_label(context_window_size: usize) -> String {
44 if context_window_size >= 1_000_000 {
45 format!("{}M", context_window_size / 1_000_000)
46 } else if context_window_size >= 1_000 {
47 format!("{}K", context_window_size / 1_000)
48 } else {
49 context_window_size.to_string()
50 }
51}
52
53fn line_is_empty(spans: &[Span<'static>]) -> bool {
54 spans.len() == 1 && spans.first().is_some_and(|span| span.content.is_empty())
55}
56
57impl Session {
58 pub(crate) fn header_lines(&mut self) -> Arc<Vec<Line<'static>>> {
59 if let Some(cached) = &self.header_lines_cache {
60 return Arc::clone(cached);
61 }
62
63 let lines = if self.appearance.hide_header {
64 vec![self.header_block_title_for_width(self.transcript_width)]
65 } else {
66 vec![self.header_compact_line()]
67 };
68 let arc = Arc::new(lines);
69 self.header_lines_cache = Some(Arc::clone(&arc));
70 arc
71 }
72
73 pub(crate) fn header_block_title_cached(&mut self) -> Line<'static> {
75 if let Some(title) = &self.header_block_title_cache {
76 return title.clone();
77 }
78 let title = self.header_block_title();
79 self.header_block_title_cache = Some(title.clone());
80 title
81 }
82
83 pub(crate) fn header_height_from_lines(&mut self, width: u16, lines: &[Line<'static>]) -> u16 {
84 if self.appearance.hide_header {
85 return 1;
86 }
87
88 if width == 0 {
89 return self.header_rows.max(ui::INLINE_HEADER_HEIGHT);
90 }
91
92 if let Some(&height) = self.header_height_cache.get(&width) {
93 return height;
94 }
95
96 let paragraph = self.build_header_paragraph(lines);
97 let measured = paragraph.line_count(width);
98 let resolved = u16::try_from(measured).unwrap_or(u16::MAX);
99 let resolved = resolved.clamp(ui::INLINE_HEADER_HEIGHT, 3);
101 self.header_height_cache.insert(width, resolved);
102 resolved
103 }
104
105 pub(crate) fn build_header_paragraph(&self, lines: &[Line<'static>]) -> Paragraph<'static> {
106 let text_style = self.header_primary_style().add_modifier(Modifier::DIM);
107
108 if self.appearance.hide_header {
109 return Paragraph::new(lines.to_vec()).style(text_style).wrap(Wrap { trim: true });
110 }
111
112 let mut border_style = Style::default();
113 if let Some(accent) = self.theme.tool_accent.or(self.theme.primary).or(self.theme.foreground) {
114 border_style = border_style.fg(ratatui_color_from_ansi(accent));
115 }
116 let text_style = self.header_primary_style().add_modifier(Modifier::DIM);
117 let block = Block::bordered()
118 .title(self.header_block_title())
119 .border_type(terminal_capabilities::get_border_type())
120 .border_style(border_style)
121 .style(self.styles.default_style());
122
123 Paragraph::new(lines.to_vec())
124 .style(text_style)
125 .wrap(Wrap { trim: true })
126 .block(block)
127 }
128
129 #[cfg(test)]
130 pub(crate) fn header_height_for_width(&mut self, width: u16) -> u16 {
131 let lines = self.header_lines();
132 self.header_height_from_lines(width, lines.as_slice())
133 }
134
135 fn header_block_title(&self) -> Line<'static> {
136 self.header_block_title_for_width(0)
137 }
138
139 fn header_block_title_for_width(&self, width: u16) -> Line<'static> {
140 let fallback = InlineHeaderContext::default();
141 let version = if self.header_context.version.trim().is_empty() {
142 fallback.version
143 } else {
144 self.header_context.version.clone()
145 };
146
147 let app_name = if self.header_context.app_name.trim().is_empty() {
148 ui::HEADER_VERSION_PREFIX
149 } else {
150 self.header_context.app_name.trim()
151 };
152 let prompt = format!("{}{}", ui::HEADER_VERSION_PROMPT, app_name);
153 let version_text =
154 format!(" {}{}{}", ui::HEADER_VERSION_LEFT_DELIMITER, version.trim(), ui::HEADER_VERSION_RIGHT_DELIMITER);
155
156 let prompt_style = self.section_title_style();
157 let version_style = self.header_secondary_style().add_modifier(Modifier::DIM);
158
159 let mut spans = vec![
160 Span::styled(prompt, prompt_style),
161 Span::styled(version_text, version_style),
162 ];
163
164 if self.appearance.hide_header && width > 0 {
165 let right_spans = self.header_compact_right_spans();
166 if right_spans.is_empty() {
167 return Line::from(spans);
168 }
169
170 let left_width = spans.iter().map(Span::width).sum::<usize>();
171 let summary_width = right_spans.iter().map(Span::width).sum::<usize>();
172 let available_width = usize::from(width);
173 let spacer_width = available_width.saturating_sub(left_width.saturating_add(summary_width)).max(1);
174 spans.push(Span::raw(" ".repeat(spacer_width)));
175 spans.extend(right_spans);
176 }
177
178 let line = Line::from(spans);
179 if width > 0 {
180 truncate_line_with_ellipsis_if_overflow(line, usize::from(width))
181 } else {
182 line
183 }
184 }
185
186 fn header_compact_right_spans(&self) -> Vec<Span<'static>> {
187 let mut spans = Vec::new();
188 let agent_label = primary_agent_header_label(self.header_context.primary_agent.as_deref());
189 let model_summary_spans = self.header_compact_model_summary_spans();
190 if !agent_label.trim().is_empty() {
191 let fallback = self.theme.primary.map(ratatui_color_from_ansi).unwrap_or(Color::LightMagenta);
192 let agent_style =
193 super::super::style::agent_color_style(self.header_context.primary_agent_color.as_deref(), fallback);
194 spans.push(Span::styled(agent_label, agent_style));
195 }
196
197 if !spans.is_empty() && !model_summary_spans.is_empty() {
198 spans.push(Span::styled(" · ".to_owned(), self.header_secondary_style()));
199 }
200 spans.extend(model_summary_spans);
201
202 spans
203 }
204
205 fn header_compact_model_summary_spans(&self) -> Vec<Span<'static>> {
206 let provider = self.header_provider_short_value();
207 let model = self.header_model_short_value();
208 let reasoning = self.header_reasoning_short_value();
209 let mut spans = Vec::new();
210 let mut provider_model_parts = Vec::new();
211
212 match (
213 !provider.is_empty() && !provider.eq_ignore_ascii_case(ui::HEADER_UNKNOWN_PLACEHOLDER),
214 !model.is_empty() && !model.eq_ignore_ascii_case(ui::HEADER_UNKNOWN_PLACEHOLDER),
215 ) {
216 (true, true) => provider_model_parts.push(format!(
217 "{} {}",
218 capitalize_first_letter(&provider),
219 format_model_summary_label(&model)
220 )),
221 (true, false) => provider_model_parts.push(capitalize_first_letter(&provider)),
222 (false, true) => provider_model_parts.push(format_model_summary_label(&model)),
223 (false, false) => {}
224 }
225
226 if let Some(summary) = (!provider_model_parts.is_empty()).then(|| provider_model_parts.join(" ")) {
227 spans.push(Span::styled(summary, self.header_secondary_style().add_modifier(Modifier::DIM)));
228 }
229
230 if !reasoning.is_empty() && !reasoning.eq_ignore_ascii_case(ui::HEADER_UNKNOWN_PLACEHOLDER) {
231 if !spans.is_empty() {
232 spans.push(Span::styled(" · ".to_owned(), self.header_secondary_style()));
233 }
234 let value_style = self.header_secondary_style().add_modifier(Modifier::ITALIC);
235 spans.push(Span::styled(reasoning, value_style));
236 }
237
238 if let Some(tier) = self.header_service_tier_short_value() {
239 if !spans.is_empty() {
240 spans.push(Span::styled(" · ".to_owned(), self.header_secondary_style()));
241 }
242 spans.push(Span::styled(
243 capitalize_first_letter(&tier),
244 self.header_secondary_style().add_modifier(Modifier::DIM),
245 ));
246 }
247
248 spans
249 }
250
251 pub(crate) fn header_title_line(&self) -> Line<'static> {
252 let mut spans = Vec::new();
254
255 let provider = self.header_provider_short_value();
256 let model = self.header_model_short_value();
257 let reasoning = self.header_reasoning_short_value();
258
259 if !provider.is_empty() {
260 let capitalized_provider = capitalize_first_letter(&provider);
261 let mut style = self.header_primary_style();
262 style = style.add_modifier(Modifier::BOLD);
263 spans.push(Span::styled(capitalized_provider, style));
264 }
265
266 if !model.is_empty() {
267 if !spans.is_empty() {
268 spans.push(Span::raw(" "));
269 }
270 let mut style = self.header_primary_style();
271 style = style.add_modifier(Modifier::ITALIC);
272 spans.push(Span::styled(model, style));
273 }
274
275 if !reasoning.is_empty() {
276 if !spans.is_empty() {
277 spans.push(Span::raw(" "));
278 }
279 let mut style = self.header_secondary_style();
280 style = style.add_modifier(Modifier::ITALIC | Modifier::DIM);
281
282 if let Some(stage) = &self.header_context.reasoning_stage {
283 let mut stage_style = style;
284 stage_style = stage_style.remove_modifier(Modifier::DIM).add_modifier(Modifier::BOLD);
285 spans.push(Span::styled(format!("[{stage}]"), stage_style));
286 spans.push(Span::raw(" "));
287 }
288
289 spans.push(Span::styled(reasoning.to_string(), style));
290 }
291
292 if let Some(tier) = self.header_service_tier_short_value() {
293 if !spans.is_empty() {
294 spans.push(Span::raw(" "));
295 }
296 spans.push(Span::styled(capitalize_first_letter(&tier), self.header_secondary_style()));
297 }
298
299 if spans.is_empty() {
300 spans.push(Span::raw(String::new()));
301 }
302
303 Line::from(spans)
304 }
305
306 fn header_compact_line(&self) -> Line<'static> {
307 let mut spans = self.header_title_line().spans;
308 if line_is_empty(&spans) {
309 spans.clear();
310 }
311
312 let mut meta_spans = self.header_meta_line().spans;
313 if line_is_empty(&meta_spans) {
314 meta_spans.clear();
315 }
316
317 let mut tail_spans = if self.should_show_suggestions() {
318 self.header_suggestions_line()
319 } else {
320 self.header_highlights_line()
321 }
322 .map(|line| line.spans)
323 .unwrap_or_default();
324
325 if line_is_empty(&tail_spans) {
326 tail_spans.clear();
327 }
328
329 let separator_style = self.header_secondary_style();
330 let separator = Span::styled(ui::HEADER_PRIMARY_SEPARATOR.to_owned(), separator_style);
331
332 let mut append_section = |section: &mut Vec<Span<'static>>| {
333 if section.is_empty() {
334 return;
335 }
336 if !spans.is_empty() {
337 spans.push(separator.clone());
338 }
339 spans.append(section);
340 };
341
342 append_section(&mut meta_spans);
343 append_section(&mut tail_spans);
344
345 if spans.is_empty() {
346 spans.push(Span::raw(String::new()));
347 }
348
349 Line::from(spans)
350 }
351
352 fn header_provider_value(&self) -> String {
353 let trimmed = self.header_context.provider.trim();
354 if trimmed.is_empty() {
355 InlineHeaderContext::default().provider
356 } else {
357 self.header_context.provider.clone()
358 }
359 }
360
361 fn header_model_value(&self) -> String {
362 let trimmed = self.header_context.model.trim();
363 if trimmed.is_empty() {
364 InlineHeaderContext::default().model
365 } else {
366 self.header_context.model.clone()
367 }
368 }
369
370 fn header_reasoning_value(&self) -> Option<String> {
371 let raw_reasoning = &self.header_context.reasoning;
372 let cleaned = clean_reasoning_text(raw_reasoning);
373 let trimmed = cleaned.trim();
374 let value = if trimmed.is_empty() {
375 InlineHeaderContext::default().reasoning
376 } else {
377 cleaned
378 };
379 if value.trim().is_empty() { None } else { Some(value) }
380 }
381
382 fn header_provider_short_value(&self) -> String {
383 let value = self.header_provider_value();
384 Self::strip_prefix(&value, ui::HEADER_PROVIDER_PREFIX).trim().to_owned()
385 }
386
387 fn header_model_short_value(&self) -> String {
388 let value = self.header_model_value();
389 let model = Self::strip_prefix(&value, ui::HEADER_MODEL_PREFIX).trim().to_owned();
390
391 match self.header_context.context_window_size {
392 Some(context_window_size) if context_window_size > 0 => {
393 format!("{} ({})", model, compact_context_window_label(context_window_size))
394 }
395 _ => model,
396 }
397 }
398
399 fn header_reasoning_short_value(&self) -> String {
400 let value = self.header_reasoning_value().unwrap_or_default();
401 Self::strip_prefix(&value, ui::HEADER_REASONING_PREFIX).trim().to_owned()
402 }
403
404 fn header_service_tier_short_value(&self) -> Option<String> {
405 let raw = self.header_context.service_tier.as_deref()?;
406 let tier = Self::strip_prefix(raw, ui::HEADER_SERVICE_TIER_PREFIX).trim();
407 (!tier.is_empty() && !tier.eq_ignore_ascii_case(ui::HEADER_UNKNOWN_PLACEHOLDER)).then(|| tier.to_owned())
408 }
409
410 pub fn header_chain_values(&self) -> Vec<String> {
411 let mut values = Vec::new();
412
413 for value in [
414 &self.header_context.tools,
415 &self.header_context.git,
416 &self.header_context.mcp,
417 ] {
418 let trimmed = value.trim();
419 if trimmed.is_empty() {
420 continue;
421 }
422
423 if trimmed.starts_with(ui::HEADER_TOOLS_PREFIX) || trimmed.starts_with(ui::HEADER_GIT_PREFIX) {
424 continue;
425 }
426
427 if let Some(body) = trimmed.strip_prefix(ui::HEADER_MCP_PREFIX) {
428 let body = body.trim();
429 if body.is_empty() || body.eq_ignore_ascii_case(ui::HEADER_UNKNOWN_PLACEHOLDER) {
430 continue;
431 }
432 values.push(format!("MCP: {body}"));
433 continue;
434 }
435
436 values.push(trimmed.to_owned());
437 }
438
439 values
440 }
441
442 pub(crate) fn header_meta_line(&self) -> Line<'static> {
443 let mut spans = Vec::new();
444
445 let mut first_section = true;
446 let separator_style = self.header_secondary_style();
447
448 let push_badge = |spans: &mut Vec<Span<'static>>, text: String, style: Style, first: &mut bool| {
449 if !*first {
450 spans.push(Span::styled(ui::HEADER_SECONDARY_SEPARATOR.to_owned(), separator_style));
451 }
452 spans.push(Span::styled(text, style));
453 *first = false;
454 };
455
456 let agent_badge_color = self
457 .theme
458 .primary
459 .or(self.theme.foreground)
460 .map(ratatui_color_from_ansi)
461 .unwrap_or(Color::LightMagenta);
462 let agent_style = Style::default().fg(agent_badge_color).add_modifier(Modifier::BOLD);
463 push_badge(
464 &mut spans,
465 primary_agent_header_label(self.header_context.primary_agent.as_deref()),
466 agent_style,
467 &mut first_section,
468 );
469
470 let trust_value = self.header_context.workspace_trust.to_lowercase();
472 if trust_value.contains("full auto") || trust_value.contains("full_auto") {
473 let auto_badge_color = self
474 .theme
475 .tool_accent
476 .or(self.theme.primary)
477 .or(self.theme.foreground)
478 .map(ratatui_color_from_ansi)
479 .unwrap_or(Color::LightCyan);
480 let badge_style = Style::default().fg(auto_badge_color).add_modifier(Modifier::BOLD);
481 push_badge(&mut spans, "Full-auto".to_string(), badge_style, &mut first_section);
482 } else if trust_value.contains("tools policy") || trust_value.contains("tools_policy") {
483 let safe_badge_color = self
484 .theme
485 .primary
486 .or(self.theme.foreground)
487 .map(ratatui_color_from_ansi)
488 .unwrap_or(Color::LightGreen);
489 let badge_style = Style::default().fg(safe_badge_color).add_modifier(Modifier::BOLD);
490 push_badge(&mut spans, "Safe".to_string(), badge_style, &mut first_section);
491 }
492
493 if let Some(badge) = self.header_context.subagent_badges.first() {
495 let mut badge_style = ratatui_style_from_inline(&badge.style, self.theme.foreground);
496 if badge.full_background {
497 badge_style = badge_style.add_modifier(Modifier::BOLD);
498 }
499 let hidden = self.header_context.subagent_badges.len().saturating_sub(1);
500 let label = if hidden == 0 {
501 badge.text.clone()
502 } else {
503 format!("{} +{}", badge.text, hidden)
504 };
505 push_badge(&mut spans, format!(" {label} "), badge_style, &mut first_section);
506 }
507
508 if let Some(badge) = self.background_header_badge_text() {
513 let style = if self.has_background_activity() {
514 self.header_primary_style()
517 .remove_modifier(Modifier::DIM)
518 .add_modifier(Modifier::BOLD)
519 } else {
520 self.header_secondary_style().add_modifier(Modifier::DIM)
521 };
522 push_badge(&mut spans, badge, style, &mut first_section);
523 }
524
525 let is_blocked_header = matches!(
530 self.activity_state,
531 vtcode_commons::ui_protocol::ActivityState::Blocked | vtcode_commons::ui_protocol::ActivityState::Recovery
532 ) || self
533 .input_status_right
534 .as_deref()
535 .is_some_and(blocked_status::right_status_is_blocked)
536 || self
537 .input_status_left
538 .as_deref()
539 .is_some_and(blocked_status::left_status_is_blocked);
540 if is_blocked_header {
541 let blocked_inline = convert_style(crate::theme::active_styles().error);
546 let blocked_style = ratatui_style_from_inline(&blocked_inline, self.theme.foreground);
547 push_badge(&mut spans, "Blocked".to_string(), blocked_style, &mut first_section);
548 }
549
550 if spans.is_empty() {
551 spans.push(Span::raw(String::new()));
552 }
553
554 Line::from(spans)
555 }
556
557 fn header_highlights_line(&self) -> Option<Line<'static>> {
558 let mut spans = Vec::new();
559 let mut first_section = true;
560
561 for highlight in &self.header_context.highlights {
562 let title = highlight.title.trim();
563 let summary = self.header_highlight_summary(highlight);
564
565 if title.is_empty() && summary.is_none() {
566 continue;
567 }
568
569 if !first_section {
570 spans.push(Span::styled(ui::HEADER_META_SEPARATOR.to_owned(), self.header_secondary_style()));
571 }
572
573 if !title.is_empty() {
574 let mut title_style = self.header_secondary_style();
575 title_style = title_style.add_modifier(Modifier::BOLD);
576 let mut title_text = title.to_owned();
577 if summary.is_some() {
578 title_text.push(':');
579 }
580 spans.push(Span::styled(title_text, title_style));
581 if summary.is_some() {
582 spans.push(Span::styled(" ".to_owned(), self.header_secondary_style()));
583 }
584 }
585
586 if let Some(body) = summary {
587 spans.push(Span::styled(body, self.header_primary_style()));
588 }
589
590 first_section = false;
591 }
592
593 if spans.is_empty() {
594 None
595 } else {
596 Some(Line::from(spans))
597 }
598 }
599
600 fn header_highlight_summary(&self, highlight: &InlineHeaderHighlight) -> Option<String> {
601 let entries: Vec<String> = highlight
602 .lines
603 .iter()
604 .map(|line| line.trim())
605 .filter(|line| !line.is_empty())
606 .map(|line| {
607 let stripped = line.strip_prefix("- ").or_else(|| line.strip_prefix("• ")).unwrap_or(line);
608 stripped.trim().to_owned()
609 })
610 .collect();
611
612 if entries.is_empty() {
613 return None;
614 }
615
616 Some(self.compact_highlight_entries(&entries))
617 }
618
619 fn compact_highlight_entries(&self, entries: &[String]) -> String {
620 let mut summary = self.truncate_highlight_preview(entries.first().map(String::as_str).unwrap_or(""));
621 if entries.len() > 1 {
622 let remaining = entries.len() - 1;
623 if !summary.is_empty() {
624 let _ = write!(summary, " (+{remaining} more)");
625 } else {
626 summary = format!("(+{remaining} more)");
627 }
628 }
629 summary
630 }
631
632 fn truncate_highlight_preview(&self, text: &str) -> String {
633 let max = ui::HEADER_HIGHLIGHT_PREVIEW_MAX_CHARS;
634 if max == 0 {
635 return String::new();
636 }
637
638 let grapheme_count = text.graphemes(true).count();
639 if grapheme_count <= max {
640 return text.to_owned();
641 }
642
643 let mut truncated = String::new();
647 truncated.reserve(text.len().min(max * 4));
649 for grapheme in text.graphemes(true).take(max.saturating_sub(1)) {
650 truncated.push_str(grapheme);
651 }
652 truncated.push_str(ui::INLINE_PREVIEW_ELLIPSIS);
653 truncated
654 }
655
656 fn should_show_suggestions(&self) -> bool {
658 self.input_manager.content().is_empty() || self.input_manager.content().starts_with('/')
660 }
661
662 pub(crate) fn header_suggestions_line(&self) -> Option<Line<'static>> {
664 let dim = self.header_secondary_style().add_modifier(Modifier::DIM);
665 let key = self.header_primary_style().add_modifier(Modifier::BOLD);
666 let label = self.header_secondary_style();
667 let dot = Span::styled(" · ", dim);
668
669 let mut spans = vec![
670 Span::styled("/help", key),
671 dot.clone(),
672 Span::styled("/model", key),
673 dot.clone(),
674 Span::styled("/effort", key),
675 dot.clone(),
676 Span::styled("/config", key),
677 dot.clone(),
678 Span::styled("/clear", key),
679 Span::styled(" │ ", dim),
680 Span::styled("↑↓", key),
681 Span::styled(" nav", label),
682 dot.clone(),
683 Span::styled("Tab", key),
684 Span::styled(" complete", label),
685 ];
686
687 if self.has_local_agents() {
688 spans.push(Span::styled(" │ ", dim));
689 spans.push(Span::styled("Alt+S", key));
690 spans.push(Span::styled(" agents", label));
691 spans.push(dot.clone());
692 spans.push(Span::styled(self.background_shortcut_label().to_owned(), key));
693 spans.push(Span::styled(" background", label));
694 } else if self.has_active_foreground_pty() {
695 spans.push(Span::styled(" │ ", dim));
696 spans.push(Span::styled(self.background_shortcut_label().to_owned(), key));
697 spans.push(Span::styled(" background", label));
698 }
699
700 Some(Line::from(spans))
701 }
702
703 pub(crate) fn section_title_style(&self) -> Style {
704 let mut style = self.styles.default_style().add_modifier(Modifier::BOLD | Modifier::DIM);
705 if let Some(primary) = self.theme.primary.or(self.theme.foreground) {
706 style = style.fg(ratatui_color_from_ansi(primary));
707 }
708 style
709 }
710
711 pub(crate) fn header_primary_style(&self) -> Style {
712 let mut style = self.styles.default_style().add_modifier(Modifier::DIM);
713 if let Some(primary) = self.theme.primary.or(self.theme.foreground) {
714 style = style.fg(ratatui_color_from_ansi(primary));
715 }
716 style
717 }
718
719 pub(crate) fn header_secondary_style(&self) -> Style {
720 let mut style = self.styles.default_style().add_modifier(Modifier::DIM);
721 if let Some(secondary) = self.theme.secondary.or(self.theme.foreground) {
722 style = style.fg(ratatui_color_from_ansi(secondary));
723 }
724 style
725 }
726
727 fn strip_prefix<'a>(value: &'a str, prefix: &str) -> &'a str {
728 value.strip_prefix(prefix).unwrap_or(value)
729 }
730}