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