depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! TUI utility functions.

use std::collections::{BTreeMap, BTreeSet};
use std::pin::Pin;

use crate::bmap;
use crate::entities::custom_command::CustomCommand;
use crate::entities::environment::with_empty_env;

/// Empty edit function type.
pub type EditFn<T> = fn(&mut T) -> Pin<Box<dyn Future<Output = anyhow::Result<()>>>>;
/// Empty create function type.
pub type CreateFn<T> = fn() -> Pin<Box<dyn Future<Output = anyhow::Result<T>>>>;
/// Empty copy function type.
pub type CopyFn<T> = fn(&T) -> Pin<Box<dyn Future<Output = anyhow::Result<T>>>>;

/// Function to edit multiple entities with optional registries.
#[allow(clippy::too_many_arguments)]
pub async fn edit_entities<T: Clone>(
  entities: &mut Vec<T>,
  registry: Option<&Vec<T>>,
  title: &str,
  show_entity: impl Fn(&T) -> String,
  mut edit: Option<impl AsyncFnMut(&mut T) -> anyhow::Result<()>>,
  mut create: Option<impl AsyncFnMut() -> anyhow::Result<T>>,
  mut copy: Option<impl AsyncFnMut(&T) -> anyhow::Result<T>>,
  reorder: bool,
) -> anyhow::Result<()> {
  fn remove_entity<T>(entities: &mut Vec<T>, show_entity: &impl Fn(&T) -> String) -> anyhow::Result<()> {
    let mut options = vec![];
    for (pos, item) in entities.iter().enumerate() {
      let item = format!("Remove #{}: {}", pos + 1, show_entity(item));
      options.push(item);
    }

    let selected = inquire_reorder::Select::new("Remove element:", options.clone()).prompt_skippable()?;
    let selected = if let Some(s) = selected {
      s
    } else {
      return Ok(());
    };

    for (pos, item) in entities.iter().enumerate() {
      if selected == format!("Remove #{}: {}", pos + 1, show_entity(item)) {
        entities.remove(pos);
        break;
      }
    }
    Ok(())
  }

  async fn copy_entity<T>(
    entities: &mut Vec<T>,
    show_entity: &impl Fn(&T) -> String,
    copy: &mut Option<impl AsyncFnMut(&T) -> anyhow::Result<T>>,
  ) -> anyhow::Result<()> {
    let mut options = vec![];
    for (pos, item) in entities.iter().enumerate() {
      let item = format!("Copy #{}: {}", pos + 1, show_entity(item));
      options.push(item);
    }

    let selected = inquire_reorder::Select::new("Copy element:", options.clone()).prompt_skippable()?;
    let selected = if let Some(s) = selected {
      s
    } else {
      return Ok(());
    };

    for (pos, item) in entities.iter().enumerate() {
      if selected == format!("Copy #{}: {}", pos + 1, show_entity(item)) {
        let new = copy.as_mut().unwrap()(item).await?;
        entities.push(new);
        break;
      }
    }
    Ok(())
  }

  fn reorder_entities<T>(entities: &mut Vec<T>, show_entity: &impl Fn(&T) -> String) -> anyhow::Result<()> {
    let mut values = bmap!();
    let mut options = vec![];

    for (pos, item) in entities.iter_mut().enumerate() {
      let item = format!("#{}: {}", pos + 1, show_entity(item));
      options.push(item.clone());
      values.insert(item, pos);
    }

    let reordered = inquire_reorder::Reorder::new("Reorder elements:", options).prompt_skippable()?;
    let reordered = if let Some(r) = reordered {
      r
    } else {
      return Ok(());
    };

    let mut new_entities = vec![];
    let mut old_entities = std::mem::take(entities)
      .into_iter()
      .map(|item| Some(item))
      .collect::<Vec<_>>();

    for each in reordered {
      let pos = *values.get(&each).unwrap();
      let item = old_entities[pos].take().unwrap();
      new_entities.push(item);
    }

    *entities = new_entities;

    Ok(())
  }

  async fn add_entity<T: Clone>(
    registry: &Option<&Vec<T>>,
    show_entity: &impl Fn(&T) -> String,
    create: &mut Option<impl AsyncFnMut() -> anyhow::Result<T>>,
  ) -> anyhow::Result<T> {
    if let Some(registry) = registry
      && !registry.is_empty()
    {
      let mut options = vec![];
      let mut registry_map = BTreeMap::new();

      for (idx, item) in registry.iter().enumerate() {
        let display = format!("From registry: {}", show_entity(item));
        options.push(display.clone());
        registry_map.insert(display, idx);
      }

      if create.is_some() {
        options.push("Create new".to_string());
      }

      let selection = inquire_reorder::Select::new("Add entity:", options).prompt()?;

      if selection == "Create new" {
        if let Some(create_fn) = create.as_mut() {
          create_fn().await
        } else {
          anyhow::bail!("No create function provided")
        }
      } else {
        let pos = *registry_map.get(&selection).unwrap();
        Ok(registry[pos].clone())
      }
    } else if let Some(create_fn) = create.as_mut() {
      create_fn().await
    } else {
      anyhow::bail!("No create function provided")
    }
  }

  loop {
    let mut options = Vec::with_capacity(entities.len() + 4);
    let mut values = BTreeMap::new();

    for (pos, item) in entities.iter().enumerate() {
      let view = format!("#{}: {}", pos + 1, show_entity(item));
      options.push(view.clone());
      values.insert(view, pos);
    }

    if create.is_some() || registry.as_ref().is_some_and(|r| !r.is_empty()) {
      options.push("Add".to_string());
    }
    if copy.is_some() {
      options.push("Copy".to_string());
    }
    options.push("Remove".to_string());
    if reorder && entities.len() > 1 {
      options.push("Reorder".to_string());
    }

    if let Ok(Some(selection)) = inquire_reorder::Select::new(title, options.clone()).prompt_skippable() {
      if selection.eq("Add") {
        let entity = add_entity(&registry, &show_entity, &mut create).await?;
        entities.push(entity);
      } else if selection.eq("Remove") {
        remove_entity(entities, &show_entity)?;
      } else if selection.eq("Reorder") {
        reorder_entities(entities, &show_entity)?;
      } else if selection.eq("Copy") {
        copy_entity(entities, &show_entity, &mut copy).await?;
      } else if let Some(edit) = edit.as_mut()
        && let Some((_, pos)) = values.iter_mut().find(|(k, _)| k.as_str().eq(selection.as_str()))
      {
        let _ = edit(&mut entities[*pos]).await;
      }
    } else {
      break;
    }
  }

  Ok(())
}

/// Function to edit multiple entities with optional registries in a BTreeMap container.
pub async fn edit_btreemap_entities<K: Clone + Ord, V: Clone>(
  entities: &mut BTreeMap<K, V>,
  registry: Option<&BTreeMap<K, V>>,
  title: &str,
  show_entity: impl Fn(&(K, V)) -> String,
  edit: Option<impl AsyncFnMut(&mut (K, V)) -> anyhow::Result<()>>,
  create: Option<impl AsyncFnMut() -> anyhow::Result<(K, V)>>,
  copy: Option<impl AsyncFnMut(&(K, V)) -> anyhow::Result<(K, V)>>,
) -> anyhow::Result<()> {
  let mut opts = vec![];
  for pair in entities.iter() {
    opts.push((pair.0.clone(), pair.1.clone()));
  }
  let mut reg_opts = vec![];
  let registry = btreemap_registry_to_vec(&mut reg_opts, registry);
  edit_entities(&mut opts, registry, title, show_entity, edit, create, copy, false).await?;
  let mut vals = bmap!();
  for (k, v) in opts {
    vals.insert(k, v);
  }
  if !vals.is_empty() {
    *entities = vals;
  }
  Ok(())
}

/// Converts optional BTreeMap to given Vec.
pub fn btreemap_registry_to_vec<'a, K: Clone, V: Clone>(
  reg_opts: &'a mut Vec<(K, V)>,
  registry: Option<&'a BTreeMap<K, V>>,
) -> Option<&'a Vec<(K, V)>> {
  if let Some(registry) = registry {
    for pair in registry {
      reg_opts.push((pair.0.clone(), pair.1.clone()));
    }
    Some(reg_opts)
  } else {
    None
  }
}

/// Converts optional BTreeMap to given Vec with only values, no keys.
pub fn btreemap_registry_to_vec_only_values<'a, K, V: Clone>(
  reg_opts: &'a mut Vec<V>,
  registry: Option<&'a BTreeMap<K, V>>,
) -> Option<&'a Vec<V>> {
  if let Some(registry) = registry {
    for pair in registry {
      reg_opts.push(pair.1.clone());
    }
    Some(reg_opts)
  } else {
    None
  }
}

/// Converts optional BTreeSet to given Vec.
pub fn btreeset_registry_to_vec<'a, V: Clone>(
  reg_opts: &'a mut Vec<V>,
  registry: Option<&'a BTreeSet<V>>,
) -> Option<&'a Vec<V>> {
  if let Some(registry) = registry {
    for item in registry.iter() {
      reg_opts.push(item.clone());
    }
    Some(reg_opts)
  } else {
    None
  }
}

/// Function to edit multiple entities with optional registries in a BTreeSet container.
pub async fn edit_btreeset_entities<T: Clone + Ord>(
  entities: &mut BTreeSet<T>,
  registry: Option<&BTreeSet<T>>,
  title: &str,
  show_entity: impl Fn(&T) -> String,
  edit: Option<impl AsyncFnMut(&mut T) -> anyhow::Result<()>>,
  create: Option<impl AsyncFnMut() -> anyhow::Result<T>>,
  copy: Option<impl AsyncFnMut(&T) -> anyhow::Result<T>>,
) -> anyhow::Result<()> {
  // we clone entities because if error will occur, the data could be dropped
  let mut en2 = entities.clone();
  let mut en2_vec = en2.into_iter().collect::<Vec<_>>();
  let registry = registry.map(|set| set.iter().cloned().collect::<Vec<_>>());
  edit_entities(
    &mut en2_vec,
    registry.as_ref(),
    title,
    show_entity,
    edit,
    create,
    copy,
    false,
  )
  .await?;
  en2 = en2_vec.into_iter().collect::<BTreeSet<_>>();
  *entities = en2;
  Ok(())
}

/// Edits multiple lines of text via `nano`.
///
/// Ignores empty lines. Also outputs prompt as a comment part above the text.
pub async fn edit_with_nano(
  prompt: impl AsRef<str>,
  text: String,
  syntax: Option<&'static str>,
) -> anyhow::Result<String> {
  let suffix = uuid::Uuid::new_v4().simple().to_string();
  let filepath = format!("/tmp/depl.{suffix}.{}", syntax.unwrap_or("txt"));

  let prompt = prompt
    .as_ref()
    .split('\n')
    .map(|s| format!("# {s}"))
    .collect::<Vec<_>>()
    .join("\n");
  let content = prompt.clone() + "\n\n" + text.as_str() + "\n";

  std::fs::write(&filepath, content.as_bytes())?;
  with_empty_env(async |env| CustomCommand::run_simple_observer(&env, format!("nano {filepath}")).await).await?;
  let mut content = std::fs::read_to_string(&filepath)?;
  std::fs::remove_file(&filepath)?;

  content = content
    .replace(&prompt, "")
    // twice to ensure there is no empty lines
    .replace("\n\n", "\n")
    .replace("\n\n", "\n")
    .trim()
    .to_string();

  Ok(content)
}