grox 0.10.0

Command-line tool that searches for regex matches in a file tree.
Documentation
use std::ffi::CString;

/// Facilitates the opening of a file with a provided editor.
pub trait Opener {
    /// Does the opener handle a given editor?
    ///
    /// # Arguments
    ///
    /// * `editor` - Name of/path to the editor.
    ///
    /// # Returns
    ///
    /// `true` or `false`.
    fn handles_editor(&self, editor: &str) -> bool;

    /// Forms the `execv` arguments for opening a file (possibly at a specified line).
    ///
    /// # Arguments
    ///
    /// * `file` - Path to the file to be opened.
    /// * `line` - Line number.
    ///
    /// # Returns
    ///
    /// Arguments to pass to `execv` minus the path to the editor that should go at the beginning.
    fn form_args(&self, file: &str, line: usize) -> Vec<String>;
}

/// [`Opener`] implementation that handles all editors and disregards line numbers.
///
/// # Examples
///
/// ```
/// use grox::open::*;
/// assert!(DefaultOpener.handles_editor("foo"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert_eq!(
///     DefaultOpener.form_args("some/file.txt", 50),
///     vec!["some/file.txt".to_string()]
/// );
/// ```
pub struct DefaultOpener;

impl Opener for DefaultOpener {
    fn handles_editor(&self, _editor: &str) -> bool {
        true
    }

    fn form_args(&self, file: &str, _line: usize) -> Vec<String> {
        vec![file.to_string()]
    }
}

/// [`Opener`] implementation that supports less/more.
///
/// # Examples
///
/// ```
/// use grox::open::*;
/// assert!(LessOpener.handles_editor("less"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert!(LessOpener.handles_editor("more"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert_eq!(
///     LessOpener.form_args("some/file.text", 100),
///     vec![
///         "+100".to_string(),
///         "-N".to_string(),
///         "some/file.text".to_string()
///     ]
/// );
/// ```
pub struct LessOpener;

impl Opener for LessOpener {
    fn handles_editor(&self, editor: &str) -> bool {
        let name = file_name(editor);
        name == "less" || name == "more"
    }

    fn form_args(&self, file: &str, line: usize) -> Vec<String> {
        vec![format!("+{line}"), "-N".to_string(), file.to_string()]
    }
}

/// [`Opener`] implementation that supports vi/vim/nvim.
///
/// # Examples
///
/// ```
/// use grox::open::*;
/// assert!(ViOpener.handles_editor("vi"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert!(ViOpener.handles_editor("vim"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert!(ViOpener.handles_editor("nvim"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert_eq!(
///     ViOpener.form_args("some/file.txt", 50),
///     vec!["some/file.txt".to_string(), "+50".to_string()]
/// );
/// ```
pub struct ViOpener;

impl Opener for ViOpener {
    fn handles_editor(&self, editor: &str) -> bool {
        let name = file_name(editor);
        name == "vim" || name == "vi" || name == "nvim"
    }

    fn form_args(&self, file: &str, line: usize) -> Vec<String> {
        vec![file.to_string(), format!("+{line}")]
    }
}

/// [`Opener`] implementation that supports emacs.
///
/// # Examples
///
/// ```
/// use grox::open::*;
/// assert!(EmacsOpener.handles_editor("emacs"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert_eq!(
///     EmacsOpener.form_args("some/file.txt", 50),
///     vec!["+50".to_string(), "some/file.txt".to_string()]
/// );
/// ```
pub struct EmacsOpener;

impl Opener for EmacsOpener {
    fn handles_editor(&self, editor: &str) -> bool {
        file_name(editor) == "emacs"
    }

    fn form_args(&self, file: &str, line: usize) -> Vec<String> {
        vec![format!("+{line}"), file.to_string()]
    }
}

/// [`Opener`] implementation that supports VSCode.
///
/// # Examples
///
/// ```
/// use grox::open::*;
/// assert!(CodeOpener.handles_editor("code"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert_eq!(
///     CodeOpener.form_args("some/file.txt", 50),
///     vec!["-g".to_string(), "some/file.txt:50".to_string()]
/// );
/// ```
pub struct CodeOpener;

impl Opener for CodeOpener {
    fn handles_editor(&self, editor: &str) -> bool {
        file_name(editor) == "code"
    }

    fn form_args(&self, file: &str, line: usize) -> Vec<String> {
        vec!["-g".to_string(), format!("{file}:{line}")]
    }
}

/// [`Opener`] implementation that supports xed, Xcode's text editor CLI tool.
///
/// # Examples
///
/// ```
/// use grox::open::*;
/// assert!(XedOpener.handles_editor("xed"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert_eq!(
///     XedOpener.form_args("some/file.txt", 50),
///     vec![
///         "-l".to_string(),
///         "50".to_string(),
///         "some/file.txt".to_string()
///     ]
/// );
/// ```
pub struct XedOpener;

impl Opener for XedOpener {
    fn handles_editor(&self, editor: &str) -> bool {
        file_name(editor) == "xed"
    }

    fn form_args(&self, file: &str, line: usize) -> Vec<String> {
        vec!["-l".to_string(), line.to_string(), file.to_string()]
    }
}

/// [`Opener`] implementation that supports subl, Sublime Text's CLI tool.
///
/// # Examples
///
/// ```
/// use grox::open::*;
/// assert!(SublimeOpener.handles_editor("subl"));
/// ```
///
/// ```
/// use grox::open::*;
/// assert_eq!(
///     SublimeOpener.form_args("some/file.txt", 50),
///     vec!["some/file.txt:50".to_string()]
/// );
/// ```
pub struct SublimeOpener;

impl Opener for SublimeOpener {
    fn handles_editor(&self, editor: &str) -> bool {
        file_name(editor) == "subl"
    }

    fn form_args(&self, file: &str, line: usize) -> Vec<String> {
        vec![format!("{file}:{line}")]
    }
}

/// Attempts to open a file with a specified editor.
///
/// # Arguments
///
/// * `opener` - `Opener` implementation.
/// * `editor` - Specified editor.
/// * `file` - File to open.
/// * `line` - Line to open at (if supported by the opener).
///
/// # Returns
///
/// Only if `execvp` fails.
pub fn open(opener: &dyn Opener, editor: String, file: &str, line: usize) {
    let command = create_c_string(&editor);

    let mut args = opener.form_args(file, line);
    args.insert(0, editor);
    let args: Vec<CString> = args.into_iter().map(|a| create_c_string(&a)).collect();

    let mut ptr_args: Vec<*const i8> = args.iter().map(|a| a.as_ptr()).collect();
    ptr_args.push(std::ptr::null());
    unsafe {
        libc::execvp(command.as_ptr(), ptr_args.as_ptr());
    }
}

fn create_c_string(string: &str) -> CString {
    CString::new(string).unwrap()
}

fn file_name(path: &str) -> &str {
    path.rsplit("/").next().unwrap()
}