Files
openbitdo/sdk/crates/bitdo_tui/src/runtime/effect_executor.rs

315 lines
12 KiB
Rust

use crate::app::effect::{Effect, MappingApplyDraft};
use crate::app::event::AppEvent;
use crate::app::state::AppState;
use crate::persistence::ui_state::persist_ui_state;
use crate::support_report::persist_support_report;
use bitdo_app_core::{
FirmwareCancelRequest, FirmwareConfirmRequest, FirmwarePreflightRequest, FirmwareStartRequest,
OpenBitdoCore, U2SlotId,
};
use std::path::Path;
pub async fn execute_effect(
core: &OpenBitdoCore,
state: &AppState,
effect: Effect,
) -> Vec<AppEvent> {
match effect {
Effect::RefreshDevices => match core.list_devices().await {
Ok(mut devices) => {
devices.sort_by_key(|d| (d.vid_pid.vid, d.vid_pid.pid));
vec![AppEvent::DevicesLoaded(devices)]
}
Err(err) => vec![AppEvent::DevicesLoadFailed(err.to_string())],
},
Effect::RunDiagnostics { vid_pid } => match core.diag_probe(vid_pid).await {
Ok(result) => {
let summary = state
.devices
.iter()
.find(|device| device.vid_pid == vid_pid)
.map(|device| core.beginner_diag_summary(device, &result))
.unwrap_or_else(|| "Diagnostics completed".to_owned());
vec![AppEvent::DiagnosticsCompleted {
vid_pid,
result,
summary,
}]
}
Err(err) => vec![AppEvent::DiagnosticsFailed {
vid_pid,
error: err.to_string(),
}],
},
Effect::LoadMappings { vid_pid } => {
let device = state.devices.iter().find(|d| d.vid_pid == vid_pid);
if let Some(device) = device {
if device.capability.supports_jp108_dedicated_map {
match core.jp108_read_dedicated_mapping(vid_pid).await {
Ok(mappings) => vec![AppEvent::MappingsLoadedJp108 { vid_pid, mappings }],
Err(err) => vec![AppEvent::MappingLoadFailed(err.to_string())],
}
} else if device.capability.supports_u2_button_map
&& device.capability.supports_u2_slot_config
{
match core.u2_read_core_profile(vid_pid, U2SlotId::Slot1).await {
Ok(profile) => vec![AppEvent::MappingsLoadedUltimate2 { vid_pid, profile }],
Err(err) => vec![AppEvent::MappingLoadFailed(err.to_string())],
}
} else {
vec![AppEvent::MappingLoadFailed(
"Device does not support mapping editor".to_owned(),
)]
}
} else {
vec![AppEvent::MappingLoadFailed("No device selected".to_owned())]
}
}
Effect::ApplyMappings { vid_pid, draft } => match draft {
MappingApplyDraft::Jp108(mappings) => match core
.jp108_apply_dedicated_mapping_with_recovery(vid_pid, mappings, true)
.await
{
Ok(report) => {
let recovery_lock = report.rollback_failed();
let message = if report.write_applied {
"JP108 mapping applied".to_owned()
} else if recovery_lock {
"Apply failed and rollback failed; writes locked until restart".to_owned()
} else {
"Apply failed but rollback restored prior mapping".to_owned()
};
vec![AppEvent::MappingApplied {
backup_id: report.backup_id,
message,
recovery_lock,
}]
}
Err(err) => vec![AppEvent::MappingApplyFailed(err.to_string())],
},
MappingApplyDraft::Ultimate2(profile) => match core
.u2_apply_core_profile_with_recovery(
vid_pid,
profile.slot,
profile.mode,
profile.mappings,
profile.l2_analog,
profile.r2_analog,
true,
)
.await
{
Ok(report) => {
let recovery_lock = report.rollback_failed();
let message = if report.write_applied {
"Ultimate2 profile applied".to_owned()
} else if recovery_lock {
"Apply failed and rollback failed; writes locked until restart".to_owned()
} else {
"Apply failed but rollback restored prior profile".to_owned()
};
vec![AppEvent::MappingApplied {
backup_id: report.backup_id,
message,
recovery_lock,
}]
}
Err(err) => vec![AppEvent::MappingApplyFailed(err.to_string())],
},
},
Effect::RestoreBackup { backup_id } => match core.restore_backup(backup_id).await {
Ok(_) => vec![AppEvent::BackupRestoreCompleted(
"Backup restore completed".to_owned(),
)],
Err(err) => vec![AppEvent::BackupRestoreFailed(format!(
"Backup restore failed: {err}"
))],
},
Effect::PreparePreflight {
vid_pid,
firmware_path_override,
allow_unsafe,
brick_risk_ack,
experimental,
chunk_size,
} => {
let device = state.devices.iter().find(|d| d.vid_pid == vid_pid);
let Some(device) = device else {
return vec![AppEvent::PreflightBlocked("No selected device".to_owned())];
};
let (firmware_path, source, version, downloaded_firmware_path) =
if let Some(path) = firmware_path_override {
(path, "local file".to_owned(), "manual".to_owned(), None)
} else {
match core.download_recommended_firmware(vid_pid).await {
Ok(download) => {
let path = download.firmware_path;
(
path.clone(),
"recommended verified download".to_owned(),
download.version,
Some(path),
)
}
Err(err) => {
return vec![AppEvent::PreflightBlocked(format!(
"Recommended firmware unavailable: {err}"
))]
}
}
};
match core
.preflight_firmware(FirmwarePreflightRequest {
vid_pid: device.vid_pid,
firmware_path: firmware_path.clone(),
allow_unsafe,
brick_risk_ack,
experimental,
chunk_size,
})
.await
{
Ok(preflight) => {
if !preflight.gate.allowed {
if let Some(path) = downloaded_firmware_path.as_ref() {
let _ = cleanup_temp_file(path).await;
}
vec![AppEvent::PreflightBlocked(
preflight
.gate
.message
.unwrap_or_else(|| "Preflight denied by policy".to_owned()),
)]
} else if let Some(plan) = preflight.plan {
vec![AppEvent::PreflightReady {
vid_pid,
firmware_path,
source,
version,
plan,
downloaded_firmware_path,
}]
} else {
if let Some(path) = downloaded_firmware_path.as_ref() {
let _ = cleanup_temp_file(path).await;
}
vec![AppEvent::PreflightBlocked(
"Preflight allowed but no transfer plan was returned".to_owned(),
)]
}
}
Err(err) => {
if let Some(path) = downloaded_firmware_path.as_ref() {
let _ = cleanup_temp_file(path).await;
}
vec![AppEvent::PreflightBlocked(format!(
"Preflight failed: {err}"
))]
}
}
}
Effect::StartFirmware {
session_id,
acknowledged_risk,
} => {
if let Err(err) = core
.start_firmware(FirmwareStartRequest {
session_id: session_id.clone(),
})
.await
{
return vec![AppEvent::UpdateFailed(err.to_string())];
}
if let Err(err) = core
.confirm_firmware(FirmwareConfirmRequest {
session_id: session_id.clone(),
acknowledged_risk,
})
.await
{
return vec![AppEvent::UpdateFailed(err.to_string())];
}
vec![AppEvent::UpdateStarted {
session_id: session_id.0,
source: "selected firmware".to_owned(),
version: "target".to_owned(),
}]
}
Effect::CancelFirmware { session_id } => match core
.cancel_firmware(FirmwareCancelRequest { session_id })
.await
{
Ok(report) => vec![AppEvent::UpdateFinished(report)],
Err(err) => vec![AppEvent::UpdateFailed(err.to_string())],
},
Effect::PollFirmwareReport { session_id } => {
match core.firmware_report(&session_id.0).await {
Ok(Some(report)) => vec![AppEvent::UpdateFinished(report)],
Ok(None) => Vec::new(),
Err(err) => vec![AppEvent::UpdateFailed(err.to_string())],
}
}
Effect::DeleteTempFile { path } => match cleanup_temp_file(&path).await {
Ok(_) => Vec::new(),
Err(err) => vec![AppEvent::Error(format!(
"Failed to delete temporary firmware {}: {err}",
path.display()
))],
},
Effect::PersistSettings {
path,
advanced_mode,
report_save_mode,
device_filter_text,
dashboard_layout_mode,
last_panel_focus,
} => match persist_ui_state(
&path,
advanced_mode,
report_save_mode,
device_filter_text,
dashboard_layout_mode,
last_panel_focus,
) {
Ok(_) => vec![AppEvent::SettingsPersisted],
Err(err) => vec![AppEvent::Error(format!("Settings save failed: {err}"))],
},
Effect::PersistSupportReport {
operation,
vid_pid,
status,
message,
diag,
firmware,
} => {
let device = vid_pid.and_then(|id| state.devices.iter().find(|d| d.vid_pid == id));
match persist_support_report(
&operation,
device,
&status,
message,
diag.as_ref(),
firmware.as_ref(),
)
.await
{
Ok(path) => vec![AppEvent::SupportReportSaved(path)],
Err(err) => vec![AppEvent::Error(format!(
"Support report save failed: {err}"
))],
}
}
}
}
async fn cleanup_temp_file(path: &Path) -> std::io::Result<()> {
match tokio::fs::remove_file(path).await {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err),
}
}