diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-22 19:12:29 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-22 19:12:29 +0300 |
| commit | 1c6397b1cddb288099ed212a5979732698bd72c2 (patch) | |
| tree | 0574b428903ea1ce106bc7ba36e213c0418aa9f4 /src/client/shell/machine_diagnostics.rs | |
| parent | 7237703dc59102cf3097e833eb896b314007b548 (diff) | |
fix: reuse ssh authentication and add machine recovery commands (#4490)
* fix: reuse ssh authentication and add in-app mfa recovery
refs #3763
* refactor: move ssh recovery to machine cli
refs #3763
Diffstat (limited to 'src/client/shell/machine_diagnostics.rs')
| -rw-r--r-- | src/client/shell/machine_diagnostics.rs | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/src/client/shell/machine_diagnostics.rs b/src/client/shell/machine_diagnostics.rs new file mode 100644 index 00000000..67983024 --- /dev/null +++ b/src/client/shell/machine_diagnostics.rs @@ -0,0 +1,130 @@ +use super::*; +use crate::raw_input::RawInputEvent; +use crossterm::event::{MouseButton, MouseEventKind}; + +#[derive(Default)] +pub(super) struct MachineDiagnostics { + errors: HashMap<ClientEndpointId, String>, + hover: Option<ClientEndpointId>, +} + +impl MachineDiagnostics { + pub(super) fn required_for(&self, endpoint: &ClientShellEndpoint) -> bool { + self.errors + .get(&endpoint.endpoint_id) + .is_some_and(|message| crate::remote::ssh_error_requires_authentication(message)) + } + + pub(super) fn badge_style( + &self, + endpoint: &ClientShellEndpoint, + palette: &Palette, + style: Style, + ) -> Style { + if self.hover.as_ref() == Some(&endpoint.endpoint_id) + && self.errors.contains_key(&endpoint.endpoint_id) + { + style + .bg(palette.active_row_bg) + .add_modifier(Modifier::REVERSED) + } else { + style + } + } +} + +impl ClientShellState { + pub(crate) fn set_machine_diagnostic(&mut self, id: &ClientEndpointId, message: String) { + if !id.is_local() { + self.machine_diagnostics.errors.insert( + id.clone(), + message + .chars() + .filter(|c| !c.is_control() || *c == '\n') + .take(4096) + .collect(), + ); + } + } + + pub(super) fn clear_machine_diagnostic(&mut self, id: &ClientEndpointId) { + self.machine_diagnostics.errors.remove(id); + } + + pub(super) fn handle_machine_badge_event( + &mut self, + event: &RawInputEvent, + outcome: &mut ClientShellInput, + ) -> bool { + let RawInputEvent::Mouse(mouse) = event else { + return false; + }; + if self.overlay.is_some() + || self.popup_pending + || self.hits.popup.is_some() + || contains(self.hits.notification_toast, (mouse.column, mouse.row)) + { + return false; + } + let hit = self + .hits + .machines + .iter() + .find(|hit| { + contains(hit.status_badge, (mouse.column, mouse.row)) + && self + .machine_diagnostics + .errors + .contains_key(&hit.endpoint_id) + }) + .map(|hit| hit.endpoint_id.clone()); + if mouse.kind == MouseEventKind::Moved { + if self.machine_diagnostics.hover != hit { + self.machine_diagnostics.hover = hit; + outcome.repaint = true; + } + return false; + } + let Some(id) = hit else { + return false; + }; + if mouse.kind == MouseEventKind::Up(MouseButton::Left) { + return true; + } + if mouse.kind != MouseEventKind::Down(MouseButton::Left) { + return false; + } + let Some(error) = self.machine_diagnostics.errors.get(&id) else { + return true; + }; + let ClientEndpointId::Ssh(profile_id) = &id else { + return true; + }; + let command = if crate::remote::ssh_error_requires_authentication(error) { + format!("herdr machine reconnect {profile_id}") + } else { + format!("herdr machine status {profile_id}") + }; + let code = format!("machine-diagnostic:{}", profile_id); + // An explicit click can reopen its diagnostic, but must not replace another notice. + if self + .visible_endpoint_notice + .as_ref() + .is_some_and(|notice| notice.key.code != code) + { + return true; + } + self.visible_endpoint_notice = Some(ClientVisibleEndpointNotice { + key: ClientEndpointNoticeKey { + boot_id: "machine".into(), + kind: ClientEndpointNoticeKind::Unavailable, + code, + }, + title: format!("{}: {command}", self.endpoint_label(&id)), + body: error.clone(), + deadline: std::time::Instant::now() + std::time::Duration::from_secs(15), + }); + outcome.repaint = true; + true + } +} |
