use std::path::Path;
use egui::{RichText, Ui};
use rigtop_core::hosts::HostKind;
use rigtop_core::update::{self, UpdateJob};
use crate::app::{HostEntry, ViewAction};
use crate::widgets::{BAD, DIM, OK, WARN};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReleaseNote {
Available(String),
Installed(String),
}
pub fn release_button(ui: &mut Ui, note: Option<&ReleaseNote>, actions: &mut Vec<ViewAction>) {
let (text, hint, action) = match note {
Some(ReleaseNote::Available(version)) => (
format!("rigtop {version} · update…"),
"a new release of rigtop is out: update this machine's copy",
ViewAction::SelfUpdate,
),
Some(ReleaseNote::Installed(version)) => (
format!("restart for rigtop {version}"),
"the new release is installed: restart rigtop to use it",
ViewAction::Restart,
),
None => return,
};
if ui
.add(egui::Button::new(RichText::new(text).color(OK)).frame(false))
.on_hover_text(hint)
.clicked()
{
actions.push(action);
}
}
pub fn agents_button(ui: &mut Ui, count: usize, actions: &mut Vec<ViewAction>) {
if count == 0 {
return;
}
if ui
.add(
egui::Button::new(RichText::new(format!("update agents ({count})…")).color(WARN))
.frame(false),
)
.on_hover_text("update rigtop on every remote host that runs an older version")
.clicked()
{
actions.push(ViewAction::UpdateAllAgents);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Target {
Host(usize),
Local(String),
Hosts(Vec<usize>),
}
type HostJob = (usize, String, UpdateJob, Option<Result<String, String>>);
enum Step {
Confirm,
Running(UpdateJob),
Done(Result<String, String>),
Many(Vec<HostJob>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdateEvent {
Reconnect(usize),
Installed(String),
Restart,
}
#[derive(Default)]
pub struct UpdateDialog {
open: Option<(Target, String, Step)>,
}
impl UpdateDialog {
pub fn ask(&mut self, index: usize, name: &str) {
self.open_on(Target::Host(index), name);
}
pub fn ask_all(&mut self, indexes: Vec<usize>) {
let name = format!("{} hosts", indexes.len());
self.open_on(Target::Hosts(indexes), &name);
}
pub fn ask_self(&mut self, version: &str) {
self.open_on(Target::Local(version.to_owned()), "this machine");
}
fn open_on(&mut self, target: Target, name: &str) {
let running = match &self.open {
Some((_, _, Step::Running(_))) => true,
Some((_, _, Step::Many(jobs))) => jobs.iter().any(|j| j.3.is_none()),
_ => false,
};
if !running {
self.open = Some((target, name.to_owned(), Step::Confirm));
}
}
pub fn show(
&mut self,
ctx: &egui::Context,
hosts: &[HostEntry],
exe: Option<&Path>,
) -> Vec<UpdateEvent> {
let mut events = Vec::new();
let Some((target, name, step)) = self.open.as_mut() else {
return events;
};
let (target, name) = (target.clone(), name.clone());
if let Step::Running(job) = step
&& let Some(result) = job.poll()
{
if result.is_ok() {
events.push(match &target {
Target::Host(index) => UpdateEvent::Reconnect(*index),
Target::Local(version) => UpdateEvent::Installed(version.clone()),
Target::Hosts(_) => unreachable!("runs as Step::Many"),
});
}
*step = Step::Done(result);
}
if let Step::Many(jobs) = step {
for (index, _, job, outcome) in jobs.iter_mut().filter(|j| j.3.is_none()) {
if let Some(result) = job.poll() {
if result.is_ok() {
events.push(UpdateEvent::Reconnect(*index));
}
*outcome = Some(result);
}
}
}
let version_of = |index: usize| {
hosts
.get(index)
.and_then(|h| h.state.snapshot.as_ref())
.map(|s| (s.host.rigtop_version.clone(), s.host.os.clone()))
.unwrap_or_default()
};
let (agent_version, os) = match &target {
Target::Host(index) => version_of(*index),
Target::Local(_) | Target::Hosts(_) => Default::default(),
};
let unsupported = match &target {
Target::Host(_) => update::unsupported_reason(&os),
Target::Local(_) => update::self_update_unsupported(),
Target::Hosts(_) => None,
};
let mut close = false;
let mut start = false;
let mut restart = false;
let screen = ctx.content_rect();
egui::Window::new("Update rigtop")
.collapsible(false)
.resizable(false)
.default_width(420.0)
.pivot(egui::Align2::CENTER_CENTER)
.default_pos(screen.center())
.show(ctx, |ui| match step {
Step::Confirm => match &unsupported {
Some(reason) => {
ui.label(format!("{name} cannot be updated from here: {reason}."));
close = ui.button("close").clicked();
}
None => {
ui.label(match &target {
Target::Host(_) => update::describe(&name, &agent_version),
Target::Local(version) => update::describe_self(version),
Target::Hosts(indexes) => {
let names: Vec<(String, String)> = indexes
.iter()
.filter_map(|&i| {
Some((hosts.get(i)?.spec.name.clone(), version_of(i).0))
})
.collect();
update::describe_many(&names)
}
});
ui.add_space(6.0);
ui.horizontal(|ui| {
start = ui.button("update").clicked();
close = ui.button("cancel").clicked();
});
}
},
Step::Running(_) => {
ui.horizontal(|ui| {
ui.spinner();
ui.label(format!("Updating rigtop on {name}…"));
});
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(250));
}
Step::Many(jobs) => {
for (_, host, _, result) in jobs.iter() {
ui.horizontal(|ui| match result {
None => {
ui.spinner();
ui.label(format!("{host}: updating…"));
}
Some(Ok(message)) => {
ui.label(RichText::new(format!("{host}: {message}")).color(OK));
}
Some(Err(error)) => {
ui.label(
RichText::new(format!("{host}: not updated: {error}"))
.color(BAD),
);
}
});
}
if jobs.iter().all(|j| j.3.is_some()) {
ui.label(RichText::new("Updated hosts reconnect.").color(DIM));
close = ui.button("close").clicked();
} else {
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(250));
}
}
Step::Done(result) => {
outcome(ui, &target, result);
ui.horizontal(|ui| {
if result.is_ok() && matches!(target, Target::Local(_)) {
restart = ui.button("restart now").clicked();
close = ui.button("later").clicked();
} else {
close = ui.button("close").clicked();
}
});
}
});
if start {
*step = match &target {
Target::Hosts(indexes) => Step::Many(
indexes
.iter()
.filter_map(|&i| match &hosts.get(i)?.spec.kind {
HostKind::Remote(remote) => Some((
i,
hosts[i].spec.name.clone(),
UpdateJob::start(remote),
None,
)),
_ => None,
})
.collect(),
),
Target::Host(index) => match hosts.get(*index).map(|h| &h.spec.kind) {
Some(HostKind::Remote(remote)) => Step::Running(UpdateJob::start(remote)),
_ => Step::Done(Err(format!("{name} is not a remote host"))),
},
Target::Local(version) => match exe {
Some(exe) => Step::Running(UpdateJob::start_self(version, exe)),
None => Step::Done(Err("the running program was not found".to_owned())),
},
};
}
if close || restart {
self.open = None;
}
if restart {
events.push(UpdateEvent::Restart);
}
events
}
}
fn outcome(ui: &mut Ui, target: &Target, result: &Result<String, String>) {
match result {
Ok(message) => {
ui.label(RichText::new(format!("Updated: {message}.")).color(OK));
let next = match target {
Target::Host(_) | Target::Hosts(_) => "Reconnecting to the new version…",
Target::Local(_) => "Restart rigtop to use it.",
};
ui.label(RichText::new(next).color(DIM));
}
Err(error) => {
ui.label(RichText::new(format!("Not updated: {error}")).color(BAD));
}
}
}