Skip to content

Commit 1813339

Browse files
committed
Added the features selection to the CLI wizard
1 parent 3fc9fac commit 1813339

File tree

7 files changed

+486
-18
lines changed

7 files changed

+486
-18
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
@@ -50,7 +50,7 @@ required-features = ["offline"]
5050
tauri-build = { version = "2.3.1", features = [], optional = true }
5151

5252
[dependencies]
53-
reqwest = "0.12.4"
53+
reqwest = {version = "0.12.4", features = ["blocking"] }
5454
serde = { version = "1.0", features = ["derive"] }
5555
serde_derive = "1.0"
5656
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/prompts.rs

Lines changed: 193 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;
@@ -264,3 +267,192 @@ pub fn save_config_if_desired(config: &Settings) -> Result<(), String> {
264267
}
265268
Ok(())
266269
}
270+
271+
/// Select features from requirements metadata with interactive or non-interactive mode
272+
///
273+
/// # Arguments
274+
/// * `metadata` - The requirements metadata containing available features
275+
/// * `non_interactive` - If true, returns all required features by default
276+
/// * `include_optional` - If true, allows selection of optional features (interactive mode only)
277+
///
278+
/// # Returns
279+
/// * `Ok(Vec<FeatureInfo>)` - Selected features
280+
/// * `Err(String)` - Error message
281+
pub fn select_features(
282+
metadata: &RequirementsMetadata,
283+
non_interactive: bool,
284+
include_optional: bool,
285+
) -> Result<Vec<FeatureInfo>, String> {
286+
if non_interactive {
287+
// Non-interactive mode: return all required features
288+
println!("Non-interactive mode: selecting all required features by default");
289+
let required = metadata
290+
.required_features()
291+
.into_iter()
292+
.cloned()
293+
.collect();
294+
Ok(required)
295+
} else {
296+
// Interactive mode: let user select features
297+
select_features_interactive(metadata, include_optional)
298+
}
299+
}
300+
301+
/// Interactive feature selection with multi-select dialog
302+
fn select_features_interactive(
303+
metadata: &RequirementsMetadata,
304+
include_optional: bool,
305+
) -> Result<Vec<FeatureInfo>, String> {
306+
let features_to_show: Vec<&FeatureInfo> = if include_optional {
307+
metadata.features.iter().collect()
308+
} else {
309+
metadata.required_features()
310+
};
311+
312+
if features_to_show.is_empty() {
313+
return Err("No features available for selection".to_string());
314+
}
315+
316+
// Create display strings for each feature
317+
let items: Vec<String> = features_to_show
318+
.iter()
319+
.map(|f| {
320+
format!(
321+
"{} {} - {}",
322+
if f.optional { "[ ]" } else { "[*]" },
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: 37 additions & 0 deletions
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;
@@ -412,6 +414,37 @@ pub async fn run_wizzard_run(mut config: Settings) -> Result<(), String> {
412414
config.idf_path = Some(paths.idf_path.clone());
413415
idf_im_lib::add_path_to_path(paths.idf_path.to_str().unwrap());
414416

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

446483
let tool_download_directory = setup_directory(

0 commit comments

Comments
 (0)