1use pi_tui::component::Component;
9use pi_tui::components::{Loader, Spacer, Text};
10
11use super::state::{
12 AuthProgress, BashProgress, CompactionProgress, OAuthStage, PendingKind, PendingQueue,
13 RetryProgress,
14};
15use super::theme::{self, ResolvedTheme, ThemeColor};
16
17#[must_use]
25pub fn build_pending(queue: &PendingQueue, th: &ResolvedTheme) -> Box<dyn Component> {
26 let mut stack = super::messages::ColumnStack::new();
27 for msg in &queue.steering {
28 stack.push(Box::new(Text::with_padding(
29 pending_line(PendingKind::Steering, &msg.text, th),
30 1,
31 0,
32 )));
33 }
34 for msg in &queue.follow_up {
35 stack.push(Box::new(Text::with_padding(
36 pending_line(PendingKind::FollowUp, &msg.text, th),
37 1,
38 0,
39 )));
40 }
41 if !queue.follow_up.is_empty() {
42 let mode = match queue.follow_up_mode {
43 super::state::QueueMode::All => "all queued follow-ups will send after this turn",
44 super::state::QueueMode::OneAtATime => "follow-ups send one at a time",
45 };
46 stack.push(Box::new(Text::with_padding(
47 th.fg(ThemeColor::Dim, mode),
48 1,
49 0,
50 )));
51 }
52 if stack.is_empty() {
53 stack.push(Box::new(Spacer::new(0)));
54 }
55 Box::new(stack)
56}
57
58fn pending_line(kind: PendingKind, text: &str, th: &ResolvedTheme) -> String {
59 let (glyph, label) = match kind {
60 PendingKind::Steering => ("↳", "steer"),
61 PendingKind::FollowUp => ("→", "queued"),
62 };
63 format!(
64 "{} {}",
65 th.fg(ThemeColor::Accent, glyph),
66 th.fg(ThemeColor::Muted, &format!("{label}: ")),
67 ) + text
68}
69
70#[must_use]
76pub fn build_auth_progress(progress: &AuthProgress, th: &ResolvedTheme) -> Box<dyn Component> {
77 let mut loader = Loader::new(
78 move |s: &str| theme::current().fg(ThemeColor::Accent, s),
79 move |s: &str| theme::current().fg(ThemeColor::Muted, s),
80 auth_stage_message(progress, th),
81 None,
82 );
83 loader.set_frame_index(0);
84 match progress.stage {
85 OAuthStage::Failed => Box::new(Text::with_padding(
86 th.fg(ThemeColor::Error, &auth_stage_message(progress, th)),
87 1,
88 0,
89 )),
90 OAuthStage::Done => Box::new(Text::with_padding(
91 th.fg(ThemeColor::Success, &auth_stage_message(progress, th)),
92 1,
93 0,
94 )),
95 _ => Box::new(loader),
96 }
97}
98
99#[must_use]
101pub fn auth_stage_message(progress: &AuthProgress, th: &ResolvedTheme) -> String {
102 match progress.stage {
103 OAuthStage::BrowserCallback => {
104 let base = format!("Opening browser to log in to {}…", progress.provider);
105 if let Some(url) = progress.detail.as_deref() {
106 format!("{} {}", base, th.fg(ThemeColor::MdLink, url))
107 } else {
108 base
109 }
110 }
111 OAuthStage::DeviceCode => {
112 let base = format!("Device flow for {} — enter code:", progress.provider);
113 if let Some(code) = progress.detail.as_deref() {
114 format!("{base} {}", theme::bold(code))
115 } else {
116 base
117 }
118 }
119 OAuthStage::ManualKey => format!("Enter API key for {}:", progress.provider),
120 OAuthStage::Exchanging => format!("Exchanging token for {}…", progress.provider),
121 OAuthStage::Done => format!("Logged in to {}.", progress.provider),
122 OAuthStage::Failed => format!("Failed to log in to {}.", progress.provider),
123 }
124}
125
126#[must_use]
132pub fn build_compaction_progress(
133 progress: &CompactionProgress,
134 th: &ResolvedTheme,
135) -> Box<dyn Component> {
136 let msg = super::status::compaction_message(progress.reason);
137 let accent = th.clone();
138 let muted = th.clone();
139 let mut loader = Loader::new(
140 move |s: &str| accent.fg(ThemeColor::Accent, s),
141 move |s: &str| muted.fg(ThemeColor::Muted, s),
142 msg,
143 None,
144 );
145 loader.set_frame_index(0);
146 Box::new(loader)
147}
148
149#[must_use]
151pub fn build_retry_progress(progress: &RetryProgress, th: &ResolvedTheme) -> Box<dyn Component> {
152 let msg =
153 super::status::retry_message(progress.attempt, progress.max_attempts, progress.seconds);
154 let warn = th.clone();
155 let muted = th.clone();
156 let mut loader = Loader::new(
157 move |s: &str| warn.fg(ThemeColor::Warning, s),
158 move |s: &str| muted.fg(ThemeColor::Muted, s),
159 msg,
160 None,
161 );
162 loader.set_frame_index(0);
163 Box::new(loader)
164}
165
166#[must_use]
168pub fn build_bash_progress(progress: &BashProgress, th: &ResolvedTheme) -> Box<dyn Component> {
169 super::messages::build_bash(
170 &super::messages::BashMessageView {
171 command: progress.command.clone(),
172 output: progress.output.clone(),
173 expanded: progress.expanded,
174 exit_code: progress.exit_code,
175 cancelled: progress.cancelled,
176 truncated: false,
177 full_output_path: None,
178 },
179 th,
180 )
181}