use std::{ffi::OsString, fmt::Display, path::PathBuf, str::FromStr};
use clap::ValueEnum;
use color_eyre::eyre::{self, Result, bail, eyre};
use inquire::Select;
use log::{info, trace};
use serde::{Deserialize, Serialize};
use crate::workspace::{DevContainer, Workspace};
pub const LAUNCH_DETECT: &str = "detect";
pub const LAUNCH_FORCE_CONTAINER: &str = "force-container";
pub const LAUNCH_FORCE_CLASSIC: &str = "force-classic";
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
ValueEnum,
Serialize,
Deserialize,
)]
pub enum ContainerStrategy {
#[default]
Detect,
ForceContainer,
ForceClassic,
}
impl FromStr for ContainerStrategy {
type Err = eyre::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
LAUNCH_DETECT => Ok(Self::Detect),
LAUNCH_FORCE_CONTAINER => Ok(Self::ForceContainer),
LAUNCH_FORCE_CLASSIC => Ok(Self::ForceClassic),
_ => Err(eyre!("Invalid launch behavior: {}", s)),
}
}
}
impl Display for ContainerStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Detect => f.write_str(LAUNCH_DETECT),
Self::ForceContainer => f.write_str(LAUNCH_FORCE_CONTAINER),
Self::ForceClassic => f.write_str(LAUNCH_FORCE_CLASSIC),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Behavior {
pub strategy: ContainerStrategy,
pub args: Vec<OsString>,
#[serde(default = "default_editor_command")]
pub command: String,
}
fn default_editor_command() -> String {
"code".to_string()
}
fn format_editor_name(command: &str) -> String {
match command.to_lowercase().as_str() {
"code" => "Visual Studio Code".to_string(),
"code-insiders" => "Visual Studio Code Insiders".to_string(),
"cursor" => "Cursor".to_string(),
"codium" => "VSCodium".to_string(),
"positron" => "Positron".to_string(),
_ => format!("'{command}'"),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Setup {
workspace: Workspace,
behavior: Behavior,
dry_run: bool,
}
impl Setup {
pub fn new(workspace: Workspace, behavior: Behavior, dry_run: bool) -> Self {
Self {
workspace,
behavior,
dry_run,
}
}
fn detect(&self, config: Option<PathBuf>) -> Result<Option<DevContainer>> {
let name = self.workspace.name.clone();
if let Some(config) = config {
trace!("Dev container set by path: {config:?}");
Ok(Some(DevContainer::from_config(config.as_path(), &name)?))
} else {
let configs = self.workspace.find_dev_container_configs();
let dev_containers = self.workspace.load_dev_containers(&configs)?;
match configs.len() {
0 => {
trace!("No dev container specified.");
Ok(None)
}
1 => {
trace!("Selected the only existing dev container.");
Ok(dev_containers.into_iter().next())
}
_ => Ok(Some(
Select::new(
"Multiple dev containers found! Please select one:",
dev_containers,
)
.prompt()?,
)),
}
}
}
pub fn launch(self, config: Option<PathBuf>) -> Result<Option<DevContainer>> {
let editor_name = format_editor_name(&self.behavior.command);
match self.behavior.strategy {
ContainerStrategy::Detect => {
let dev_container = self.detect(config)?;
if let Some(ref dev_container) = dev_container {
info!("Opening dev container with {}...", editor_name);
self.workspace.open(
self.behavior.args,
self.dry_run,
dev_container,
&self.behavior.command,
)?;
} else {
info!(
"No dev container found, opening on host system with {}...",
editor_name
);
self.workspace.open_classic(
self.behavior.args,
self.dry_run,
&self.behavior.command,
)?;
}
Ok(dev_container)
}
ContainerStrategy::ForceContainer => {
let dev_container = self.detect(config)?;
if let Some(ref dev_container) = dev_container {
info!("Force opening dev container with {}...", editor_name);
self.workspace.open(
self.behavior.args,
self.dry_run,
dev_container,
&self.behavior.command,
)?;
} else {
bail!(
"No dev container found, but was forced to open it using dev containers."
);
}
Ok(dev_container)
}
ContainerStrategy::ForceClassic => {
info!("Opening without dev containers using {}...", editor_name);
self.workspace.open_classic(
self.behavior.args,
self.dry_run,
&self.behavior.command,
)?;
Ok(None)
}
}
}
}