use core::fmt;
use std::path::Path;
use shep_core::paths::ShepPaths;
use super::*;
use crate::commands::dogs::installed;
use crate::commands::shep_toml::ShepToml;
use crate::dog_index::{
self, AvailableDog, CATEGORIES, DogSourceKind, Index, MAX_AGE, cached_with_age,
};
use crate::dog_origin::{DogOrigin, Origins};
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DogState {
NotInstalled,
Adopted,
Installed,
Enabled,
}
impl DogState {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::NotInstalled => "not installed",
Self::Adopted => "adopted",
Self::Installed => "installed",
Self::Enabled => "enabled",
}
}
}
impl fmt::Display for DogState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CatalogueRow {
pub(crate) dog: AvailableDog,
pub(crate) version: String,
pub(crate) state: DogState,
}
pub(crate) fn build_rows(index: &Index, paths: &ShepPaths, config: &ShepToml) -> Vec<CatalogueRow> {
let origins = Origins::over(paths, Some(index.clone()));
let adopted = config.adopted_dog_names();
let enabled = config.enabled_dog_names();
let mut rows: Vec<CatalogueRow> = index
.dogs
.iter()
.map(|dog| {
let name = dog.adopt_as.as_str();
let has_receipt = installed(paths, name).is_some();
let is_installed = adopted.iter().any(|adopted| adopted == name) || has_receipt;
let (state, version) = if is_installed {
let path = config.adopted_dog_path(name).unwrap_or_default();
let origin = origins.of(name, Some(Path::new(&path)));
let state = if !has_receipt {
DogState::Adopted
} else if enabled.iter().any(|enabled| enabled == name) {
DogState::Enabled
} else {
DogState::Installed
};
(state, origin.version_cell())
} else {
(DogState::NotInstalled, listed_version(dog))
};
CatalogueRow {
dog: dog.clone(),
version,
state,
}
})
.collect();
rows.sort_by_key(|row| (category_rank(&row.dog.category), row.dog.name.clone()));
rows
}
fn listed_version(dog: &AvailableDog) -> String {
let version = match &dog.source {
DogSourceKind::Cargo { version, .. } => version.clone(),
DogSourceKind::CargoGit { .. }
| DogSourceKind::GoInstall { .. }
| DogSourceKind::Manual { .. } => None,
};
DogOrigin::adopted(version).version_cell()
}
fn category_rank(category: &str) -> usize {
CATEGORIES
.iter()
.position(|known| *known == category)
.unwrap_or(CATEGORIES.len())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CatalogueRead {
pub(crate) rows: Vec<CatalogueRow>,
pub(crate) refreshing: bool,
}
fn rows_for(paths: &ShepPaths, index: &Index) -> Result<Vec<CatalogueRow>, String> {
let config = ShepToml::read_only(&paths.daemon_config).map_err(|err| err.to_string())?;
Ok(build_rows(index, paths, &config))
}
pub(crate) fn read_cache(
paths: &ShepPaths,
url: &str,
fetch: bool,
) -> Result<Box<CatalogueRead>, String> {
let Some((index, age)) = cached_with_age(paths, url) else {
return Ok(Box::new(CatalogueRead {
rows: Vec::new(),
refreshing: fetch,
}));
};
Ok(Box::new(CatalogueRead {
rows: rows_for(paths, &index)?,
refreshing: fetch && age >= MAX_AGE,
}))
}
pub(crate) async fn fetch(paths: ShepPaths, url: String) -> Result<Box<CatalogueRead>, String> {
let loaded = dog_index::cached_index(&paths, &url, false)
.await
.map_err(|err| err.to_string())?;
let rows = tokio::task::spawn_blocking(move || rows_for(&paths, &loaded.index))
.await
.map_err(|err| err.to_string())??;
Ok(Box::new(CatalogueRead {
rows,
refreshing: false,
}))
}
#[derive(Debug)]
pub(crate) struct CataloguePane {
pub(crate) selected: usize,
pub(crate) entry: Option<String>,
pub(crate) refreshing: bool,
pub(crate) direct: bool,
}
impl CatalogueRow {
pub(crate) fn is_behind(&self) -> bool {
self.version.contains('\u{2192}')
}
}
impl CataloguePane {
pub(crate) fn open_entry(&mut self, name: &str, rows: &[CatalogueRow]) {
let Some(at) = rows.iter().position(|row| row.dog.adopt_as == name) else {
return;
};
self.selected = at;
self.entry = Some(name.to_owned());
self.direct = true;
}
}
impl App {
pub(crate) fn open_catalogue(&mut self) -> Effect {
if self.refuse_over_host() {
return Effect::None;
}
self.body = Body::Catalogue(CataloguePane {
selected: 0,
entry: None,
refreshing: false,
direct: false,
});
Effect::LoadCatalogue { fetch: true }
}
pub(crate) fn open_catalogue_entry(&mut self, name: &str) -> Effect {
let effect = self.open_catalogue();
if let Body::Catalogue(pane) = &mut self.body {
pane.open_entry(name, &self.catalogue);
}
effect
}
pub(crate) fn catalogue_rows(&self) -> &[CatalogueRow] {
&self.catalogue
}
pub(super) fn on_catalogue(&mut self, result: Result<Box<CatalogueRead>, String>) -> Effect {
let read = match result {
Ok(read) => read,
Err(message) => {
if let Body::Catalogue(pane) = &mut self.body {
pane.refreshing = false;
}
if self.catalogue.is_empty() {
self.notice = Some(Notice::grave(message));
}
return Effect::None;
}
};
let CatalogueRead { rows, refreshing } = *read;
if let Body::Catalogue(pane) = &mut self.body {
let under_cursor = self
.catalogue
.get(pane.selected)
.map(|row| &row.dog.adopt_as);
pane.selected = under_cursor
.and_then(|name| rows.iter().position(|row| &row.dog.adopt_as == name))
.unwrap_or(pane.selected)
.min(rows.len().saturating_sub(1));
if let Some(open) = &pane.entry
&& !rows.iter().any(|row| &row.dog.adopt_as == open)
{
pane.entry = None;
}
pane.refreshing = refreshing;
}
self.catalogue = rows;
if let Body::Settings(settings) = &mut self.body {
settings.set_available(&self.catalogue);
}
if refreshing {
Effect::FetchCatalogue
} else {
Effect::None
}
}
pub(crate) fn catalogue_caption(&self) -> String {
let refreshing = matches!(&self.body, Body::Catalogue(pane) if pane.refreshing);
match (self.catalogue.len(), refreshing) {
(0, true) => "refreshing".to_owned(),
(0, false) => "no index cached, no network".to_owned(),
(count, true) => format!("{count} dogs in the index \u{b7} refreshing"),
(count, false) => format!("{count} dogs in the index"),
}
}
pub(super) fn on_catalogue_key(&mut self, key: KeyPress) -> Effect {
let rows = self.catalogue.len();
let Body::Catalogue(pane) = &mut self.body else {
return Effect::None;
};
match key {
KeyPress::Quit => return Effect::Quit,
KeyPress::Escape => {
if pane.direct || pane.entry.take().is_none() {
self.body = Body::FlockTable;
}
}
KeyPress::Confirm => {
let open = pane.entry.clone();
match open {
None => {
pane.entry = self
.catalogue
.get(pane.selected)
.map(|row| row.dog.adopt_as.clone());
}
Some(name) => {
let row = self.catalogue.iter().find(|row| row.dog.adopt_as == name);
if row.is_some_and(|row| self.can_install(row)) {
return self.start_install(&name);
}
if row.is_some_and(|row| self.can_upgrade(row)) {
return self.start_upgrade(&name);
}
}
}
}
KeyPress::ArmDelete => {
let deletable = pane.entry.clone().filter(|name| {
self.catalogue
.iter()
.find(|row| &row.dog.adopt_as == name)
.is_some_and(|row| self.can_delete(row))
});
if let Some(name) = deletable {
return self.ask_delete(&name);
}
}
KeyPress::SelectUp if pane.entry.is_none() => {
pane.selected = pane.selected.saturating_sub(1);
}
KeyPress::SelectDown if pane.entry.is_none() => {
pane.selected = (pane.selected + 1).min(rows.saturating_sub(1));
}
KeyPress::SelectFirst if pane.entry.is_none() => pane.selected = 0,
KeyPress::SelectLast if pane.entry.is_none() => {
pane.selected = rows.saturating_sub(1);
}
KeyPress::Help => return self.open_keymap(),
KeyPress::SelectUp
| KeyPress::SelectDown
| KeyPress::SelectFirst
| KeyPress::SelectLast
| KeyPress::Refresh
| KeyPress::Action(_)
| KeyPress::FilterStart
| KeyPress::TextChar(_)
| KeyPress::TextBackspace
| KeyPress::TextEdit(_)
| KeyPress::TextApply
| KeyPress::TextAbandon
| KeyPress::Settings
| KeyPress::Catalogue
| KeyPress::Secrets
| KeyPress::Reveal
| KeyPress::Copy
| KeyPress::TabPrev
| KeyPress::TabNext
| KeyPress::Cycle
| KeyPress::Edit
| KeyPress::Remove
| KeyPress::StepUp
| KeyPress::StepDown
| KeyPress::FoldView
| KeyPress::Collapse
| KeyPress::Bleats
| KeyPress::StreamCycle
| KeyPress::LevelCycle
| KeyPress::PageDown
| KeyPress::PageUp
| KeyPress::FollowToggle
| KeyPress::WrapToggle
| KeyPress::MatchNext
| KeyPress::MatchPrev
| KeyPress::NextGroup
| KeyPress::PrevGroup
| KeyPress::NextField
| KeyPress::PrevField
| KeyPress::Group(_)
| KeyPress::Undo
| KeyPress::Continue => {}
}
Effect::None
}
}