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 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}