use crate::app::Message;
use crate::theme;
use bombadil_core::model::{DefaultVenvLocation, Project, Settings, UvSource, VenvLocation};
use bombadil_core::venv_path;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UvSourceChoice {
Auto,
Bundled,
FromPath,
}
pub fn apply_choice(settings: &mut Settings, choice: UvSourceChoice) {
settings.uv_source = match choice {
UvSourceChoice::Auto => UvSource::Auto,
UvSourceChoice::Bundled => UvSource::Bundled,
UvSourceChoice::FromPath => UvSource::FromPath,
};
}
pub fn current_choice(settings: &Settings) -> Option<UvSourceChoice> {
match settings.uv_source {
UvSource::Auto => Some(UvSourceChoice::Auto),
UvSource::Bundled => Some(UvSourceChoice::Bundled),
UvSource::FromPath => Some(UvSourceChoice::FromPath),
UvSource::Custom { .. } => None,
}
}
pub fn apply_default_venv_location(settings: &mut Settings, location: DefaultVenvLocation) {
settings.default_venv_location = location;
}
pub fn resolved_default_venv_paths(
projects: &[Project],
settings: &Settings,
) -> Vec<(String, Result<PathBuf, String>)> {
projects
.iter()
.flat_map(|project| {
project
.environments
.iter()
.filter(|environment| matches!(environment.location, VenvLocation::Default))
.map(move |environment| {
(
project.label.clone(),
venv_path::resolve(project, environment, settings)
.map_err(|e| e.to_string()),
)
})
})
.collect()
}
fn uv_source_button<'a>(
label: &'static str,
choice: UvSourceChoice,
current: Option<UvSourceChoice>,
) -> iced::Element<'a, Message> {
iced::widget::button(iced::widget::text(label))
.on_press_maybe(
(current != Some(choice)).then_some(Message::SettingsUvSourceChoiceChanged(choice)),
)
.into()
}
pub fn global_view<'a>(
settings: &Settings,
projects: &[Project],
uv_custom_path_draft: &str,
uv_custom_validation: Option<&Result<String, String>>,
) -> iced::Element<'a, Message> {
let current = current_choice(settings);
let uv_source_buttons = iced::widget::row![
uv_source_button("auto", UvSourceChoice::Auto, current),
uv_source_button("bundled", UvSourceChoice::Bundled, current),
uv_source_button("from PATH", UvSourceChoice::FromPath, current),
]
.spacing(theme::SPACE_1);
let current_source: iced::Element<'a, Message> = match &settings.uv_source {
UvSource::Auto => theme::labeled_prose("current uv source:", "auto"),
UvSource::Bundled => theme::labeled_prose("current uv source:", "bundled"),
UvSource::FromPath => theme::labeled_prose("current uv source:", "from PATH"),
UvSource::Custom { path } => {
theme::labeled_value("current uv source:", path.display().to_string())
}
};
let custom_path_input = iced::widget::text_input("path to a uv binary", uv_custom_path_draft)
.font(theme::FONT_DATA)
.size(theme::DATA)
.on_input(Message::SettingsUvCustomPathChanged);
let validate_button = iced::widget::button(iced::widget::text("validate and use"))
.on_press_maybe(
(!uv_custom_path_draft.trim().is_empty())
.then_some(Message::SettingsUvCustomValidateRequested),
);
let mut column = iced::widget::column![
iced::widget::text("Settings")
.font(theme::FONT_PROSE_SEMIBOLD)
.size(theme::TITLE),
iced::widget::text("uv binary")
.font(theme::FONT_PROSE)
.size(theme::LABEL)
.color(theme::SLATE),
current_source,
uv_source_buttons,
iced::widget::row![custom_path_input, validate_button].spacing(theme::SPACE_2),
]
.spacing(theme::SPACE_3);
if let Some(validation) = uv_custom_validation {
let row: iced::Element<'a, Message> = match validation {
Ok(version) => theme::labeled_value("validated: uv", version.clone()),
Err(message) => iced::widget::text(format!("could not use this uv: {message}"))
.size(theme::BODY)
.into(),
};
column = column.push(row);
}
let default_venv: iced::Element<'a, Message> = match &settings.default_venv_location {
DefaultVenvLocation::Alongside => theme::labeled_prose(
"current default:",
"alongside each project (<project>/.venv)",
),
DefaultVenvLocation::Central { path } => {
theme::labeled_value("current default:", path.display().to_string())
}
};
let alongside_button = iced::widget::button(iced::widget::text("alongside each project"))
.on_press_maybe(
(!matches!(
settings.default_venv_location,
DefaultVenvLocation::Alongside
))
.then_some(Message::SettingsDefaultVenvLocationAlongsideSelected),
);
let choose_folder_button =
iced::widget::button(iced::widget::text("choose a central folder..."))
.on_press(Message::SettingsDefaultVenvLocationPickFolderRequested);
column = column
.push(
iced::widget::text("default venv location")
.font(theme::FONT_PROSE)
.size(theme::LABEL)
.color(theme::SLATE),
)
.push(default_venv)
.push(iced::widget::row![alongside_button, choose_folder_button].spacing(theme::SPACE_2));
for (label, resolved) in resolved_default_venv_paths(projects, settings) {
let value: iced::Element<'a, Message> = match resolved {
Ok(path) => iced::widget::text(path.display().to_string())
.font(theme::FONT_DATA)
.size(theme::DATA)
.color(theme::SLATE)
.into(),
Err(message) => iced::widget::text(message)
.size(theme::DATA)
.color(theme::SLATE)
.into(),
};
column = column.push(
iced::widget::row![
iced::widget::text(format!("{label}:"))
.size(theme::BODY)
.color(theme::SLATE),
value,
]
.spacing(theme::SPACE_1),
);
}
column.into()
}
#[cfg(test)]
mod tests {
use super::*;
use bombadil_core::model::{Environment, PythonPin};
use std::path::PathBuf;
use uuid::Uuid;
fn project(label: &str, venv: VenvLocation, dir: &str) -> Project {
Project {
id: Uuid::from_u128(1),
label: label.into(),
pyproject_path: PathBuf::from(dir).join("pyproject.toml"),
environments: vec![Environment {
location: venv.clone(),
python: PythonPin::Unpinned,
}],
active: venv,
..Project::default()
}
}
#[test]
fn apply_choice_bundled_sets_the_bundled_source() {
let mut settings = Settings {
uv_source: UvSource::Auto,
..Settings::default()
};
apply_choice(&mut settings, UvSourceChoice::Bundled);
assert_eq!(settings.uv_source, UvSource::Bundled);
}
#[test]
fn apply_choice_from_a_previous_custom_source_replaces_it() {
let mut settings = Settings {
uv_source: UvSource::Custom {
path: PathBuf::from("/opt/uv"),
},
..Settings::default()
};
apply_choice(&mut settings, UvSourceChoice::Auto);
assert_eq!(settings.uv_source, UvSource::Auto);
}
#[test]
fn current_choice_is_none_for_a_custom_source() {
let settings = Settings {
uv_source: UvSource::Custom {
path: PathBuf::from("/opt/uv"),
},
..Settings::default()
};
assert_eq!(current_choice(&settings), None);
}
#[test]
fn current_choice_names_each_of_the_three_plain_variants() {
for (source, want) in [
(UvSource::Auto, UvSourceChoice::Auto),
(UvSource::Bundled, UvSourceChoice::Bundled),
(UvSource::FromPath, UvSourceChoice::FromPath),
] {
let settings = Settings {
uv_source: source,
..Settings::default()
};
assert_eq!(current_choice(&settings), Some(want));
}
}
#[test]
fn resolved_default_venv_paths_follows_the_alongside_setting() {
let settings = Settings {
default_venv_location: DefaultVenvLocation::Alongside,
..Settings::default()
};
let projects = vec![project("api", VenvLocation::Default, "/work/api")];
let got = resolved_default_venv_paths(&projects, &settings);
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, "api");
assert_eq!(
got[0].1.as_ref().unwrap(),
&PathBuf::from("/work/api/.venv")
);
}
#[test]
fn resolved_default_venv_paths_follows_the_central_setting() {
let projects = vec![project("api", VenvLocation::Default, "/work/api")];
let alongside = resolved_default_venv_paths(
&projects,
&Settings {
default_venv_location: DefaultVenvLocation::Alongside,
..Settings::default()
},
);
let central = resolved_default_venv_paths(
&projects,
&Settings {
default_venv_location: DefaultVenvLocation::Central {
path: PathBuf::from("/home/t/.venvs"),
},
..Settings::default()
},
);
assert_ne!(
alongside[0].1.as_ref().unwrap(),
central[0].1.as_ref().unwrap(),
"changing the setting must change the resolved path"
);
assert!(
central[0].1.as_ref().unwrap().starts_with("/home/t/.venvs"),
"got {:?}",
central[0].1
);
}
#[test]
fn resolved_default_venv_paths_ignores_a_project_with_its_own_location() {
let projects = vec![
project("api", VenvLocation::Default, "/work/api"),
project("pinned", VenvLocation::Alongside, "/work/pinned"),
project(
"custom",
VenvLocation::Custom {
path: PathBuf::from("/mnt/fast/custom"),
},
"/work/custom",
),
];
let settings = Settings {
default_venv_location: DefaultVenvLocation::Central {
path: PathBuf::from("/home/t/.venvs"),
},
..Settings::default()
};
let got = resolved_default_venv_paths(&projects, &settings);
assert_eq!(got.len(), 1, "got {got:?}");
assert_eq!(got[0].0, "api");
}
#[test]
fn apply_default_venv_location_writes_the_field_resolve_reads() {
let mut settings = Settings::default();
apply_default_venv_location(
&mut settings,
DefaultVenvLocation::Central {
path: PathBuf::from("/home/t/.venvs"),
},
);
assert_eq!(
settings.default_venv_location,
DefaultVenvLocation::Central {
path: PathBuf::from("/home/t/.venvs")
}
);
}
}