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