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 pub fn display_text(&self) -> &str {
80 match self {
81 GenerationStatus::Idle => "Idle",
82 GenerationStatus::Sending => "Sending",
83 GenerationStatus::Thinking => "Thinking",
84 GenerationStatus::Streaming => "Streaming",
85 GenerationStatus::RunningTools => "Running tools",
86 GenerationStatus::Compacting => "Compacting",
87 GenerationStatus::Cancelling => "Cancelling",
88 }
89 }
90
91 pub fn from_turn(turn: &crate::domain::TurnState) -> Self {
97 use crate::domain::{GenPhase, TurnState};
98 match turn {
99 TurnState::Idle => GenerationStatus::Idle,
100 TurnState::Generating { phase, .. } => match phase {
101 GenPhase::Sending => GenerationStatus::Sending,
102 GenPhase::Thinking => GenerationStatus::Thinking,
103 GenPhase::Streaming => GenerationStatus::Streaming,
104 },
105 TurnState::ExecutingTools { .. } => GenerationStatus::RunningTools,
106 TurnState::Compacting { .. } => GenerationStatus::Compacting,
107 TurnState::Cancelling { .. } => GenerationStatus::Cancelling,
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::truncate_to_cells;
115 use unicode_width::UnicodeWidthStr;
116
117 #[test]
118 fn fits_within_width_returns_unchanged() {
119 assert_eq!(truncate_to_cells("hello", 10), "hello");
120 assert_eq!(truncate_to_cells("hello", 5), "hello");
121 }
122
123 #[test]
124 fn ascii_truncates_with_ellipsis_within_budget() {
125 let out = truncate_to_cells("hello world", 5);
126 assert_eq!(out, "hell…");
127 assert!(UnicodeWidthStr::width(out.as_str()) <= 5);
128 }
129
130 #[test]
131 fn wide_glyphs_never_exceed_budget() {
132 let cjk = "你好世界你好世界"; let out = truncate_to_cells(cjk, 6);
137 assert!(
138 UnicodeWidthStr::width(out.as_str()) <= 6,
139 "width exceeded budget: {out:?}"
140 );
141 assert!(out.ends_with('…'));
142 }
143
144 #[test]
145 fn zero_width_is_empty() {
146 assert_eq!(truncate_to_cells("anything", 0), "");
147 }
148}