spm_swift_package/presentation/
cli_controller.rs

1use colored::Colorize;
2use demand::{DemandOption, Input, MultiSelect, Select, Spinner, SpinnerStyle};
3use std::process::Command;
4
5use crate::domain::builder::spm_builder::*;
6
7/// Controls all CLI interactions and orchestrates the project creation flow
8pub struct CliController;
9
10impl CliController {
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
17		Self::loading().await?;
18		SpmBuilder::builder(&project_name, file_selected, vec![platform_selected]).await?;
19
20		Self::command_open_xcode(&project_name)?;
21		Ok(())
22	}
23
24	// Internal functions
25
26	/// Prompts the user to input the library or module name
27	/// Validates that the name is not empty
28	fn project_name_input() -> Result<String, String> {
29		let validation_empty = |s: &str| {
30			if s.is_empty() {
31				Err("Library name cannot be empty")
32			} else {
33				Ok(())
34			}
35		};
36
37		let input = Input::new("Library name")
38			.placeholder("Enter the library name")
39			.prompt("Library: ")
40			.validation(validation_empty);
41
42		input.run().map_err(|e| {
43			if e.kind() == std::io::ErrorKind::Interrupted {
44				"Operation interrupted by user".to_string()
45			} else {
46				format!("Error getting library name: {}", e)
47			}
48		})
49	}
50
51	/// Creates a generic multiselect component with a prompt, description, and list of options
52	/// Ensures at least one option is selected before continuing
53	fn multiselect_options(
54		prompt: &str,
55		description: &str,
56		options: &[&'static str],
57	) -> Result<Vec<&'static str>, String> {
58		loop {
59			let mut multiselect = MultiSelect::new(prompt)
60				.description(description)
61				.filterable(true);
62
63			for &option in options {
64				multiselect = multiselect.option(DemandOption::new(option));
65			}
66
67			let result = match multiselect.run() {
68				Ok(selection) => selection,
69				Err(e) => {
70					if e.kind() == std::io::ErrorKind::Interrupted {
71						return Err("Operation interrupted by user".to_string());
72					} else {
73						return Err(format!("Error selecting options: {}", e));
74					}
75				}
76			};
77
78			let selected: Vec<&str> = result
79				.iter()
80				.filter(|opt| !opt.is_empty())
81				.copied()
82				.collect();
83
84			if selected.is_empty() {
85				println!(
86					"{}",
87					"You need to choose at least one option to continue".yellow()
88				);
89				continue;
90			}
91
92			return Ok(selected);
93		}
94	}
95
96	/// Defines the file options available for selection (Changelog, SPI, README, SwiftLint)
97	fn multiselect_files() -> Result<Vec<&'static str>, String> {
98		Self::multiselect_options(
99			"Add files",
100			"Do you want to add some of these files?",
101			&["Changelog", "Swift Package Index", "Readme", "SwiftLint"],
102		)
103	}
104
105	/// Displays a select input for platform choice
106	/// The result is always a single selected platform
107	fn select_platform() -> Result<&'static str, String> {
108		let mut select = Select::new("Choose platform")
109			.description("Which platform do you want to choose?")
110			.filterable(true);
111
112		for option in ["iOS", "macOS", "tvOS", "watchOS", "visionOS"].iter() {
113			select = select.option(DemandOption::new(*option));
114		}
115
116		select.run().map_err(|e| {
117			if e.kind() == std::io::ErrorKind::Interrupted {
118				"Operation interrupted by user".to_string()
119			} else {
120				format!("Error selecting platform: {}", e)
121			}
122		})
123	}
124
125	/// Shows a loading spinner while running a simulated build step
126	/// Uses a 5 second delay for effect
127	async fn loading() -> Result<(), String> {
128		Spinner::new("Building the Package...")
129			.style(&SpinnerStyle::line())
130			.run(|_| {
131				std::thread::sleep(std::time::Duration::from_secs(5));
132			})
133			.map_err(|_| "Error running spinner".to_string())
134	}
135
136	/// Opens the generated Package.swift in Xcode using a shell command
137	/// Changes directory into the created project before launching Xcode
138	fn command_open_xcode(project_name: &str) -> Result<(), String> {
139		let command = format!("cd {} && open Package.swift", project_name);
140		let mut child = Command::new("sh")
141			.arg("-c")
142			.arg(&command)
143			.spawn()
144			.map_err(|e| format!("Failed to open Xcode: {}", e))?;
145
146		child
147			.wait()
148			.map_err(|e| format!("Failed to wait for Xcode: {}", e))?;
149		Ok(())
150	}
151}