par_term/integrations_ui.rs
1//! Combined integrations welcome dialog.
2//!
3//! Shows on first run when integrations are not installed,
4//! offering to install shaders and/or shell integration.
5
6use crate::config::ShellType;
7use egui::{Align2, Color32, Context, Frame, RichText, Window, epaint::Shadow};
8
9/// User's response to the integrations dialog
10#[derive(Debug, Clone, Default)]
11pub struct IntegrationsResponse {
12 /// User wants to install shaders
13 pub install_shaders: bool,
14 /// User wants to install shell integration
15 pub install_shell_integration: bool,
16 /// User clicked Skip (dismiss for this session)
17 pub skipped: bool,
18 /// User clicked Never Ask
19 pub never_ask: bool,
20 /// Dialog was closed
21 pub closed: bool,
22 /// User responded to shader overwrite prompt
23 pub shader_conflict_action: Option<ShaderConflictAction>,
24}
25
26/// Action chosen when modified shaders are detected
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ShaderConflictAction {
29 /// Overwrite modified bundled shaders
30 Overwrite,
31 /// Keep user-modified shaders (skip overwrite)
32 SkipModified,
33 /// Cancel the installation flow
34 Cancel,
35}
36
37/// Combined integrations welcome dialog
38pub struct IntegrationsUI {
39 /// Whether the dialog is visible
40 pub visible: bool,
41 /// Whether shaders checkbox is checked
42 pub shaders_checked: bool,
43 /// Whether shell integration checkbox is checked
44 pub shell_integration_checked: bool,
45 /// Detected shell type
46 pub detected_shell: ShellType,
47 /// Whether installation is in progress
48 pub installing: bool,
49 /// Installation progress message
50 pub progress_message: Option<String>,
51 /// Installation error message
52 pub error_message: Option<String>,
53 /// Installation success message
54 pub success_message: Option<String>,
55 /// Whether we're waiting for user decision on modified shaders
56 pub awaiting_shader_overwrite: bool,
57 /// List of modified bundled shader files detected
58 pub shader_conflicts: Vec<String>,
59 /// Pending install request flags preserved while waiting for confirmation
60 pub pending_install_shaders: bool,
61 pub pending_install_shell_integration: bool,
62}
63
64impl IntegrationsUI {
65 /// Create a new integrations UI
66 pub fn new() -> Self {
67 Self {
68 visible: false,
69 shaders_checked: true,
70 shell_integration_checked: true,
71 detected_shell: ShellType::detect(),
72 installing: false,
73 progress_message: None,
74 error_message: None,
75 success_message: None,
76 awaiting_shader_overwrite: false,
77 shader_conflicts: Vec::new(),
78 pending_install_shaders: false,
79 pending_install_shell_integration: false,
80 }
81 }
82
83 /// Show the dialog
84 pub fn show_dialog(&mut self) {
85 self.visible = true;
86 self.installing = false;
87 self.progress_message = None;
88 self.error_message = None;
89 self.success_message = None;
90 // Re-detect shell when showing dialog
91 self.detected_shell = ShellType::detect();
92 self.awaiting_shader_overwrite = false;
93 self.shader_conflicts.clear();
94 self.pending_install_shaders = false;
95 self.pending_install_shell_integration = false;
96 }
97
98 /// Hide the dialog
99 pub fn hide(&mut self) {
100 self.visible = false;
101 }
102
103 /// Render the integrations dialog
104 /// Returns the user's response
105 pub fn show(&mut self, ctx: &Context) -> IntegrationsResponse {
106 if !self.visible {
107 return IntegrationsResponse::default();
108 }
109
110 let mut response = IntegrationsResponse::default();
111
112 // Ensure dialog is fully opaque
113 let mut style = (*ctx.style()).clone();
114 let solid_bg = Color32::from_rgba_unmultiplied(32, 32, 32, 255);
115 style.visuals.window_fill = solid_bg;
116 style.visuals.panel_fill = solid_bg;
117 style.visuals.widgets.noninteractive.bg_fill = solid_bg;
118 ctx.set_style(style);
119
120 let viewport = ctx.input(|i| i.viewport_rect());
121
122 Window::new("Welcome to par-term")
123 .resizable(false)
124 .collapsible(false)
125 .default_width(500.0)
126 .default_pos(viewport.center())
127 .pivot(Align2::CENTER_CENTER)
128 .frame(
129 Frame::window(&ctx.style())
130 .fill(solid_bg)
131 .inner_margin(24.0)
132 .stroke(egui::Stroke::new(1.0, Color32::from_gray(80)))
133 .shadow(Shadow {
134 offset: [4, 4],
135 blur: 16,
136 spread: 4,
137 color: Color32::from_black_alpha(180),
138 }),
139 )
140 .show(ctx, |ui| {
141 ui.vertical_centered(|ui| {
142 // Header with version
143 ui.add_space(8.0);
144 ui.label(
145 RichText::new(format!("Welcome to par-term v{}", env!("CARGO_PKG_VERSION")))
146 .size(22.0)
147 .strong(),
148 );
149 ui.add_space(8.0);
150 ui.label(
151 RichText::new("A GPU-accelerated terminal emulator")
152 .size(14.0)
153 .weak(),
154 );
155 ui.add_space(4.0);
156 ui.hyperlink_to(
157 RichText::new("View Changelog").size(12.0),
158 "https://github.com/paulrobello/par-term/blob/main/CHANGELOG.md",
159 );
160 ui.add_space(16.0);
161 });
162
163 // Description
164 ui.label("par-term includes optional integrations to enhance your experience:");
165 ui.add_space(16.0);
166
167 // Show installation progress/error/success
168 if self.installing {
169 ui.horizontal(|ui| {
170 ui.spinner();
171 ui.label(
172 self.progress_message
173 .as_deref()
174 .unwrap_or("Installing..."),
175 );
176 });
177 ui.add_space(16.0);
178 } else if let Some(error) = &self.error_message {
179 ui.colored_label(Color32::from_rgb(255, 100, 100), error);
180 ui.add_space(8.0);
181 } else if let Some(success) = &self.success_message {
182 ui.colored_label(Color32::from_rgb(100, 255, 100), success);
183 ui.add_space(8.0);
184 ui.label("You can configure these in Settings (F12).");
185 ui.add_space(16.0);
186 }
187
188 // Checkboxes for integrations (only show when not installing/succeeded)
189 if !self.installing && self.success_message.is_none() {
190 if self.awaiting_shader_overwrite {
191 ui.group(|ui| {
192 ui.vertical(|ui| {
193 ui.label(RichText::new("Modified shaders detected").strong());
194 if self.shader_conflicts.is_empty() {
195 ui.label(
196 RichText::new(
197 "Some bundled shaders were modified. Overwrite or keep your versions?",
198 )
199 .weak(),
200 );
201 } else {
202 ui.label(RichText::new(
203 format!(
204 "{} modified files found. Overwrite them or keep your changes?",
205 self.shader_conflicts.len()
206 ),
207 ));
208 let preview: Vec<_> = self
209 .shader_conflicts
210 .iter()
211 .take(5)
212 .cloned()
213 .collect();
214 ui.label(
215 RichText::new(preview.join(", "))
216 .small()
217 .weak(),
218 );
219 if self.shader_conflicts.len() > 5 {
220 ui.label(
221 RichText::new("…and more")
222 .small()
223 .weak(),
224 );
225 }
226 }
227 });
228 });
229 ui.add_space(16.0);
230 } else {
231 // Shaders checkbox with description
232 ui.group(|ui| {
233 ui.horizontal(|ui| {
234 ui.checkbox(&mut self.shaders_checked, "");
235 ui.vertical(|ui| {
236 ui.label(RichText::new("Custom Shaders").strong());
237 ui.label(
238 RichText::new(
239 "49+ background shaders (CRT, Matrix, plasma) and \
240 12 cursor effects (trails, glows)",
241 )
242 .weak()
243 .small(),
244 );
245 });
246 });
247 });
248
249 ui.add_space(8.0);
250
251 // Shell integration checkbox with description
252 ui.group(|ui| {
253 ui.horizontal(|ui| {
254 ui.checkbox(&mut self.shell_integration_checked, "");
255 ui.vertical(|ui| {
256 let shell_name = self.detected_shell.display_name();
257 let label = if self.detected_shell == ShellType::Unknown {
258 "Shell Integration".to_string()
259 } else {
260 format!("Shell Integration ({})", shell_name)
261 };
262 ui.label(RichText::new(label).strong());
263 ui.label(
264 RichText::new(
265 "Current directory tracking, command markers, \
266 and semantic prompt zones",
267 )
268 .weak()
269 .small(),
270 );
271 if self.detected_shell == ShellType::Unknown {
272 ui.label(
273 RichText::new(
274 "Note: Could not detect shell. Manual setup may be required.",
275 )
276 .weak()
277 .italics()
278 .small(),
279 );
280 }
281 });
282 });
283 });
284
285 ui.add_space(20.0);
286 }
287 }
288
289 // Buttons (centered)
290 ui.vertical_centered(|ui| {
291 if !self.installing && self.success_message.is_none() {
292 ui.horizontal(|ui| {
293 let button_width = 130.0;
294
295 if self.awaiting_shader_overwrite {
296 if ui
297 .add_sized(
298 [button_width + 20.0, 32.0],
299 egui::Button::new("Overwrite modified"),
300 )
301 .clicked()
302 {
303 response.shader_conflict_action =
304 Some(ShaderConflictAction::Overwrite);
305 }
306
307 ui.add_space(8.0);
308
309 if ui
310 .add_sized(
311 [button_width + 10.0, 32.0],
312 egui::Button::new("Skip modified"),
313 )
314 .clicked()
315 {
316 response.shader_conflict_action =
317 Some(ShaderConflictAction::SkipModified);
318 }
319
320 ui.add_space(8.0);
321
322 if ui
323 .add_sized([button_width, 32.0], egui::Button::new("Cancel"))
324 .clicked()
325 {
326 response.shader_conflict_action =
327 Some(ShaderConflictAction::Cancel);
328 }
329 } else {
330 // Install Selected button (only if something is checked)
331 let can_install =
332 self.shaders_checked || self.shell_integration_checked;
333 ui.add_enabled_ui(can_install, |ui| {
334 if ui
335 .add_sized(
336 [button_width, 32.0],
337 egui::Button::new("Install Selected"),
338 )
339 .clicked()
340 {
341 response.install_shaders = self.shaders_checked;
342 response.install_shell_integration =
343 self.shell_integration_checked;
344 }
345 });
346
347 ui.add_space(8.0);
348
349 if ui
350 .add_sized([button_width, 32.0], egui::Button::new("Skip"))
351 .on_hover_text("Dismiss for this session")
352 .clicked()
353 {
354 response.skipped = true;
355 }
356
357 ui.add_space(8.0);
358
359 if ui
360 .add_sized([button_width, 32.0], egui::Button::new("Never Ask"))
361 .on_hover_text("Don't ask again for these integrations")
362 .clicked()
363 {
364 response.never_ask = true;
365 }
366 }
367 });
368 } else if self.success_message.is_some() {
369 // Show OK button after successful install
370 if ui
371 .add_sized([120.0, 32.0], egui::Button::new("OK"))
372 .clicked()
373 {
374 self.visible = false;
375 response.closed = true;
376 }
377 }
378 });
379
380 ui.add_space(12.0);
381
382 // Help text
383 if !self.installing
384 && self.success_message.is_none()
385 && self.error_message.is_none()
386 {
387 ui.vertical_centered(|ui| {
388 let msg = if self.awaiting_shader_overwrite {
389 "Choose how to handle modified shaders to continue installation"
390 } else {
391 "You can install these later via CLI or Settings (F12)"
392 };
393 ui.label(RichText::new(msg).weak().small());
394 });
395 }
396 });
397
398 response
399 }
400
401 /// Set installation in progress
402 pub fn set_installing(&mut self, message: &str) {
403 self.installing = true;
404 self.progress_message = Some(message.to_string());
405 self.error_message = None;
406 }
407
408 /// Set installation error
409 pub fn set_error(&mut self, error: &str) {
410 self.installing = false;
411 self.progress_message = None;
412 self.error_message = Some(error.to_string());
413 }
414
415 /// Set installation success
416 pub fn set_success(&mut self, message: &str) {
417 self.installing = false;
418 self.progress_message = None;
419 self.error_message = None;
420 self.success_message = Some(message.to_string());
421 }
422}
423
424impl Default for IntegrationsUI {
425 fn default() -> Self {
426 Self::new()
427 }
428}