Skip to main content

spm_swift_package/cli/
cli_controller.rs

1use colored::Colorize;
2use demand::{Confirm, DemandOption, Input, MultiSelect, Select, Spinner, SpinnerStyle};
3
4use crate::core::spm_builder::*;
5use crate::utils::xcode;
6
7/// Controls all CLI interactions and orchestrates the project creation flow
8pub struct Cli;
9
10impl Cli {
11	/// Executes the complete flow: prompts user input, builds the project, and opens it in Xcode
12	pub async fn execute_flow() -> Result<(), String> {
13		let project_name = Self::project_name_input()?;
14		let file_selected = Self::multiselect_files()?;
15		let platform_selected = Self::select_platform()?;
16		let test_framework = Self::select_test_framework()?;
17
18		Self::loading().await?;
19
20		SpmBuilder::create(
21			&project_name,
22			&file_selected,
23			&[platform_selected],
24			test_framework,
25		)?;
26
27		Self::confirm_open_xcode(project_name)?;
28
29		Ok(())
30	}
31
32	// Internal functions
33
34	/// Prompts the user to input the library or module name
35	/// Validates that the name is not empty
36	fn project_name_input() -> Result<String, String> {
37		let validation_empty = |s: &str| {
38			if s.is_empty() {
39				Err("Library name cannot be empty")
40			} else {
41				Ok(())
42			}
43		};
44
45		let input = Input::new("Library name")
46			.placeholder("Enter the library name")
47			.prompt("Library: ")
48			.validation(validation_empty);
49
50		input.run().map_err(|e| {
51			if e.kind() == std::io::ErrorKind::Interrupted {
52				"Operation interrupted by user".to_string()
53			} else {
54				format!("Error getting library name: {}", e)
55			}
56		})
57	}
58
59	/// Creates a generic multiselect component with a prompt, description, and list of options
60	/// Ensures at least one option is selected before continuing
61	fn multiselect_options(
62		prompt: &str,
63		description: &str,
64		options: &[&'static str],
65	) -> Result<Vec<&'static str>, String> {
66		loop {
67			let mut multiselect = MultiSelect::new(prompt)
68				.description(description)
69				.filterable(true);
70
71			for &option in options {
72				multiselect = multiselect.option(DemandOption::new(option));
73			}
74
75			let result = match multiselect.run() {
76				Ok(selection) => selection,
77				Err(e) => {
78					if e.kind() == std::io::ErrorKind::Interrupted {
79						return Err("Operation interrupted by user".to_string());
80					} else {
81						return Err(format!("Error selecting options: {}", e));
82					}
83				}
84			};
85
86			let selected: Vec<&str> = result
87				.iter()
88				.filter(|opt| !opt.is_empty())
89				.copied()
90				.collect();
91
92			if selected.is_empty() {
93				println!(
94					"{}",
95					"You need to choose at least one option to continue".yellow()
96				);
97
98				continue;
99			}
100
101			return Ok(selected);
102		}
103	}
104
105	/// Defines the file options available for selection (Changelog, SPI, README, SwiftLint)
106	fn multiselect_files() -> Result<Vec<&'static str>, String> {
107		Self::multiselect_options(
108			"Add files",
109			"Do you want to add some of these files?",
110			&["Changelog", "Swift Package Index", "Readme", "SwiftLint"],
111		)
112	}
113
114	/// Displays a select input for platform choice
115	/// The result is always a single selected platform
116	fn select_platform() -> Result<&'static str, String> {
117		let mut select = Select::new("Choose platform")
118			.description("Which platform do you want to choose?")
119			.filterable(true);
120
121		for option in ["iOS", "macOS", "tvOS", "watchOS", "visionOS"].iter() {
122			select = select.option(DemandOption::new(*option));
123		}
124
125		select.run().map_err(|e| {
126			if e.kind() == std::io::ErrorKind::Interrupted {
127				"Operation interrupted by user".to_string()
128			} else {
129				format!("Error selecting platform: {}", e)
130			}
131		})
132	}
133
134	/// Displays a select input for test framework choice
135	fn select_test_framework() -> Result<&'static str, String> {
136		let mut select = Select::new("Choose test framework")
137			.description("Which test framework do you want to use?")
138			.filterable(true);
139
140		for option in ["XCTest", "Swift Testing"].iter() {
141			select = select.option(DemandOption::new(*option));
142		}
143
144		select.run().map_err(|e| {
145			if e.kind() == std::io::ErrorKind::Interrupted {
146				"Operation interrupted by user".to_string()
147			} else {
148				format!("Error selecting test framework: {}", e)
149			}
150		})
151	}
152
153	/// Shows a loading spinner while running a simulated build step
154	/// Uses a 5 second delay for effect
155	async fn loading() -> Result<(), String> {
156		Spinner::new("Building the Package...")
157			.style(&SpinnerStyle::line())
158			.run(|_| {
159				std::thread::sleep(std::time::Duration::from_secs(5));
160			})
161			.map_err(|_| "Error running spinner".to_string())
162	}
163
164	/// Asks the user whether to open the generated package in Xcode
165	/// Opens Xcode if confirmed, otherwise returns without opening Xcode
166	fn confirm_open_xcode(project_name: String) -> Result<(), String> {
167		let is_yes = Confirm::new("Do you want to open the package in Xcode?")
168			.affirmative("Yes")
169			.negative("No")
170			.run()
171			.map_err(|e| {
172				if e.kind() == std::io::ErrorKind::Interrupted {
173					"Operation interrupted by user".to_string()
174				} else {
175					"Error running confirm".to_string()
176				}
177			})?;
178
179		if is_yes {
180			xcode::open_xcode(&project_name)?;
181		}
182
183		Ok(())
184	}
185}