use crate::app::Message;
use crate::theme;
use bombadil_core::model::Project;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VenvStatus {
Present,
Absent,
}
pub struct Header {
pub label: String,
pub path: PathBuf,
pub venv_status: VenvStatus,
pub python_version: Option<String>,
pub venv_path: Option<PathBuf>,
}
pub fn header(
project: &Project,
venv_path: Option<PathBuf>,
venv_probe: &dyn Fn(&Project) -> Option<String>,
) -> Header {
let python_version = venv_probe(project);
let venv_status = if python_version.is_some() {
VenvStatus::Present
} else {
VenvStatus::Absent
};
Header {
label: project.label.clone(),
path: project_dir(project),
venv_status,
python_version,
venv_path,
}
}
fn project_dir(project: &Project) -> PathBuf {
project
.pyproject_path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| project.pyproject_path.clone())
}
pub fn view<'a>(header: &Header, index: usize) -> iced::Element<'a, Message> {
let version: iced::Element<'_, Message> = match (header.venv_status, &header.python_version) {
(VenvStatus::Present, Some(version)) => iced::widget::text(version.clone())
.font(theme::FONT_DATA)
.size(theme::DATA)
.into(),
(VenvStatus::Present, None) => iced::widget::text("environment ready")
.size(theme::DATA)
.color(theme::SLATE)
.into(),
(VenvStatus::Absent, _) => iced::widget::text("no environment")
.size(theme::DATA)
.color(theme::SLATE)
.into(),
};
let title_row = iced::widget::row![
iced::widget::text(header.label.clone())
.font(theme::FONT_PROSE_SEMIBOLD)
.size(theme::DISPLAY),
iced::widget::Space::new().width(iced::Length::Fill),
version,
];
let sync = iced::widget::button(iced::widget::text("Sync").size(theme::BODY))
.on_press(Message::SyncRequested(index))
.padding([theme::SPACE_1, theme::SPACE_3])
.style(theme::button_primary);
let terminal = iced::widget::tooltip(
iced::widget::button(
iced::widget::text(">_")
.font(theme::FONT_DATA)
.size(theme::BODY),
)
.on_press(Message::OpenTerminalHereRequested(index))
.padding([theme::SPACE_1, theme::SPACE_2])
.style(theme::button_quiet),
iced::widget::container(
iced::widget::text("Open a terminal here, with the environment activated")
.size(theme::LABEL),
)
.padding(theme::SPACE_1)
.style(theme::panel),
iced::widget::tooltip::Position::Left,
);
let pycharm = iced::widget::tooltip(
iced::widget::button(
iced::widget::text("PC")
.font(theme::FONT_DATA)
.size(theme::BODY),
)
.on_press(Message::OpenPycharmRequested(index))
.padding([theme::SPACE_1, theme::SPACE_2])
.style(theme::button_quiet),
iced::widget::container(
iced::widget::text(
"Open in PyCharm, in this folder, with the environment's variables and PATH",
)
.size(theme::LABEL),
)
.padding(theme::SPACE_1)
.style(theme::panel),
iced::widget::tooltip::Position::Left,
);
let path_row = iced::widget::row![
iced::widget::text(header.path.display().to_string())
.font(theme::FONT_DATA)
.size(theme::DATA)
.color(theme::SLATE),
iced::widget::Space::new().width(iced::Length::Fill),
pycharm,
terminal,
sync,
]
.spacing(theme::SPACE_2)
.align_y(iced::Alignment::Center);
let venv_row: iced::Element<'_, Message> = match &header.venv_path {
Some(path) => theme::labeled_value("environment", path.display().to_string()),
None => iced::widget::text("no environment configured")
.size(theme::DATA)
.color(theme::SLATE)
.into(),
};
iced::widget::column![title_row, path_row, venv_row]
.spacing(theme::SPACE_2)
.into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_header_carries_the_active_environments_path() {
let path = PathBuf::from("/home/me/envs/api-312");
let header = header(&project("api"), Some(path.clone()), &|_| {
Some("3.12.1".into())
});
assert_eq!(header.venv_path, Some(path));
assert_ne!(
header.venv_path.as_ref(),
Some(&header.path),
"the environment path is not the project path -- showing one twice \
would say nothing"
);
}
#[test]
fn a_project_with_no_environments_carries_no_path() {
let header = header(&project("api"), None, &|_| None);
assert_eq!(header.venv_path, None);
}
#[test]
fn a_project_with_a_venv_shows_its_python_version() {
let header = header(&project("api"), None, &|_| Some("3.12.1".into()));
assert_eq!(header.python_version.as_deref(), Some("3.12.1"));
assert_eq!(header.venv_status, VenvStatus::Present);
}
#[test]
fn a_project_without_a_venv_reports_no_environment() {
let header = header(&project("api"), None, &|_| None);
assert_eq!(header.python_version, None);
assert_eq!(header.venv_status, VenvStatus::Absent);
}
#[test]
fn the_displayed_path_is_the_project_directory_not_the_manifest_file() {
let header = header(&project("api"), None, &|_| None);
assert_eq!(header.path, PathBuf::from("/p/api"));
assert_ne!(header.path, PathBuf::from("/p/api/pyproject.toml"));
}
fn project(label: &str) -> bombadil_core::model::Project {
bombadil_core::model::Project {
id: uuid::Uuid::new_v4(),
label: label.into(),
pyproject_path: std::path::PathBuf::from(format!("/p/{label}/pyproject.toml")),
environments: vec![bombadil_core::model::Environment {
location: bombadil_core::model::VenvLocation::Alongside,
python: bombadil_core::model::PythonPin::Unpinned,
}],
active: bombadil_core::model::VenvLocation::Alongside,
..bombadil_core::model::Project::default()
}
}
}