summaryrefslogtreecommitdiffstats
path: root/src/app/app_context.rs
blob: 8b56db485f0c0a58447a77e9e249d42d42d1de11 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
use {
    super::*,
    crate::{
        app::Mode,
        cli::{Args, TriBool},
        conf::*,
        content_search,
        errors::*,
        file_sum,
        icon::*,
        pattern::SearchModeMap,
        path::SpecialPaths,
        skin::ExtColorMap,
        syntactic::SyntaxTheme,
        tree::TreeOptions,
        verb::*,
    },
    crokey::crossterm::tty::IsTty,
    std::{
        convert::{TryFrom, TryInto},
        io,
        path::{Path, PathBuf},
    },
};

/// The container that can be passed around to provide the configuration things
/// for the whole life of the App
pub struct AppContext {

    /// Whether the application is running in a normal TTY context
    pub is_tty: bool,

    /// The initial tree root
    pub initial_root: PathBuf,

    /// The initial file to select and preview
    pub initial_file: Option<PathBuf>,

    /// Initial tree options
    pub initial_tree_options: TreeOptions,

    /// where's the config file we're using
    /// This vec can't be empty
    pub config_paths: Vec<PathBuf>,

    /// all the arguments specified at launch
    pub launch_args: Args,

    /// the "launch arguments" found in the default_flags
    /// of the config file(s)
    pub config_default_args: Option<Args>,

    /// the verbs in use (builtins and configured ones)
    pub verb_store: VerbStore,

    /// the paths for which there's a special behavior to follow (comes from conf)
    pub special_paths: SpecialPaths,

    /// the map between search prefixes and the search mode to apply
    pub search_modes: SearchModeMap,

    /// whether to show a triangle left to selected lines
    pub show_selection_mark: bool,

    /// mapping from file extension to colors (comes from conf)
    pub ext_colors: ExtColorMap,

    /// the syntect theme to use for text files previewing
    pub syntax_theme: Option<SyntaxTheme>,

    /// precomputed status to display in standard cases
    /// (ie when no verb is involved)
    pub standard_status: StandardStatus,

    /// whether we can use 24 bits colors for previewed images
    pub true_colors: bool,

    /// map extensions to icons, icon set chosen based on config
    /// Send, Sync safely because once created, everything is immutable
    pub icons: Option<Box<dyn IconPlugin + Send + Sync>>,

    /// modal (aka "vim) mode enabled
    pub modal: bool,

    /// the initial mode (only relevant when modal is true)
    pub initial_mode: Mode,

    /// Whether to support mouse interactions
    pub capture_mouse: bool,

    /// max number of panels (including preview) that can be
    /// open. Guaranteed to be at least 2.
    pub max_panels_count: usize,

    /// whether to quit broot when the user hits "escape"
    /// and there's nothing to cancel
    pub quit_on_last_cancel: bool,

    /// number of threads used by file_sum (count, size, date)
    /// computation
    pub file_sum_threads_count: usize,

    /// number of files which may be staged in one staging operation
    pub max_staged_count: usize,

    /// max file size when searching file content
    pub content_search_max_file_size: usize,

    /// the optional pattern used to change the terminal's title
    /// (if none, the title isn't modified)
    pub terminal_title_pattern: Option<ExecPattern>,

    /// whether to sync broot's work dir with the current panel's root
    pub update_work_dir: bool,

    /// Whether Kitty keyboard enhancement flags are pushed, so that
    /// we know whether we need to temporarily disable them during
    /// the execution of a terminal program.
    /// This is determined by app::run on launching the event source.
    pub keyboard_enhanced: bool,
}

impl AppContext {
    pub fn from(
        launch_args: Args,
        verb_store: VerbStore,
        config: &Conf,
    ) -> Result<Self, ProgramError> {
        let is_tty = std::io::stdout().is_tty();
        let config_default_args = config
            .default_flags
            .as_ref()
            .map(|flags| parse_default_flags(flags))
            .transpose()?;
        let config_paths = config.files.clone();
        let standard_status = StandardStatus::new(&verb_store);
        let true_colors = if let Some(value) = config.true_colors {
            value
        } else {
            are_true_colors_available()
        };
        let icons = config.icon_theme.as_ref()
            .and_then(|itn| icon_plugin(itn));
        let mut special_paths: SpecialPaths = (&config.special_paths).try_into()?;
        special_paths.add_defaults();
        let search_modes = config
            .search_modes
            .as_ref()
            .map(|map| map.try_into())
            .transpose()?
            .unwrap_or_default();
        let ext_colors = ExtColorMap::try_from(&config.ext_colors)
            .map_err(ConfError::from)?;
        let file_sum_threads_count = config.file_sum_threads_count
            .unwrap_or(file_sum::DEFAULT_THREAD_COUNT);
        if !(1..=50).contains(&file_sum_threads_count) {
            return Err(ConfError::InvalidThreadsCount{ count: file_sum_threads_count }.into());
        }
        let max_panels_count = config.max_panels_count
            .unwrap_or(2)
            .clamp(2, 100);
        let capture_mouse = match (config.capture_mouse, config.disable_mouse_capture) {
            (Some(b), _) => b, // the new "capture_mouse" argument takes precedence
            (_, Some(b)) => !b,
            _ => true,
        };
        let max_staged_count = config.max_staged_count
            .unwrap_or(10_000)
            .clamp(10, 100_000);
        let (initial_root, initial_file) = initial_root_file(&launch_args)?;

        // tree options are built from the default_flags
        // found in the config file(s) (if any) then overridden
        // by the cli args (order is important)
        let mut initial_tree_options = TreeOptions::default();
        initial_tree_options.apply_config(config)?;
        if let Some(args) = &config_default_args {
            initial_tree_options.apply_launch_args(args);
        }
        initial_tree_options.apply_launch_args(&launch_args);
        if launch_args.color == TriBool::No {
            initial_tree_options.show_selection_mark = true;
        }

        let content_search_max_file_size = config.content_search_max_file_size
            .map(|u64value| usize::try_from(u64value).unwrap_or(usize::MAX))
            .unwrap_or(content_search::DEFAULT_MAX_FILE_SIZE);

        let terminal_title_pattern = config.terminal_title.clone();

        Ok(Self {
            is_tty,
            initial_root,
            initial_file,
            initial_tree_options,
            config_paths,
            launch_args,
            config_default_args,
            verb_store,
            special_paths,
            search_modes,
            show_selection_mark: config.show_selection_mark.unwrap_or(false),
            ext_colors,
            syntax_theme: config.syntax_theme,
            standard_status,
            true_colors,
            icons,
            modal: config.modal.unwrap_or(false),
            initial_mode: config.initial_mode.unwrap_or(Mode::Command),
            capture_mouse,
            max_panels_count,
            quit_on_last_cancel: config.quit_on_last_cancel.unwrap_or(false),
            file_sum_threads_count,
            max_staged_count,
            content_search_max_file_size,
            terminal_title_pattern,
            update_work_dir: config.update_work_dir.unwrap_or(true),
            keyboard_enhanced: false,
        })
    }
    /// Return the --cmd argument, coming from the launch arguments (prefered)
    /// or from the default_flags parameter of a config file
    pub fn cmd(&self) -> Option<&str> {
        self.launch_args.cmd.as_ref().or(
            self.config_default_args.as_ref().and_then(|args| args.cmd.as_ref())
        ).map(|s| s.as_str())
    }
    pub fn initial_mode(&self) -> Mode {
        if self.modal {
            self.initial_mode
        } else {
            Mode::Input
        }
    }
}

/// try to determine whether the terminal supports true
/// colors. This doesn't work well, hence the use of an
/// optional config setting.
/// Based on https://gist.github.com/XVilka/8346728#true-color-detection
fn are_true_colors_available() -> bool {
    if let Ok(colorterm) = std::env::var("COLORTERM") {
        debug!("COLORTERM env variable = {:?}", colorterm);
        if colorterm.contains("truecolor") || colorterm.contains("24bit") {
            debug!("true colors are available");
            true
        } else {
            false
        }
    } else {
        // this is debatable... I've found some terminals with COLORTERM
        // unset but supporting true colors. As it's easy to determine
        // that true colors aren't supported when looking at previewed
        // images I prefer this value
        true
    }
}

/// Determine the initial root folder to show, and the optional
/// initial file to open in preview
fn initial_root_file(cli_args: &Args) -> Result<(PathBuf, Option<PathBuf>), ProgramError> {
    let mut file = None;
    let mut root = match cli_args.root.as_ref() {
        Some(path) => canonicalize_root(path)?,
        None => std::env::current_dir()?,
    };
    if !root.exists() {
        return Err(TreeBuildError::FileNotFound {
            path: format!("{:?}", &root),
        }.into());
    }
    if !root.is_dir() {
        // we try to open the parent directory if the passed file isn't one
        if let Some(parent) = root.parent() {
            file = Some(root.clone());
            info!("Passed path isn't a directory => opening parent instead");
            root = parent.to_path_buf();
        } else {
            // this is a weird filesystem, let's give up
            return Err(TreeBuildError::NotADirectory {
                path: format!("{:?}", &root),
            }.into());
        }
    }
    Ok((root, file))
}

#[cfg(not(windows))]
fn canonicalize_root(root: &Path) -> io::Result<PathBuf> {
    root.canonicalize()
}

#[cfg(windows)]
fn canonicalize_root(root: &Path) -> io::Result<PathBuf> {
    Ok(if root.is_relative()