mermaid_cli/render/widgets/
mod.rs1mod approval;
8mod attachment;
9mod chat;
10mod conversation_list;
11mod input;
12mod slash_palette;
13mod status;
14mod status_line;
15
16pub use approval::ApprovalModalWidget;
17pub use attachment::AttachmentWidget;
18pub use chat::{ChatState, ChatWidget, ImageClickTarget};
19pub use conversation_list::ConversationListWidget;
20pub use input::{InputState, InputWidget};
21pub use slash_palette::SlashPaletteWidget;
22pub use status::StatusWidget;
23pub use status_line::build_status_lines;
24
25use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
26
27pub(super) fn truncate_to_cells(s: &str, width: usize) -> String {
32 if UnicodeWidthStr::width(s) <= width {
33 return s.to_string();
34 }
35 if width == 0 {
36 return String::new();
37 }
38 let budget = width - 1; let mut out = String::new();
40 let mut w = 0usize;
41 for ch in s.chars() {
42 let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
43 if w + cw > budget {
44 break;
45 }
46 out.push(ch);
47 w += cw;
48 }
49 out.push('…');
50 out
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum GenerationStatus {
59 Idle,
60 Sending,
61 Thinking,
62 Streaming,
63 RunningTools,
64 Compacting,
65 Cancelling,
66}
67
68impl GenerationStatus {
69 pub fn display_text(&self) -> &str {
70 match self {
71 GenerationStatus::Idle => "Idle",
72 GenerationStatus::Sending => "Sending",
73 GenerationStatus::Thinking => "Thinking",
74 GenerationStatus::Streaming => "Streaming",
75 GenerationStatus::RunningTools => "Running tools",
76 GenerationStatus::Compacting => "Compacting",
77 GenerationStatus::Cancelling => "Cancelling",
78 }
79 }
80
81 pub fn from_turn(turn: &crate::domain::TurnState) -> Self {
87 use crate::domain::{GenPhase, TurnState};
88 match turn {
89 TurnState::Idle => GenerationStatus::Idle,
90 TurnState::Generating { phase, .. } => match phase {
91 GenPhase::Sending => GenerationStatus::Sending,
92 GenPhase::Thinking => GenerationStatus::Thinking,
93 GenPhase::Streaming => GenerationStatus::Streaming,
94 },
95 TurnState::ExecutingTools { .. } => GenerationStatus::RunningTools,
96 TurnState::Compacting { .. } => GenerationStatus::Compacting,
97 TurnState::Cancelling { .. } => GenerationStatus::Cancelling,
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::truncate_to_cells;
105 use unicode_width::UnicodeWidthStr;
106
107 #[test]
108 fn fits_within_width_returns_unchanged() {
109 assert_eq!(truncate_to_cells("hello", 10), "hello");
110 assert_eq!(truncate_to_cells("hello", 5), "hello");
111 }
112
113 #[test]
114 fn ascii_truncates_with_ellipsis_within_budget() {
115 let out = truncate_to_cells("hello world", 5);
116 assert_eq!(out, "hell…");
117 assert!(UnicodeWidthStr::width(out.as_str()) <= 5);
118 }
119
120 #[test]
121 fn wide_glyphs_never_exceed_budget() {
122 let cjk = "你好世界你好世界"; let out = truncate_to_cells(cjk, 6);
127 assert!(
128 UnicodeWidthStr::width(out.as_str()) <= 6,
129 "width exceeded budget: {out:?}"
130 );
131 assert!(out.ends_with('…'));
132 }
133
134 #[test]
135 fn zero_width_is_empty() {
136 assert_eq!(truncate_to_cells("anything", 0), "");
137 }
138}