mermaid_cli/render/widgets/
mod.rs1mod approval;
8mod chat;
9mod conversation_list;
10mod file_picker;
11mod input;
12mod model_picker;
13mod plan_config;
14mod question;
15mod rewind_picker;
16mod slash_palette;
17mod status;
18mod status_line;
19mod tasks;
20
21pub use approval::ApprovalModalWidget;
22pub use chat::{ChatState, ChatWidget, ImageClickTarget};
23pub use conversation_list::ConversationListWidget;
24pub use file_picker::FilePickerWidget;
25pub use input::{InputState, InputWidget};
26pub use model_picker::{MODEL_PICKER_HEIGHT, ModelPickerWidget};
27pub use plan_config::{PLAN_CONFIG_HEIGHT, PLAN_CONFIG_ROWS, PlanConfigWidget, plan_config_rows};
28pub use question::{QuestionModalWidget, question_modal_height};
29pub use rewind_picker::RewindPickerWidget;
30pub use slash_palette::SlashPaletteWidget;
31pub use status::StatusWidget;
32pub use status_line::{AgentPanelRow, build_status_lines};
33pub use tasks::{build_task_lines, tasks_height, tasks_visible};
34
35use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
36
37pub(super) fn truncate_to_cells(s: &str, width: usize) -> String {
42 if UnicodeWidthStr::width(s) <= width {
43 return s.to_string();
44 }
45 if width == 0 {
46 return String::new();
47 }
48 let budget = width - 1; let mut out = String::new();
50 let mut w = 0usize;
51 for ch in s.chars() {
52 let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
53 if w + cw > budget {
54 break;
55 }
56 out.push(ch);
57 w += cw;
58 }
59 out.push('…');
60 out
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum GenerationStatus {
69 Idle,
70 Sending,
71 Thinking,
72 Streaming,
73 RunningTools,
74 Compacting,
75 Cancelling,
76}
77
78impl GenerationStatus {
79 #[must_use]
80 pub fn display_text(&self) -> &str {
81 match self {
82 Self::Idle => "Idle",
83 Self::Sending => "Sending",
84 Self::Thinking => "Thinking",
85 Self::Streaming => "Streaming",
86 Self::RunningTools => "Running tools",
87 Self::Compacting => "Compacting",
88 Self::Cancelling => "Cancelling",
89 }
90 }
91
92 #[must_use]
98 pub fn from_turn(turn: &mermaid_domain::TurnState) -> Self {
99 use mermaid_domain::{GenPhase, TurnState};
100 match turn {
101 TurnState::Idle => Self::Idle,
102 TurnState::Generating { phase, .. } => match phase {
103 GenPhase::Sending => Self::Sending,
104 GenPhase::Thinking => Self::Thinking,
105 GenPhase::Streaming => Self::Streaming,
106 },
107 TurnState::ExecutingTools { .. } => Self::RunningTools,
108 TurnState::Compacting { .. } => Self::Compacting,
109 TurnState::Cancelling { .. } => Self::Cancelling,
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::truncate_to_cells;
117 use unicode_width::UnicodeWidthStr;
118
119 #[test]
120 fn fits_within_width_returns_unchanged() {
121 assert_eq!(truncate_to_cells("hello", 10), "hello");
122 assert_eq!(truncate_to_cells("hello", 5), "hello");
123 }
124
125 #[test]
126 fn ascii_truncates_with_ellipsis_within_budget() {
127 let out = truncate_to_cells("hello world", 5);
128 assert_eq!(out, "hell…");
129 assert!(UnicodeWidthStr::width(out.as_str()) <= 5);
130 }
131
132 #[test]
133 fn wide_glyphs_never_exceed_budget() {
134 let cjk = "你好世界你好世界"; let out = truncate_to_cells(cjk, 6);
139 assert!(
140 UnicodeWidthStr::width(out.as_str()) <= 6,
141 "width exceeded budget: {out:?}"
142 );
143 assert!(out.ends_with('…'));
144 }
145
146 #[test]
147 fn zero_width_is_empty() {
148 assert_eq!(truncate_to_cells("anything", 0), "");
149 }
150}