Skip to main content

broot/verb/
internal_focus.rs

1//! utility functions to help handle the `:focus` internal
2use {
3    super::*,
4    crate::{
5        app::*,
6        browser::BrowserState,
7        command::TriggerType,
8        display::Screen,
9        path::{
10            self,
11            PathAnchor,
12        },
13        pattern::InputPattern,
14        preview::PreviewState,
15        task_sync::Dam,
16        tree::TreeOptions,
17    },
18    std::path::{
19        Path,
20        PathBuf,
21    },
22};
23
24pub fn on_path(
25    path: PathBuf,
26    screen: Screen,
27    tree_options: TreeOptions,
28    in_new_panel: bool,
29    con: &AppContext,
30) -> CmdResult {
31    if in_new_panel {
32        new_panel_on_path(
33            path,
34            screen,
35            tree_options,
36            PanelPurpose::None,
37            con,
38            HDir::Right,
39        )
40    } else {
41        new_state_on_path(path, screen, tree_options, con)
42    }
43}
44
45pub fn new_state_on_path(
46    path: PathBuf,
47    screen: Screen,
48    tree_options: TreeOptions,
49    con: &AppContext,
50) -> CmdResult {
51    let path = path::closest_dir(&path);
52    CmdResult::from_optional_browser_state(
53        BrowserState::new(path, tree_options, screen, con, &Dam::unlimited()),
54        None,
55        false,
56    )
57}
58
59#[allow(unused_mut)]
60pub fn new_panel_on_path(
61    mut path: PathBuf,
62    screen: Screen,
63    mut tree_options: TreeOptions,
64    purpose: PanelPurpose,
65    con: &AppContext,
66    direction: HDir,
67) -> CmdResult {
68    #[cfg(not(windows))]
69    // We try to canonicalize the path, mostly to resolve links
70    // We don't do it on Windows due to issue #809
71    if let Ok(canonic) = std::fs::canonicalize(&path) {
72        path = canonic;
73        // If it can't be canonicalized, we'll let the panel state
74        // deal with the original path
75    }
76    if purpose.is_preview() {
77        let pattern = tree_options.pattern.tree_to_preview();
78        CmdResult::NewPanel {
79            state: Box::new(PreviewState::new(path, pattern, None, tree_options, con)),
80            purpose,
81            direction,
82        }
83    } else {
84        let path = path::closest_dir(&path);
85        // We remove the pattern on opening another browser. This will probably
86        // be configuratble with a clear_pattern verb option in the future
87        tree_options.pattern = InputPattern::none();
88        match BrowserState::new(path, tree_options, screen, con, &Dam::unlimited()) {
89            Ok(os) => CmdResult::NewPanel {
90                state: Box::new(os),
91                purpose,
92                direction,
93            },
94            Err(e) => CmdResult::DisplayError(e.to_string()),
95        }
96    }
97}
98
99/// Compute the path to go to in case of the internal being triggered from
100/// the input.
101///
102/// This path depends on the verb (which may hardcore the path or have a
103/// pattern), from the selection,
104fn path_from_input(
105    verb: &Verb,
106    internal_exec: &InternalExecution,
107    base_path: &Path, // either the selected path or the root path
108    input_arg: Option<&String>,
109    app_state: &AppState,
110    con: &AppContext,
111) -> PathBuf {
112    match (input_arg, internal_exec.arg.as_ref()) {
113        (Some(input_arg), Some(verb_arg)) => {
114            // The verb probably defines some pattern which uses the input.
115            // For example:
116            // {
117            //     invocation: "gotar {path}"
118            //     execution: ":focus {path}/target"
119            // }
120            // (or that input is useless)
121            let path_builder = ExecutionBuilder::with_invocation(
122                verb.invocation_parser.as_ref(),
123                SelInfo::from_path(base_path),
124                app_state,
125                Some(input_arg),
126            );
127            path_builder.path(verb_arg, con)
128        }
129        (Some(input_arg), None) => {
130            // the verb defines nothing
131            // The :focus internal execution was triggered from the
132            // input (which must be a kind of alias for :focus)
133            // so we do exactly what the input asks for
134            path::path_from(base_path, PathAnchor::Unspecified, input_arg)
135        }
136        (None, Some(verb_arg)) => {
137            // the verb defines the path where to go..
138            // the internal_execution specifies the path to use
139            // (it may come from a configured verb whose execution is
140            //  `:focus some/path`).
141            // The given path may be relative hence the need for the
142            // state's selection
143            // (we assume a check before ensured it doesn't need an input)
144            let path_builder = ExecutionBuilder::with_invocation(
145                verb.invocation_parser.as_ref(),
146                SelInfo::from_path(base_path),
147                app_state,
148                None,
149            );
150            path_builder.path(verb_arg, con)
151        }
152        (None, None) => {
153            // user only wants to open the selected path, either in the same panel or
154            // in a new one
155            base_path.to_path_buf()
156        }
157    }
158}
159
160pub fn get_status_markdown(
161    verb: &Verb,
162    internal_exec: &InternalExecution,
163    sel_info: SelInfo<'_>,
164    invocation: &VerbInvocation,
165    app_state: &AppState,
166    con: &AppContext,
167) -> String {
168    let base_path = sel_info.one_path().unwrap_or(&app_state.root);
169    let path = path_from_input(
170        verb,
171        internal_exec,
172        base_path,
173        invocation.args.as_ref(),
174        app_state,
175        con,
176    );
177    format!("Hit *enter* to focus `{}`", path.to_string_lossy())
178}
179
180/// general implementation for verbs based on the :focus internal with optionally
181/// a bang or an argument.
182#[allow(clippy::too_many_arguments)]
183pub fn on_internal(
184    internal_exec: &InternalExecution,
185    input_invocation: Option<&VerbInvocation>,
186    trigger_type: TriggerType,
187    selected_path: &Path,
188    is_root_selected: bool,
189    tree_options: TreeOptions,
190    app_state: &AppState,
191    cc: &CmdContext,
192) -> CmdResult {
193    let con = &cc.app.con;
194    let screen = cc.app.screen;
195    let bang = input_invocation
196        .map(|inv| inv.bang)
197        .unwrap_or(internal_exec.bang);
198    let input_arg = input_invocation
199        .as_ref()
200        .and_then(|invocation| invocation.args.as_ref());
201    match trigger_type {
202        TriggerType::Input(verb) => {
203            let path = path_from_input(
204                verb,
205                internal_exec,
206                selected_path,
207                input_arg,
208                app_state,
209                cc.app.con,
210            );
211            on_path(path, screen, tree_options, bang, con)
212        }
213        _ => {
214            // the :focus internal was triggered by a key
215            if let Some(arg) = &internal_exec.arg {
216                // the internal_execution specifies the path to use
217                // (it may come from a configured verb whose execution is
218                //  `:focus some/path` or `:focus {initial-root}̀).
219                // The given path may be relative hence the need for the
220                // state's selection
221                let path_builder = ExecutionBuilder::without_invocation(
222                    SelInfo::from_path(selected_path),
223                    app_state,
224                );
225                let path = path_builder.path(arg, con);
226                let bang = input_invocation
227                    .map(|inv| inv.bang)
228                    .unwrap_or(internal_exec.bang);
229                on_path(path, screen, tree_options, bang, con)
230            } else if let Some(input_arg) = input_arg {
231                let base_dir = selected_path.to_string_lossy();
232                let path = path::path_from(&*base_dir, PathAnchor::Unspecified, input_arg);
233                if bang {
234                    // Unsure this special behavior is really needed. It was based
235                    // on the assumption that the user wanted to edit an argument
236                    // of a verb, and that the trigering was a key (but it can also
237                    // be another medium, like a command sequence or with the server)
238                    let arg_type = SelectionType::Any; // We might do better later
239                    let purpose = PanelPurpose::ArgEdition { arg_type };
240                    new_panel_on_path(path, screen, tree_options, purpose, con, HDir::Right)
241                } else {
242                    on_path(path, screen, tree_options, bang, con)
243                }
244            } else {
245                // user only wants to open the selected path, either in the same panel or
246                // in a new one
247                let mut path = selected_path.to_path_buf();
248                if !bang && is_root_selected {
249                    // the selected path is the root, focusing it would do nothing, so
250                    // we rather go up one level
251                    if let Some(parent_path) = selected_path.parent() {
252                        path = parent_path.to_path_buf();
253                    }
254                }
255                on_path(path, screen, tree_options, bang, con)
256            }
257        }
258    }
259}