Skip to main content

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}