Skip to content

Commit 8ce8e25

Browse files
committed
Added the features selection to the CLI wizard
Added the features selection to the GUI wizard fixed store wizard total step count updated error handling windows gui installation now locks the mutex update the logic of reading config from the file and overriding it with cli args
1 parent 1a4747d commit 8ce8e25

File tree

18 files changed

+1153
-171
lines changed

18 files changed

+1153
-171
lines changed

src-tauri/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ required-features = ["offline"]
5151
tauri-build = { version = "2.3.1", features = [], optional = true }
5252

5353
[dependencies]
54-
reqwest = "0.12.4"
54+
reqwest = {version = "0.12.4", features = ["blocking"] }
5555
serde = { version = "1.0", features = ["derive"] }
5656
serde_derive = "1.0"
5757
serde_json = "1.0"

src-tauri/locales/app.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ wizard.idf.user_cancelled:
7171
wizard.idf.cloning:
7272
en: Cloning ESP-IDF
7373
cn: 正在克隆 ESP-IDF
74+
wizard.idf.submodule_finish:
75+
en: "IDF submodule correctly downloaded to:"
76+
cn: "IDF 子模块已正确下载到:"
7477
wizard.idf_version.selected:
7578
en: "Selected IDF version: %{version}"
7679
cn: "已选择 IDF 版本: %{version}"

src-tauri/src/cli/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ pub async fn run_cli(cli: Cli) -> anyhow::Result<()> {
168168
let result = wizard::run_wizzard_run(settings).await;
169169
match result {
170170
Ok(r) => {
171-
info!("{}", t!("install.wizard_result"));
171+
info!("{}", t!("install.wizard_result", r = "Ok".to_string()));
172172
info!("{}", t!("install.success"));
173173
info!("{}", t!("install.ready"));
174174
if !do_not_track {

src-tauri/src/cli/prompts.rs

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use std::path::PathBuf;
33
use crate::cli::helpers::{
44
first_defaulted_multiselect, generic_confirm, generic_input, generic_select, run_with_spinner,
55
};
6-
use idf_im_lib::settings::Settings;
6+
use dialoguer::theme::ColorfulTheme;
7+
use dialoguer::MultiSelect;
8+
use idf_im_lib::idf_features::FeatureInfo;
9+
use idf_im_lib::{idf_features::RequirementsMetadata, settings::Settings};
710
use idf_im_lib::system_dependencies;
811
use log::{debug, info};
912
use rust_i18n::t;
@@ -265,3 +268,191 @@ pub fn save_config_if_desired(config: &Settings) -> Result<(), String> {
265268
}
266269
Ok(())
267270
}
271+
272+
/// Select features from requirements metadata with interactive or non-interactive mode
273+
///
274+
/// # Arguments
275+
/// * `metadata` - The requirements metadata containing available features
276+
/// * `non_interactive` - If true, returns all required features by default
277+
/// * `include_optional` - If true, allows selection of optional features (interactive mode only)
278+
///
279+
/// # Returns
280+
/// * `Ok(Vec<FeatureInfo>)` - Selected features
281+
/// * `Err(String)` - Error message
282+
pub fn select_features(
283+
metadata: &RequirementsMetadata,
284+
non_interactive: bool,
285+
include_optional: bool,
286+
) -> Result<Vec<FeatureInfo>, String> {
287+
if non_interactive {
288+
// Non-interactive mode: return all required features
289+
println!("Non-interactive mode: selecting all required features by default");
290+
let required = metadata
291+
.required_features()
292+
.into_iter()
293+
.cloned()
294+
.collect();
295+
Ok(required)
296+
} else {
297+
// Interactive mode: let user select features
298+
select_features_interactive(metadata, include_optional)
299+
}
300+
}
301+
302+
/// Interactive feature selection with multi-select dialog
303+
fn select_features_interactive(
304+
metadata: &RequirementsMetadata,
305+
include_optional: bool,
306+
) -> Result<Vec<FeatureInfo>, String> {
307+
let features_to_show: Vec<&FeatureInfo> = if include_optional {
308+
metadata.features.iter().collect()
309+
} else {
310+
metadata.required_features()
311+
};
312+
313+
if features_to_show.is_empty() {
314+
return Err("No features available for selection".to_string());
315+
}
316+
317+
// Create display strings for each feature
318+
let items: Vec<String> = features_to_show
319+
.iter()
320+
.map(|f| {
321+
format!(
322+
"{} - {}",
323+
f.name,
324+
f.description.as_deref().unwrap_or("No description")
325+
)
326+
})
327+
.collect();
328+
329+
// Pre-select all required features
330+
let defaults: Vec<bool> = features_to_show
331+
.iter()
332+
.map(|f| !f.optional)
333+
.collect();
334+
335+
// Show multi-select dialog
336+
let selections = MultiSelect::with_theme(&ColorfulTheme::default())
337+
.with_prompt("Select ESP-IDF features to install (Space to toggle, Enter to confirm)")
338+
.items(&items)
339+
.defaults(&defaults)
340+
.interact()
341+
.map_err(|e| format!("Selection failed: {}", e))?;
342+
343+
if selections.is_empty() {
344+
return Err("No features selected. At least one feature must be selected.".to_string());
345+
}
346+
347+
// Return selected features
348+
let selected_features: Vec<FeatureInfo> = selections
349+
.into_iter()
350+
.map(|idx| features_to_show[idx].clone())
351+
.collect();
352+
353+
Ok(selected_features)
354+
}
355+
356+
/// Select features and return their names only
357+
pub fn select_feature_names(
358+
metadata: &RequirementsMetadata,
359+
non_interactive: bool,
360+
include_optional: bool,
361+
) -> Result<Vec<String>, String> {
362+
let features = select_features(metadata, non_interactive, include_optional)?;
363+
Ok(features.into_iter().map(|f| f.name).collect())
364+
}
365+
366+
/// Select features and return their requirement paths
367+
pub fn select_requirement_paths(
368+
metadata: &RequirementsMetadata,
369+
non_interactive: bool,
370+
include_optional: bool,
371+
) -> Result<Vec<String>, String> {
372+
let features = select_features(metadata, non_interactive, include_optional)?;
373+
Ok(features.into_iter().map(|f| f.requirement_path).collect())
374+
}
375+
376+
/// Advanced selection: filter by specific criteria
377+
pub struct FeatureSelectionOptions {
378+
pub non_interactive: bool,
379+
pub include_optional: bool,
380+
pub show_only_optional: bool,
381+
pub filter_by_name: Option<Vec<String>>,
382+
}
383+
384+
impl Default for FeatureSelectionOptions {
385+
fn default() -> Self {
386+
Self {
387+
non_interactive: false,
388+
include_optional: true,
389+
show_only_optional: false,
390+
filter_by_name: None,
391+
}
392+
}
393+
}
394+
395+
/// Advanced feature selection with filtering options
396+
pub fn select_features_advanced(
397+
metadata: &RequirementsMetadata,
398+
options: FeatureSelectionOptions,
399+
) -> Result<Vec<FeatureInfo>, String> {
400+
// Apply filters
401+
let mut filtered_features: Vec<&FeatureInfo> = metadata.features.iter().collect();
402+
403+
// Filter by optional/required
404+
if options.show_only_optional {
405+
filtered_features.retain(|f| f.optional);
406+
} else if !options.include_optional {
407+
filtered_features.retain(|f| !f.optional);
408+
}
409+
410+
// Filter by name if specified
411+
if let Some(ref names) = options.filter_by_name {
412+
filtered_features.retain(|f| names.contains(&f.name));
413+
}
414+
415+
if filtered_features.is_empty() {
416+
return Err("No features match the specified criteria".to_string());
417+
}
418+
419+
if options.non_interactive {
420+
// Return all filtered features in non-interactive mode
421+
println!(
422+
"Non-interactive mode: selecting {} filtered feature(s)",
423+
filtered_features.len()
424+
);
425+
Ok(filtered_features.into_iter().cloned().collect())
426+
} else {
427+
// Interactive selection from filtered features
428+
let items: Vec<String> = filtered_features
429+
.iter()
430+
.map(|f| {
431+
format!(
432+
"{} {} - {}",
433+
if f.optional { "[ ]" } else { "[*]" },
434+
f.name,
435+
f.description.as_deref().unwrap_or("No description")
436+
)
437+
})
438+
.collect();
439+
440+
let defaults: Vec<bool> = filtered_features.iter().map(|f| !f.optional).collect();
441+
442+
let selections = MultiSelect::with_theme(&ColorfulTheme::default())
443+
.with_prompt("Select ESP-IDF features (Space to toggle, Enter to confirm)")
444+
.items(&items)
445+
.defaults(&defaults)
446+
.interact()
447+
.map_err(|e| format!("Selection failed: {}", e))?;
448+
449+
if selections.is_empty() {
450+
return Err("No features selected".to_string());
451+
}
452+
453+
Ok(selections
454+
.into_iter()
455+
.map(|idx| filtered_features[idx].clone())
456+
.collect())
457+
}
458+
}

src-tauri/src/cli/wizard.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use anyhow::anyhow;
22
use anyhow::Result;
33
use dialoguer::FolderSelect;
4+
use idf_im_lib::idf_features::get_requirements_json_url;
5+
use idf_im_lib::idf_features::RequirementsMetadata;
46
use idf_im_lib::idf_tools::ToolsFile;
57
use idf_im_lib::offline_installer::copy_idf_from_offline_archive;
68
use idf_im_lib::offline_installer::install_prerequisites_offline;
@@ -328,7 +330,7 @@ async fn download_and_extract_tools(
328330
}
329331

330332
pub async fn run_wizzard_run(mut config: Settings) -> Result<(), String> {
331-
debug!(
333+
info!(
332334
"{}",
333335
t!(
334336
"wizard.debug.config_entering",
@@ -413,6 +415,37 @@ pub async fn run_wizzard_run(mut config: Settings) -> Result<(), String> {
413415
config.idf_path = Some(paths.idf_path.clone());
414416
idf_im_lib::add_path_to_path(paths.idf_path.to_str().unwrap());
415417

418+
let req_url = get_requirements_json_url(config.repo_stub.clone().as_deref(), &idf_version.to_string(), config.idf_mirror.clone().as_deref());
419+
420+
let requirements_files = match RequirementsMetadata::from_url(&req_url) {
421+
Ok(files) => files,
422+
Err(err) => {
423+
warn!("{}: {}. {}", t!("wizard.requirements.read_failure"), err, t!("wizard.features.selection_unavailable"));
424+
return Err(err.to_string());
425+
}
426+
};
427+
428+
let features = select_features(
429+
&requirements_files,
430+
config.non_interactive.unwrap_or_default(),
431+
true,
432+
)?;
433+
debug!(
434+
"{}: {}",
435+
t!("wizard.features.selected"),
436+
features
437+
.iter()
438+
.map(|f| f.name.clone())
439+
.collect::<Vec<String>>()
440+
.join(", ")
441+
);
442+
config.idf_features = Some(
443+
features
444+
.iter()
445+
.map(|f| f.name.clone())
446+
.collect::<Vec<String>>(),
447+
);
448+
416449
if !using_existing_idf {
417450
// download idf
418451
let download_config = DownloadConfig {
@@ -442,6 +475,10 @@ pub async fn run_wizzard_run(mut config: Settings) -> Result<(), String> {
442475
}
443476
}
444477
}
478+
// IDF features
479+
480+
481+
445482
// setup tool directories
446483

447484
let tool_download_directory = setup_directory(

src-tauri/src/gui/commands/idf_tools.rs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
use crate::gui::ui::{emit_installation_event, emit_log_message, send_message, send_tools_message, InstallationProgress, InstallationStage, MessageLevel, ProgressBar};
1+
use crate::gui::{app_state::{get_settings_non_blocking, update_settings}, ui::{emit_installation_event, emit_log_message, send_message, send_tools_message, InstallationProgress, InstallationStage, MessageLevel, ProgressBar}};
22
use anyhow::{anyhow, Context, Result};
33

44
use idf_im_lib::{
5-
add_path_to_path,ensure_path,
6-
idf_tools::{self, get_tools_export_paths},
7-
DownloadProgress,
8-
settings::Settings,
5+
add_path_to_path, ensure_path, idf_features::{get_requirements_json_url, FeatureInfo, RequirementsMetadata}, idf_tools::{self, get_tools_export_paths}, settings::Settings, DownloadProgress
96
};
10-
use log::{ error, info};
7+
use log::{ error, info, warn};
118
use std::{
129
path::{Path, PathBuf}, sync::{Arc, Mutex},
1310
};
@@ -419,3 +416,55 @@ pub async fn setup_tools(
419416

420417
Ok(export_paths)
421418
}
419+
420+
#[tauri::command]
421+
pub async fn get_features_list(
422+
app_handle: AppHandle,
423+
) -> Result<Vec<FeatureInfo>, String> {
424+
let settings = get_settings_non_blocking(&app_handle)?;
425+
let first_version = match &settings.idf_versions {
426+
Some(versions) if !versions.is_empty() => versions.first().unwrap(),
427+
_ => {
428+
let msg = t!("wizard.requirements.no_idf_version_specified").to_string();
429+
warn!("{}", msg);
430+
return Err(msg);
431+
}
432+
};
433+
let req_url = get_requirements_json_url(settings.repo_stub.clone().as_deref(), &first_version.to_string(), settings.idf_mirror.clone().as_deref());
434+
println!("Requirements URL: {}", req_url);
435+
let requirements_files = match RequirementsMetadata::from_url_async(&req_url).await {
436+
Ok(files) => files,
437+
Err(err) => {
438+
warn!("{}: {}. {}", t!("wizard.requirements.read_failure"), err, t!("wizard.features.selection_unavailable"));
439+
return Err(err.to_string());
440+
}
441+
};
442+
443+
Ok(requirements_files.features.clone())
444+
}
445+
446+
/// Sets the selected ESP-IDF features
447+
#[tauri::command]
448+
pub fn set_selected_features(
449+
app_handle: AppHandle,
450+
features: Vec<String>,
451+
) -> Result<(), String> {
452+
info!("Setting selected features: {:?}", features);
453+
update_settings(&app_handle, |settings| {
454+
settings.idf_features = Some(features);
455+
})?;
456+
457+
send_message(
458+
&app_handle,
459+
t!("gui.settings.features_updated").to_string(),
460+
"info".to_string(),
461+
);
462+
Ok(())
463+
}
464+
465+
/// Gets the currently selected features from settings
466+
#[tauri::command]
467+
pub fn get_selected_features(app_handle: AppHandle) -> Result<Vec<String>, String> {
468+
let settings = get_settings_non_blocking(&app_handle)?;
469+
Ok(settings.idf_features.clone().unwrap_or_default())
470+
}

src-tauri/src/gui/commands/installation.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -352,15 +352,13 @@ pub async fn install_single_version(
352352
#[cfg(target_os = "windows")]
353353
#[tauri::command]
354354
pub async fn start_installation(app_handle: AppHandle) -> Result<(), String> {
355-
let app_state = app_handle.state::<crate::gui::app_state::AppState>();
356-
357355
// Set installation flag
358356
if let Err(e) = set_installation_status(&app_handle, true) {
359357
return Err(e);
360358
}
361359

362360
// Get the settings and save to a temporary config file
363-
let settings = get_settings_non_blocking(&app_handle)?;
361+
let settings = get_locked_settings(&app_handle)?;
364362
let temp_dir = std::env::temp_dir();
365363
let config_path = temp_dir.join(format!("eim_config_{}.toml", std::process::id()));
366364

0 commit comments

Comments
 (0)