Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ required-features = ["offline"]
tauri-build = { version = "2.3.1", features = [], optional = true }

[dependencies]
reqwest = "0.12.4"
reqwest = {version = "0.12.4", features = ["blocking"] }
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/locales/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ wizard.idf.user_cancelled:
wizard.idf.cloning:
en: Cloning ESP-IDF
cn: 正在克隆 ESP-IDF
wizard.idf.submodule_finish:
en: "IDF submodule correctly downloaded to:"
cn: "IDF 子模块已正确下载到:"
wizard.idf_version.selected:
en: "Selected IDF version: %{version}"
cn: "已选择 IDF 版本: %{version}"
Expand Down
8 changes: 4 additions & 4 deletions src-tauri/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ pub async fn run_cli(cli: Cli) -> anyhow::Result<()> {
install_args.config.clone(),
install_args.clone().into_iter(),
);
info!("Returned settings: {:?}", settings);
match settings {
Ok(mut settings) => {
info!("Settings before adjustments: {:?}", settings);
if install_args.install_all_prerequisites.is_none() { // if cli argument is not set
settings.install_all_prerequisites = Some(true); // The non-interactive install will always install all prerequisites
}
if install_args.idf_versions.is_none() { // if no version is specified, use the latest stable (skipping pre-releases)
settings.idf_versions = Some(vec![idf_im_lib::idf_versions::get_idf_names(false).await.first().unwrap().to_string()]);
}
info!("Settings after adjustments: {:?}", settings);
let time = std::time::SystemTime::now();
if !do_not_track {
track_cli_event("CLI installation started", Some(json!({
Expand All @@ -168,7 +168,7 @@ pub async fn run_cli(cli: Cli) -> anyhow::Result<()> {
let result = wizard::run_wizzard_run(settings).await;
match result {
Ok(r) => {
info!("{}", t!("install.wizard_result"));
info!("{}", t!("install.wizard_result", r = "Ok".to_string()));
info!("{}", t!("install.success"));
info!("{}", t!("install.ready"));
if !do_not_track {
Expand Down
193 changes: 192 additions & 1 deletion src-tauri/src/cli/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use std::path::PathBuf;
use crate::cli::helpers::{
first_defaulted_multiselect, generic_confirm, generic_input, generic_select, run_with_spinner,
};
use idf_im_lib::settings::Settings;
use dialoguer::theme::ColorfulTheme;
use dialoguer::MultiSelect;
use idf_im_lib::idf_features::FeatureInfo;
use idf_im_lib::{idf_features::RequirementsMetadata, settings::Settings};
use idf_im_lib::system_dependencies;
use log::{debug, info};
use rust_i18n::t;
Expand Down Expand Up @@ -265,3 +268,191 @@ pub fn save_config_if_desired(config: &Settings) -> Result<(), String> {
}
Ok(())
}

/// Select features from requirements metadata with interactive or non-interactive mode
///
/// # Arguments
/// * `metadata` - The requirements metadata containing available features
/// * `non_interactive` - If true, returns all required features by default
/// * `include_optional` - If true, allows selection of optional features (interactive mode only)
///
/// # Returns
/// * `Ok(Vec<FeatureInfo>)` - Selected features
/// * `Err(String)` - Error message
pub fn select_features(
metadata: &RequirementsMetadata,
non_interactive: bool,
include_optional: bool,
) -> Result<Vec<FeatureInfo>, String> {
if non_interactive {
// Non-interactive mode: return all required features
println!("Non-interactive mode: selecting all required features by default");
let required = metadata
.required_features()
.into_iter()
.cloned()
.collect();
Ok(required)
} else {
// Interactive mode: let user select features
select_features_interactive(metadata, include_optional)
}
}

/// Interactive feature selection with multi-select dialog
fn select_features_interactive(
metadata: &RequirementsMetadata,
include_optional: bool,
) -> Result<Vec<FeatureInfo>, String> {
let features_to_show: Vec<&FeatureInfo> = if include_optional {
metadata.features.iter().collect()
} else {
metadata.required_features()
};

if features_to_show.is_empty() {
return Err("No features available for selection".to_string());
}

// Create display strings for each feature
let items: Vec<String> = features_to_show
.iter()
.map(|f| {
format!(
"{} - {}",
f.name,
f.description.as_deref().unwrap_or("No description")
)
})
.collect();

// Pre-select all required features
let defaults: Vec<bool> = features_to_show
.iter()
.map(|f| !f.optional)
.collect();

// Show multi-select dialog
let selections = MultiSelect::with_theme(&ColorfulTheme::default())
.with_prompt("Select ESP-IDF features to install (Space to toggle, Enter to confirm)")
.items(&items)
.defaults(&defaults)
.interact()
.map_err(|e| format!("Selection failed: {}", e))?;

if selections.is_empty() {
return Err("No features selected. At least one feature must be selected.".to_string());
}

// Return selected features
let selected_features: Vec<FeatureInfo> = selections
.into_iter()
.map(|idx| features_to_show[idx].clone())
.collect();

Ok(selected_features)
}

/// Select features and return their names only
pub fn select_feature_names(
metadata: &RequirementsMetadata,
non_interactive: bool,
include_optional: bool,
) -> Result<Vec<String>, String> {
let features = select_features(metadata, non_interactive, include_optional)?;
Ok(features.into_iter().map(|f| f.name).collect())
}

/// Select features and return their requirement paths
pub fn select_requirement_paths(
metadata: &RequirementsMetadata,
non_interactive: bool,
include_optional: bool,
) -> Result<Vec<String>, String> {
let features = select_features(metadata, non_interactive, include_optional)?;
Ok(features.into_iter().map(|f| f.requirement_path).collect())
}

/// Advanced selection: filter by specific criteria
pub struct FeatureSelectionOptions {
pub non_interactive: bool,
pub include_optional: bool,
pub show_only_optional: bool,
pub filter_by_name: Option<Vec<String>>,
}

impl Default for FeatureSelectionOptions {
fn default() -> Self {
Self {
non_interactive: false,
include_optional: true,
show_only_optional: false,
filter_by_name: None,
}
}
}

/// Advanced feature selection with filtering options
pub fn select_features_advanced(
metadata: &RequirementsMetadata,
options: FeatureSelectionOptions,
) -> Result<Vec<FeatureInfo>, String> {
// Apply filters
let mut filtered_features: Vec<&FeatureInfo> = metadata.features.iter().collect();

// Filter by optional/required
if options.show_only_optional {
filtered_features.retain(|f| f.optional);
} else if !options.include_optional {
filtered_features.retain(|f| !f.optional);
}

// Filter by name if specified
if let Some(ref names) = options.filter_by_name {
filtered_features.retain(|f| names.contains(&f.name));
}

if filtered_features.is_empty() {
return Err("No features match the specified criteria".to_string());
}

if options.non_interactive {
// Return all filtered features in non-interactive mode
println!(
"Non-interactive mode: selecting {} filtered feature(s)",
filtered_features.len()
);
Ok(filtered_features.into_iter().cloned().collect())
} else {
// Interactive selection from filtered features
let items: Vec<String> = filtered_features
.iter()
.map(|f| {
format!(
"{} {} - {}",
if f.optional { "[ ]" } else { "[*]" },
f.name,
f.description.as_deref().unwrap_or("No description")
)
})
.collect();

let defaults: Vec<bool> = filtered_features.iter().map(|f| !f.optional).collect();

let selections = MultiSelect::with_theme(&ColorfulTheme::default())
.with_prompt("Select ESP-IDF features (Space to toggle, Enter to confirm)")
.items(&items)
.defaults(&defaults)
.interact()
.map_err(|e| format!("Selection failed: {}", e))?;

if selections.is_empty() {
return Err("No features selected".to_string());
}

Ok(selections
.into_iter()
.map(|idx| filtered_features[idx].clone())
.collect())
}
Comment on lines +427 to +457

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The interactive selection logic in this function is very similar to the logic in select_features_interactive. To avoid code duplication and improve maintainability, consider extracting the common parts into a private helper function. This helper could handle building the MultiSelect prompt, interacting with the user, and returning the selected features.

For example, you could create a function like this:

fn prompt_user_for_features(
    features: &[&FeatureInfo],
    prompt_message: &str,
) -> Result<Vec<FeatureInfo>, String> {
    // ... implementation of the MultiSelect dialog ...
}

Both select_features_interactive and select_features_advanced could then call this helper with the appropriate list of features and prompt message.

}
39 changes: 38 additions & 1 deletion src-tauri/src/cli/wizard.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anyhow::anyhow;
use anyhow::Result;
use dialoguer::FolderSelect;
use idf_im_lib::idf_features::get_requirements_json_url;
use idf_im_lib::idf_features::RequirementsMetadata;
use idf_im_lib::idf_tools::ToolsFile;
use idf_im_lib::offline_installer::copy_idf_from_offline_archive;
use idf_im_lib::offline_installer::install_prerequisites_offline;
Expand Down Expand Up @@ -328,7 +330,7 @@ async fn download_and_extract_tools(
}

pub async fn run_wizzard_run(mut config: Settings) -> Result<(), String> {
debug!(
info!(
"{}",
t!(
"wizard.debug.config_entering",
Expand Down Expand Up @@ -413,6 +415,37 @@ pub async fn run_wizzard_run(mut config: Settings) -> Result<(), String> {
config.idf_path = Some(paths.idf_path.clone());
idf_im_lib::add_path_to_path(paths.idf_path.to_str().unwrap());

let req_url = get_requirements_json_url(config.repo_stub.clone().as_deref(), &idf_version.to_string(), config.idf_mirror.clone().as_deref());

let requirements_files = match RequirementsMetadata::from_url(&req_url) {
Ok(files) => files,
Err(err) => {
warn!("{}: {}. {}", t!("wizard.requirements.read_failure"), err, t!("wizard.features.selection_unavailable"));
return Err(err.to_string());
}
};
Comment on lines +420 to +426

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The run_wizzard_run function is async, but it's using the blocking RequirementsMetadata::from_url to fetch the requirements file. This will block the async runtime. The idf_im_lib::idf_features module provides an async version, from_url_async, which should be used here with .await.

Suggested change
let requirements_files = match RequirementsMetadata::from_url(&req_url) {
Ok(files) => files,
Err(err) => {
warn!("{}: {}. {}", t!("wizard.requirements.read_failure"), err, t!("wizard.features.selection_unavailable"));
return Err(err.to_string());
}
};
let requirements_files = match RequirementsMetadata::from_url_async(&req_url).await {
Ok(files) => files,
Err(err) => {
warn!("{}: {}. {}", t!("wizard.requirements.read_failure"), err, t!("wizard.features.selection_unavailable"));
return Err(err.to_string());
}
};


let features = select_features(
&requirements_files,
config.non_interactive.unwrap_or_default(),
true,
)?;
debug!(
"{}: {}",
t!("wizard.features.selected"),
features
.iter()
.map(|f| f.name.clone())
.collect::<Vec<String>>()
.join(", ")
);
config.idf_features = Some(
features
.iter()
.map(|f| f.name.clone())
.collect::<Vec<String>>(),
);

if !using_existing_idf {
// download idf
let download_config = DownloadConfig {
Expand Down Expand Up @@ -442,6 +475,10 @@ pub async fn run_wizzard_run(mut config: Settings) -> Result<(), String> {
}
}
}
// IDF features



// setup tool directories

let tool_download_directory = setup_directory(
Expand Down
61 changes: 55 additions & 6 deletions src-tauri/src/gui/commands/idf_tools.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use crate::gui::ui::{emit_installation_event, emit_log_message, send_message, send_tools_message, InstallationProgress, InstallationStage, MessageLevel, ProgressBar};
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}};
use anyhow::{anyhow, Context, Result};

use idf_im_lib::{
add_path_to_path,ensure_path,
idf_tools::{self, get_tools_export_paths},
DownloadProgress,
settings::Settings,
add_path_to_path, ensure_path, idf_features::{get_requirements_json_url, FeatureInfo, RequirementsMetadata}, idf_tools::{self, get_tools_export_paths}, settings::Settings, DownloadProgress
};
use log::{ error, info};
use log::{ error, info, warn};
use std::{
path::{Path, PathBuf}, sync::{Arc, Mutex},
};
Expand Down Expand Up @@ -419,3 +416,55 @@ pub async fn setup_tools(

Ok(export_paths)
}

#[tauri::command]
pub async fn get_features_list(
app_handle: AppHandle,
) -> Result<Vec<FeatureInfo>, String> {
let settings = get_settings_non_blocking(&app_handle)?;
let first_version = match &settings.idf_versions {
Some(versions) if !versions.is_empty() => versions.first().unwrap(),
_ => {
let msg = t!("wizard.requirements.no_idf_version_specified").to_string();
warn!("{}", msg);
return Err(msg);
}
};
let req_url = get_requirements_json_url(settings.repo_stub.clone().as_deref(), &first_version.to_string(), settings.idf_mirror.clone().as_deref());
println!("Requirements URL: {}", req_url);
let requirements_files = match RequirementsMetadata::from_url_async(&req_url).await {
Ok(files) => files,
Err(err) => {
warn!("{}: {}. {}", t!("wizard.requirements.read_failure"), err, t!("wizard.features.selection_unavailable"));
return Err(err.to_string());
}
};

Ok(requirements_files.features.clone())
}

/// Sets the selected ESP-IDF features
#[tauri::command]
pub fn set_selected_features(
app_handle: AppHandle,
features: Vec<String>,
) -> Result<(), String> {
info!("Setting selected features: {:?}", features);
update_settings(&app_handle, |settings| {
settings.idf_features = Some(features);
})?;

send_message(
&app_handle,
t!("gui.settings.features_updated").to_string(),
"info".to_string(),
);
Ok(())
}

/// Gets the currently selected features from settings
#[tauri::command]
pub fn get_selected_features(app_handle: AppHandle) -> Result<Vec<String>, String> {
let settings = get_settings_non_blocking(&app_handle)?;
Ok(settings.idf_features.clone().unwrap_or_default())
}
Loading
Loading