fm/modes/menu/
cli_menu.rs1use 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
16pub trait Execute<T> {
19 fn execute(&self, status: &Status) -> Result<T>;
20}
21
22#[derive(Clone)]
34pub struct CliCommand {
35 pub executable: String,
37 parsable_command: String,
39 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 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
80pub trait TerminalApplications<T: Execute<U>, U>: Sized + Default + Content<T> {
87 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 fn execute(&self, status: &Status) -> Result<U> {
113 self.selected().context("")?.execute(status)
114 }
115}
116
117#[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);