added auto update & changed config path

This commit is contained in:
Uber Veng
2026-05-21 22:59:02 +07:00
parent 1599cfcafa
commit f2349c9f15
7 changed files with 435 additions and 7 deletions

View File

@@ -4,6 +4,7 @@ use crate::error::Result;
use crate::executor;
use crate::menu::{Action, InteractionMode, MenuItem, MenuItemKind, MenuRoot};
use crate::network;
use crate::updater;
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEventKind};
use tokio::sync::mpsc::UnboundedSender;
@@ -16,6 +17,10 @@ pub enum Event {
StructuredCommand(executor::StructuredCommand),
StructuredOutput(String),
StructuredFinished(i32),
UpdateAvailable(updater::UpdateInfo),
UpdateProgress(u64),
UpdateDone,
UpdateError(String),
}
/// A pending request to hand the terminal to an external interactive process.
@@ -35,6 +40,7 @@ pub struct App {
pub event_tx: UnboundedSender<Event>,
/// When set, main loop suspends ratatui, runs the command, then restores.
pub pending_exec: Option<PendingExec>,
pub pending_restart: bool,
}
pub enum Popup {
@@ -62,6 +68,22 @@ pub enum Popup {
},
Downloading { progress: f32, message: String },
Message { text: String, level: MessageLevel },
UpdateConfirm {
info: updater::UpdateInfo,
},
Updating {
info: updater::UpdateInfo,
downloaded: u64,
status: UpdatingStatus,
},
}
#[derive(Debug, PartialEq)]
pub enum UpdatingStatus {
Downloading,
Applying,
Done,
Failed(String),
}
#[derive(Debug, Clone, Copy)]
@@ -82,9 +104,25 @@ impl App {
popup: None,
event_tx,
pending_exec: None,
pending_restart: false,
}
}
pub fn check_update(&mut self) {
let api_base = match &self.config.update_api {
Some(url) => url.clone(),
None => return,
};
let timeout = self.config.timeout_sec;
let tx = self.event_tx.clone();
tokio::spawn(async move {
if let Ok(Some(info)) = updater::check(&api_base, timeout).await {
let _ = tx.send(Event::UpdateAvailable(info));
}
});
}
pub async fn load_menu(&mut self) {
let server_url = self.config.server_url.clone();
let timeout = self.config.timeout_sec;
@@ -190,6 +228,32 @@ impl App {
// script), we silently discard the exit event.
Ok(false)
}
Event::UpdateAvailable(info) => {
// Only show if no popup is currently open (don't interrupt running scripts)
if self.popup.is_none() {
self.popup = Some(Popup::UpdateConfirm { info });
}
Ok(false)
}
Event::UpdateProgress(bytes) => {
if let Some(Popup::Updating { downloaded, .. }) = &mut self.popup {
*downloaded = bytes;
}
Ok(false)
}
Event::UpdateDone => {
if let Some(Popup::Updating { status, .. }) = &mut self.popup {
*status = UpdatingStatus::Done;
}
self.pending_restart = true;
Ok(false)
}
Event::UpdateError(msg) => {
if let Some(Popup::Updating { status, .. }) = &mut self.popup {
*status = UpdatingStatus::Failed(msg);
}
Ok(false)
}
}
}
@@ -347,6 +411,66 @@ impl App {
self.popup = None;
return Ok(false);
}
Popup::UpdateConfirm { info } => {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
let info = info.clone();
self.popup = Some(Popup::Updating {
info: info.clone(),
downloaded: 0,
status: UpdatingStatus::Downloading,
});
let (progress_tx, mut progress_rx) =
tokio::sync::mpsc::unbounded_channel::<u64>();
let tx = self.event_tx.clone();
let info_clone = info.clone();
// Forward progress events
tokio::spawn(async move {
while let Some(bytes) = progress_rx.recv().await {
let _ = tx.send(Event::UpdateProgress(bytes));
}
});
// Download and apply
let tx2 = self.event_tx.clone();
tokio::spawn(async move {
match updater::download_and_apply(
&info_clone,
progress_tx,
)
.await
{
Ok(()) => {
let _ = tx2.send(Event::UpdateDone);
}
Err(e) => {
let _ =
tx2.send(Event::UpdateError(e.to_string()));
}
}
});
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
self.popup = None;
}
_ => {}
}
return Ok(false);
}
Popup::Updating { status, .. } => {
if key.code == KeyCode::Esc {
if matches!(status, UpdatingStatus::Failed(_)) {
self.popup = None;
}
// Ignore Esc while downloading/applying
}
return Ok(false);
}
_ => {}
}
}