rura 1.5.0

Interactive TUI pipeline editor built for rapid iteration
use itertools::Itertools;
use log::{debug, error};
use std::process::Command;

pub trait Completer {
    fn completions(&self, prefix: &str, completion_type: CompletionType) -> Vec<String>;
}

#[allow(dead_code)]
pub struct NoopCompleter;

impl Completer for NoopCompleter {
    fn completions(&self, prefix: &str, completion_type: CompletionType) -> Vec<String> {
        debug!(
            "calling noop completions [{:?}]: '{}'",
            completion_type, prefix
        );
        vec![]
    }
}

pub struct BashCompleter;

impl Completer for BashCompleter {
    fn completions(&self, prefix: &str, completion_type: CompletionType) -> Vec<String> {
        debug!(
            "calling bash completions [{:?}]: '{}'",
            completion_type, prefix
        );

        let comp_type_str = match completion_type {
            CompletionType::Command => "-c",
            CompletionType::File => "-f",
        };

        let output = Command::new("/usr/bin/env")
            .args([
                "bash",
                "-c",
                &format!("compgen {} -- \"{}\"", comp_type_str, prefix),
            ])
            .output();

        match output {
            Ok(output) => {
                let stdout = String::from_utf8_lossy(&output.stdout);
                let completions: Vec<String> = stdout
                    .lines()
                    .map(|s| s.to_string())
                    .unique()
                    .sorted_by(|a, b| a.len().cmp(&b.len()))
                    .sorted()
                    .collect();

                debug!(
                    "completion results [{}]: {:?}",
                    completions.len(),
                    completions
                );

                completions
            }
            Err(e) => {
                error!("Failed fetching completions {}", e);
                vec![]
            }
        }
    }
}

pub struct ZshCompleter;

impl Completer for ZshCompleter {
    fn completions(&self, prefix: &str, completion_type: CompletionType) -> Vec<String> {
        debug!(
            "calling zsh completions [{:?}]: '{}'",
            completion_type, prefix
        );

        let cmd = match completion_type {
            CompletionType::Command => {
                format!("print -l ${{(k)commands[(I){}*]}}", prefix)
            }
            CompletionType::File => {
                if prefix.starts_with("~") {
                    format!("setopt extended_glob && print -l {}*(N)", prefix)
                } else {
                    format!("setopt extended_glob && print -l (#i){}*(N)", prefix)
                }
            }
        };

        let output = Command::new("/usr/bin/env")
            .args(["zsh", "-c", &cmd])
            .output();

        match output {
            Ok(output) => {
                let stdout = String::from_utf8_lossy(&output.stdout);
                let completions: Vec<String> = stdout
                    .lines()
                    .map(|s| s.to_string())
                    .filter(|s| !s.is_empty())
                    .collect_vec();

                debug!(
                    "completion results [{}]: {:?}",
                    completions.len(),
                    completions
                );

                completions
            }
            Err(e) => {
                error!("Failed fetching zsh completions {}", e);
                vec![]
            }
        }
    }
}

pub struct FishCompleter;

impl Completer for FishCompleter {
    fn completions(&self, prefix: &str, completion_type: CompletionType) -> Vec<String> {
        debug!(
            "calling fish completions [{:?}]: '{}'",
            completion_type, prefix
        );

        let cmd = match completion_type {
            CompletionType::Command => {
                format!("complete -C {} | cut -f1", prefix)
            }
            CompletionType::File => {
                format!("complete -C \"cat {}\"", prefix)
            }
        };

        let output = Command::new("/usr/bin/env")
            .args(["fish", "-c", &cmd])
            .output();

        match output {
            Ok(output) => {
                let stdout = String::from_utf8_lossy(&output.stdout);
                let completions: Vec<String> = stdout
                    .lines()
                    .map(|s| s.to_string())
                    .filter(|s| !s.is_empty())
                    .collect_vec();

                debug!(
                    "completion results [{}]: {:?}",
                    completions.len(),
                    completions
                );

                completions
            }
            Err(e) => {
                error!("Failed fetching fish completions {}", e);
                vec![]
            }
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum CompletionType {
    Command,
    File,
}

pub struct Completers;

impl Completers {
    pub fn for_shell(shell: &str) -> Box<dyn Completer> {
        match shell {
            "bash" => Box::new(BashCompleter {}),
            "zsh" => Box::new(ZshCompleter {}),
            "fish" => Box::new(FishCompleter {}),
            "sh" => Box::new(NoopCompleter {}),
            _ => Box::new(BashCompleter {}),
        }
    }
}