fm/modes/menu/
cli_menu.rs

1use std::borrow::Cow;
2
3use anyhow::Context;
4use anyhow::Result;
5use serde_yml::from_reader;
6use serde_yml::Mapping;
7
8use crate::app::Status;
9use crate::common::CLI_PATH;
10use crate::common::{is_in_path, tilde};
11use crate::impl_draw_menu_with_char;
12use crate::io::execute_with_ansi_colors;
13use crate::modes::shell_command_parser;
14use crate::{impl_content, impl_selectable, log_info, log_line};
15
16/// Simple method used to execute a command.
17/// All static command should implemtant it (cli_menu, tui_menu).
18pub trait Execute<T> {
19    fn execute(&self, status: &Status) -> Result<T>;
20}
21
22/// A command line application launcher.
23/// It's constructed from a line in a config file.
24/// Each command has a short description, a name (first word of second element)
25/// and a list of parsable parameters.
26/// See [`crate::modes::ShellCommandParser`] for a description of accetable tokens.
27///
28/// Only commands which are in `$PATH` at runtime are built from `Self::new(...)`,
29/// Commands which aren't accessible return `None`
30///
31/// Those commands should output a string (therefore be command line).
32/// No interaction with the user is possible.
33#[derive(Clone)]
34pub struct CliCommand {
35    /// The executable itself like `ls`
36    pub executable: String,
37    /// The full command with parsable arguments like %s
38    parsable_command: String,
39    /// A single line description of the command
40    pub desc: String,
41}
42
43impl CliCommand {
44    fn new(desc: String, args: String) -> Option<Self> {
45        let executable = args.split(' ').next()?;
46        if !is_in_path(executable) {
47            return None;
48        }
49        let desc = desc.replace('_', " ");
50        Some(Self {
51            executable: executable.to_owned(),
52            parsable_command: args,
53            desc,
54        })
55    }
56}
57
58impl Execute<(String, String)> for CliCommand {
59    /// Run its parsable command and capture its output.
60    /// Some environement variables are first set to ensure the colored output.
61    /// Long running commands may freeze the display.
62    fn execute(&self, status: &Status) -> Result<(String, String)> {
63        let args = shell_command_parser(&self.parsable_command, status)?;
64        log_info!("execute. {args:?}");
65        log_line!("Executed {args:?}");
66
67        let command_output = execute_with_ansi_colors(&args)?;
68        let text_output = String::from_utf8(command_output.stdout)?;
69        if !command_output.status.success() {
70            log_info!(
71                "Command {a} exited with error code {e}",
72                a = args[0],
73                e = command_output.status
74            );
75        };
76        Ok((text_output, self.parsable_command.to_owned()))
77    }
78}
79
80/// Common methods of terminal applications. Wether they require interaction
81/// and are opened in a new terminal or not and are previewed.
82/// All those applications are configurable from a config file and share their
83/// configuration.
84/// Only the yaml parsing should be implemented specifically since more
85/// information is required for some application.
86pub trait TerminalApplications<T: Execute<U>, U>: Sized + Default + Content<T> {
87    // fn new(config_file: &str) -> Self {
88    //     Self::default().update_from_config(config_file)
89    // }
90
91    fn update_from_config(&mut self, config_file: &str) {
92        let Ok(file) = std::fs::File::open(std::path::Path::new(&tilde(config_file).to_string()))
93        else {
94            log_info!("Couldn't open cli file at {config_file}. Using default");
95            return;
96        };
97        match from_reader(file) {
98            Ok(yaml) => {
99                self.parse_yaml(&yaml);
100            }
101            Err(error) => {
102                log_info!("error parsing yaml file {config_file}. Error: {error:?}");
103            }
104        }
105    }
106
107    fn parse_yaml(&mut self, yaml: &Mapping);
108
109    /// Run the selected command and capture its output.
110    /// Some environement variables are first set to ensure the colored output.
111    /// Long running commands may freeze the display.
112    fn execute(&self, status: &Status) -> Result<U> {
113        self.selected().context("")?.execute(status)
114    }
115}
116
117/// Holds the command line commands we can run and display
118/// without leaving FM.
119/// Those are non interactive commands displaying some info about the current
120/// file tree or setup.
121#[derive(Clone, Default)]
122pub struct CliApplications {
123    pub content: Vec<CliCommand>,
124    index: usize,
125    pub desc_size: usize,
126}
127
128impl CliApplications {
129    pub fn setup(&mut self) {
130        self.update_from_config(CLI_PATH);
131        self.update_desc_size();
132    }
133
134    pub fn update_desc_size(&mut self) {
135        let desc_size = self
136            .content
137            .iter()
138            .map(|cli| cli.desc.len())
139            .fold(usize::MIN, |a, b| a.max(b));
140        self.desc_size = desc_size;
141    }
142}
143
144impl TerminalApplications<CliCommand, (String, String)> for CliApplications {
145    fn parse_yaml(&mut self, yaml: &Mapping) {
146        for (key, mapping) in yaml {
147            let Some(name) = key.as_str() else {
148                continue;
149            };
150            let Some(command) = mapping.get("command") else {
151                continue;
152            };
153            let Some(command) = command.as_str() else {
154                continue;
155            };
156            let Some(cli_command) = CliCommand::new(name.to_owned(), command.to_owned()) else {
157                continue;
158            };
159            self.content.push(cli_command)
160        }
161    }
162}
163
164impl CowStr for CliCommand {
165    fn cow_str(&self) -> Cow<str> {
166        let desc_size = 20_usize.saturating_sub(self.desc.len());
167        format!(
168            "{desc}{space:<desc_size$}{exe}",
169            desc = self.desc,
170            exe = self.executable,
171            space = " "
172        )
173        .into()
174    }
175}
176
177impl_selectable!(CliApplications);
178impl_content!(CliApplications, CliCommand);
179impl_draw_menu_with_char!(CliApplications, CliCommand);