Skip to main content

par_term/
remote_shell_install_ui.rs

1//! Remote shell integration install confirmation dialog.
2//!
3//! Shows a confirmation dialog when the user selects "Install Shell Integration
4//! on Remote Host" from the Shell menu. Displays the exact curl command that will
5//! be sent to the active terminal and lets the user confirm or cancel.
6
7/// The install command URL
8const INSTALL_URL: &str = "https://paulrobello.github.io/par-term/install-shell-integration.sh";
9
10/// Action returned by the remote shell install dialog
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum RemoteShellInstallAction {
13    /// User confirmed - send the install command to the active terminal
14    Install,
15    /// User cancelled
16    Cancel,
17    /// No action yet (dialog still showing or not visible)
18    None,
19}
20
21/// State for the remote shell integration install dialog
22pub struct RemoteShellInstallUI {
23    /// Whether the dialog is visible
24    visible: bool,
25    /// Brief flash message after copy
26    copy_feedback: Option<std::time::Instant>,
27}
28
29impl Default for RemoteShellInstallUI {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl RemoteShellInstallUI {
36    /// Create a new remote shell install UI
37    pub fn new() -> Self {
38        Self {
39            visible: false,
40            copy_feedback: None,
41        }
42    }
43
44    /// Check if the dialog is currently visible
45    pub fn is_visible(&self) -> bool {
46        self.visible
47    }
48
49    /// Show the confirmation dialog
50    pub fn show_dialog(&mut self) {
51        self.visible = true;
52        self.copy_feedback = None;
53    }
54
55    /// Hide the dialog
56    fn hide(&mut self) {
57        self.visible = false;
58        self.copy_feedback = None;
59    }
60
61    /// Get the install command string
62    pub fn install_command() -> String {
63        format!("curl -sSL {} | sh", INSTALL_URL)
64    }
65
66    /// Render the dialog and return any action
67    pub fn show(&mut self, ctx: &egui::Context) -> RemoteShellInstallAction {
68        if !self.visible {
69            return RemoteShellInstallAction::None;
70        }
71
72        let mut action = RemoteShellInstallAction::None;
73        let command = Self::install_command();
74
75        // Request continuous repaints while dialog is visible to ensure clicks are processed
76        ctx.request_repaint();
77
78        egui::Window::new("Install Shell Integration on Remote Host")
79            .collapsible(false)
80            .resizable(false)
81            .order(egui::Order::Foreground)
82            .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
83            .show(ctx, |ui| {
84                ui.vertical_centered(|ui| {
85                    ui.add_space(10.0);
86
87                    ui.label(
88                        egui::RichText::new("Send Install Command to Terminal")
89                            .size(16.0)
90                            .strong(),
91                    );
92                    ui.add_space(8.0);
93
94                    ui.label("This will send the following command to the active terminal:");
95                    ui.add_space(8.0);
96
97                    // Command preview in a highlighted code block
98                    egui::Frame::new()
99                        .fill(egui::Color32::from_rgba_unmultiplied(40, 40, 40, 220))
100                        .inner_margin(egui::Margin::symmetric(12, 8))
101                        .corner_radius(4.0)
102                        .show(ui, |ui| {
103                            ui.label(
104                                egui::RichText::new(&command)
105                                    .color(egui::Color32::LIGHT_GREEN)
106                                    .monospace()
107                                    .size(13.0),
108                            );
109                        });
110
111                    ui.add_space(4.0);
112
113                    // Copy button on its own line
114                    let copy_label = if self
115                        .copy_feedback
116                        .is_some_and(|t| t.elapsed().as_millis() < 1500)
117                    {
118                        "Copied!"
119                    } else {
120                        "Copy Command"
121                    };
122                    if ui
123                        .button(egui::RichText::new(copy_label).size(12.0))
124                        .clicked()
125                    {
126                        ctx.copy_text(command.clone());
127                        self.copy_feedback = Some(std::time::Instant::now());
128                    }
129
130                    ui.add_space(10.0);
131
132                    // Warning
133                    ui.label(
134                        egui::RichText::new(
135                            "Only use this when SSH'd into a remote host that needs shell integration.",
136                        )
137                        .color(egui::Color32::YELLOW)
138                        .size(12.0),
139                    );
140
141                    ui.add_space(15.0);
142
143                    // Buttons
144                    ui.horizontal(|ui| {
145                        if ui.button("Install").clicked() {
146                            action = RemoteShellInstallAction::Install;
147                        }
148
149                        ui.add_space(10.0);
150
151                        if ui.button("Cancel").clicked() {
152                            action = RemoteShellInstallAction::Cancel;
153                        }
154                    });
155                    ui.add_space(10.0);
156                });
157            });
158
159        // Handle escape key to cancel
160        if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
161            action = RemoteShellInstallAction::Cancel;
162        }
163
164        // Handle enter key to confirm install
165        if ctx.input(|i| i.key_pressed(egui::Key::Enter)) {
166            action = RemoteShellInstallAction::Install;
167        }
168
169        // Hide dialog on any action (except None)
170        if !matches!(action, RemoteShellInstallAction::None) {
171            self.hide();
172        }
173
174        action
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_install_command_format() {
184        let cmd = RemoteShellInstallUI::install_command();
185        assert!(cmd.starts_with("curl"));
186        assert!(cmd.contains("paulrobello.github.io/par-term"));
187        assert!(cmd.contains("install-shell-integration.sh"));
188        assert!(cmd.ends_with("| sh"));
189    }
190
191    #[test]
192    fn test_dialog_initial_state() {
193        let ui = RemoteShellInstallUI::new();
194        assert!(!ui.is_visible());
195    }
196
197    #[test]
198    fn test_dialog_show_hide() {
199        let mut ui = RemoteShellInstallUI::new();
200        assert!(!ui.is_visible());
201
202        ui.show_dialog();
203        assert!(ui.is_visible());
204
205        ui.hide();
206        assert!(!ui.is_visible());
207    }
208
209    #[test]
210    fn test_default_impl() {
211        let ui = RemoteShellInstallUI::default();
212        assert!(!ui.is_visible());
213    }
214}