kubie 0.28.0

A more powerful alternative to kubectx and kubens.
use std::env;
use std::fmt::Display;
use std::path::PathBuf;
use std::process::Command;

use anyhow::{anyhow, Context, Result};
use which::which;

use crate::cmd::{select_or_list_context, SelectResult};
use crate::kubeconfig;
use crate::settings::Settings;

struct EditorCommand {
    executable: PathBuf,
    args: Vec<String>,
}

impl Display for EditorCommand {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.args.is_empty() {
            return write!(f, "{}", self.executable.display());
        }
        write!(f, "{} {}", self.executable.display(), self.args.join(" "))
    }
}

fn parse_editor_command(raw: &str) -> Result<EditorCommand> {
    let mut parts = raw.split_whitespace();
    let executable = parts.next().context("executable is empty")?.into();
    let args: Vec<String> = parts.map(String::from).collect();
    Ok(EditorCommand { executable, args })
}

fn get_editor(settings: &Settings) -> Result<EditorCommand> {
    settings
        .default_editor
        .as_deref()
        .map(|editor| {
            parse_editor_command(editor)
                .with_context(|| format!("unable to parse default_editor command {}", editor))
        })
        .or_else(|| {
            env::var("EDITOR").ok().map(|editor| {
                parse_editor_command(&editor)
                    .with_context(|| format!("unable to parse EDITOR command {}", editor))
            })
        })
        .transpose()?
        .or_else(|| {
            ["nvim", "vim", "emacs", "vi", "nano"].iter().find_map(|editor| {
                which(editor).ok().map(|path| EditorCommand {
                    executable: path,
                    args: vec![],
                })
            })
        })
        .ok_or_else(|| anyhow!("Could not find any editor to use"))
}

pub fn edit_context(settings: &Settings, context_name: Option<String>) -> Result<()> {
    let mut installed = kubeconfig::get_installed_contexts(settings)?;
    installed.contexts.sort_by(|a, b| a.item.name.cmp(&b.item.name));

    let context_name = match context_name {
        Some(context_name) => context_name,
        None => match select_or_list_context(&settings.fzf, &mut installed)? {
            SelectResult::Selected(x) => x,
            _ => return Ok(()),
        },
    };

    let context_src = installed
        .find_context_by_name(&context_name)
        .ok_or_else(|| anyhow!("Could not find context {}", context_name))?;

    let command = get_editor(settings)?;

    let mut job = Command::new(&command.executable)
        .args(&command.args)
        .arg(context_src.source.as_ref())
        .spawn()
        .context(format!("Failed to spawn editor command '{}'", command))?;
    job.wait()?;

    Ok(())
}

pub fn edit_config(settings: &Settings) -> Result<()> {
    let command = get_editor(settings)?;
    let settings_path = Settings::path();

    let mut job = Command::new(&command.executable)
        .args(&command.args)
        .arg(settings_path)
        .spawn()
        .context(format!("Failed to spawn editor command '{}'", command))?;
    job.wait()?;

    Ok(())
}