Skip to main content

broot/
launchable.rs

1use {
2    crate::{
3        app::AppContext,
4        display::{
5            DisplayableTree,
6            Screen,
7            W,
8        },
9        errors::ProgramError,
10        skin::{
11            ExtColorMap,
12            StyleMap,
13        },
14        tree::Tree,
15    },
16    crokey::crossterm::{
17        QueueableCommand,
18        cursor,
19        event::{
20            DisableMouseCapture,
21            EnableMouseCapture,
22        },
23        terminal::{
24            self,
25            EnterAlternateScreen,
26            LeaveAlternateScreen,
27        },
28    },
29    opener,
30    std::{
31        env,
32        io::{
33            self,
34            Write,
35        },
36        path::PathBuf,
37        path::Path,
38        process::Command,
39    },
40    which::which,
41};
42
43/// description of a possible launch of an external program
44/// A launchable can only be executed on end of life of broot.
45#[derive(Debug)]
46pub enum Launchable {
47    /// just print something on stdout on end of broot
48    Printer { to_print: String },
49
50    /// print the tree on end of broot
51    TreePrinter {
52        tree: Box<Tree>,
53        skin: Box<StyleMap>,
54        ext_colors: ExtColorMap,
55        width: u16,
56        height: u16,
57    },
58
59    /// execute an external program
60    Program {
61        exe: String,
62        args: Vec<String>,
63        working_dir: Option<PathBuf>,
64        switch_terminal: bool,
65        capture_mouse: bool,
66        keyboard_enhanced: bool,
67    },
68
69    /// open a path
70    SystemOpen { path: PathBuf },
71}
72
73/// If a part starts with a '$', replace it by the environment variable of the same name.
74/// This part is split too (because of <https://github.com/Canop/broot/issues/114>)
75fn resolve_env_variables(parts: Vec<String>) -> Vec<String> {
76    let mut resolved = Vec::new();
77    for part in parts {
78        if let Some(var_name) = part.strip_prefix('$') {
79            if let Ok(val) = env::var(var_name) {
80                resolved.extend(val.split(' ').map(ToString::to_string));
81                continue;
82            }
83            if var_name == "EDITOR" {
84                debug!("Env var $EDITOR not set, looking at editor command for fallback");
85                if let Ok(editor) = which("editor") {
86                    if let Some(editor) = editor.to_str() {
87                        debug!("Using editor solved as {editor:?}");
88                        resolved.push(editor.to_string());
89                        continue;
90                    }
91                }
92            }
93        }
94        resolved.push(part);
95    }
96    resolved
97}
98
99impl Launchable {
100    pub fn opener(path: PathBuf) -> Launchable {
101        Launchable::SystemOpen { path }
102    }
103    pub fn printer(to_print: String) -> Launchable {
104        Launchable::Printer { to_print }
105    }
106    pub fn tree_printer(
107        tree: &Tree,
108        screen: Screen,
109        style_map: StyleMap,
110        ext_colors: ExtColorMap,
111    ) -> Launchable {
112        Launchable::TreePrinter {
113            tree: Box::new(tree.clone()),
114            skin: Box::new(style_map),
115            ext_colors,
116            width: screen.width,
117            height: (tree.lines.len() as u16).min(screen.height - 1),
118        }
119    }
120
121    pub fn program(
122        parts: Vec<String>,
123        working_dir: Option<PathBuf>,
124        switch_terminal: bool,
125        con: &AppContext,
126    ) -> io::Result<Launchable> {
127        let mut parts = resolve_env_variables(parts).into_iter();
128        match parts.next() {
129            Some(exe) => Ok(Launchable::Program {
130                exe,
131                args: parts.collect(),
132                working_dir,
133                switch_terminal,
134                capture_mouse: con.capture_mouse,
135                keyboard_enhanced: con.keyboard_enhanced,
136            }),
137            None => Err(io::Error::other("Empty launch string")),
138        }
139    }
140
141    pub fn execute(
142        &self,
143        mut w: Option<&mut W>,
144    ) -> Result<(), ProgramError> {
145        match self {
146            Launchable::Printer { to_print } => {
147                println!("{to_print}");
148                Ok(())
149            }
150            Launchable::TreePrinter {
151                tree,
152                skin,
153                ext_colors,
154                width,
155                height,
156            } => {
157                let dp = DisplayableTree::out_of_app(tree, skin, ext_colors, *width, *height);
158                dp.write_on(&mut std::io::stdout())
159            }
160            Launchable::Program {
161                working_dir,
162                switch_terminal,
163                exe,
164                args,
165                capture_mouse,
166                keyboard_enhanced,
167            } => {
168                debug!("working_dir: {working_dir:?}");
169                debug!("switch_terminal: {switch_terminal:?}");
170                if *switch_terminal {
171                    // we restore the normal terminal in case the executable
172                    // is a terminal application, and we'll switch back to
173                    // broot's alternate terminal when we're back to broot
174                    if let Some(ref mut w) = &mut w {
175                        if *keyboard_enhanced {
176                            crokey::pop_keyboard_enhancement_flags()?;
177                        }
178                        w.queue(cursor::Show)?;
179                        w.queue(LeaveAlternateScreen)?;
180                        if *capture_mouse {
181                            w.queue(DisableMouseCapture)?;
182                        }
183                        terminal::disable_raw_mode()?;
184                        w.flush()?;
185                    }
186                }
187                let mut old_working_dir = None;
188                if let Some(working_dir) = working_dir {
189                    old_working_dir = std::env::current_dir().ok();
190                    if !try_set_current_dir(working_dir) {
191                        warn!("Unable to set working dir to {working_dir:?}");
192                        old_working_dir = None;
193                    }
194                }
195                let exec_res = Command::new(exe)
196                    .args(args.iter())
197                    .spawn()
198                    .and_then(|mut p| p.wait())
199                    .map_err(|source| ProgramError::LaunchError {
200                        program: exe.clone(),
201                        source,
202                    });
203                if *switch_terminal {
204                    if let Some(ref mut w) = &mut w {
205                        terminal::enable_raw_mode()?;
206                        if *capture_mouse {
207                            w.queue(EnableMouseCapture)?;
208                        }
209                        w.queue(EnterAlternateScreen)?;
210                        w.queue(cursor::Hide)?;
211                        w.flush()?;
212                        if *keyboard_enhanced {
213                            crokey::push_keyboard_enhancement_flags()?;
214                        }
215                    }
216                }
217                if let Some(old_working_dir) = old_working_dir {
218                    if !try_set_current_dir(&old_working_dir) {
219                        warn!("Unable to restore working dir to {old_working_dir:?}");
220                    }
221                }
222                exec_res?; // we trigger the error display after restoration
223                Ok(())
224            }
225            Launchable::SystemOpen { path } => {
226                opener::open(path)?;
227                Ok(())
228            }
229        }
230    }
231}
232
233/// Try set the current dir to the given path, and if it fails, try to climb the path until an
234/// existing folder is found. Return true if the current dir has been changed, false otherwise.
235pub fn try_set_current_dir(mut dir: &Path) -> bool {
236    loop {
237        if std::env::set_current_dir(dir).is_ok() {
238            debug!("Working dir set to {dir:?}");
239            return true;
240        }
241        let Some(parent_dir) = dir.parent() else {
242            return false;
243        };
244        dir = parent_dir;
245    }
246}