summaryrefslogtreecommitdiffstats
path: root/src/conf.rs
blob: d02901d2e1a7d4bac37b5603dd075dc0becce6ee (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
//! manage reading the verb shortcuts from the configuration file,
//! initializing if if it doesn't yet exist

use custom_error::custom_error;
use directories::ProjectDirs;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::result::Result;
use toml::{self, Value};

custom_error! {pub ConfError
    Io{source: io::Error}           = "unable to read from the file",
    Toml{source: toml::de::Error}   = "unable to parse TOML",
    MissingField{txt: String}       = "missing field in conf",
}

/// what's needed to handle a verb
#[derive(Debug)]
pub struct VerbConf {
    pub name: String,
    pub invocation: String,
    pub execution: String,
    pub from_shell: bool,
}

#[derive(Debug)]
pub struct Conf {
    pub verbs: Vec<VerbConf>,
}

fn string_field(value: &Value, field_name: &str) -> Option<String> {
    if let Value::Table(tbl) = value {
        if let Some(fv) = tbl.get(field_name) {
            if let Some(s) = fv.as_str() {
                return Some(s.to_string());
            }
        }
    }
    None
}

// return the path to the config directory, based on XDG
pub fn dir() -> PathBuf {
    if let Some(dirs) = ProjectDirs::from("org", "dystroy", "broot") {
        dirs.config_dir().to_path_buf()
    } else {
        panic!("Unable to find configuration directories");
    }
}

impl Conf {
    pub fn default_location() -> PathBuf {
        dir().join("conf.toml")
    }
    // read the configuration file from the default OS specific location.
    // Create it if it doesn't exist
    pub fn from_default_location() -> Result<Conf, ConfError> {
        let conf_filepath = Conf::default_location();
        if !conf_filepath.exists() {
            Conf::write_sample(&conf_filepath)?;
            println!(
                "{}New Configuration file written in {:?}.{}",
                termion::style::Bold,
                &conf_filepath,
                termion::style::Reset
            );
            println!("You should have a look at it.");
        }
        Ok(Conf::from_file(&conf_filepath)?)
    }
    // assume the file doesn't yet exist
    pub fn write_sample(filepath: &Path) -> Result<(), io::Error> {
        fs::create_dir_all(filepath.parent().unwrap())?;
        fs::write(filepath, DEFAULT_CONF_FILE)?;
        Ok(())
    }
    // read the configuration from a given path. Assume it exists.
    // stderr is supposed to be a valid solution for displaying errors
    // (i.e. this function is called before or after the terminal alternation)
    pub fn from_file(filepath: &Path) -> Result<Conf, ConfError> {
        let data = fs::read_to_string(filepath)?;
        let root: Value = data.parse::<Value>()?;
        let mut verbs: Vec<VerbConf> = vec![];
        if let Value::Array(verbs_value) = &root["verbs"] {
            for verb_value in verbs_value.iter() {
                let invocation = match string_field(verb_value, "invocation") {
                    Some(s) => s,
                    None => {
                        eprintln!("Missing invocation in [[verbs]] entry in configuration");
                        continue;
                    }
                };
                let execution = match string_field(verb_value, "execution") {
                    Some(s) => s,
                    None => {
                        eprintln!("Missing execution in [[verbs]] entry in configuration");
                        continue;
                    }
                };
                let name = match string_field(verb_value, "name") {
                    Some(s) => s,
                    None => {
                        if execution.starts_with(':') {
                            // we'll assume that this entry isn't a new verb definition
                            // but just the addition of a shortcut for a built-in verb
                            "unnamed".to_string()
                        } else {
                            eprintln!("Missing name in [[verbs]] entry in configuration");
                            continue;
                        }
                    }
                };
                let mut from_shell = false;
                if let Value::Table(tbl) = verb_value {
                    if let Some(Value::Boolean(b)) = tbl.get("from_shell") {
                        from_shell = *b;
                    }
                };
                verbs.push(VerbConf {
                    name,
                    invocation,
                    execution,
                    from_shell,
                });
            }
        }
        Ok(Conf { verbs })
    }
}

const DEFAULT_CONF_FILE: &str = r#"
# This configuration file lets you define new commands
# or change the shortcut of built-in verbs.
#
# 'invocation' can be a letter or a word
# 'execution' is either a command, where {file} will be replaced by the selected line,
# 	or one of the built-in commands.
#
# The configuration documentation and complete list of built-in verbs
# can be found in https://github.com/Canop/broot/blob/master/documentation.md


###############################
# shortcuts for built-in verbs:

[[verbs]]
invocation = "p"
execution = ":parent"

#####################
# user defined verbs:

[[verbs]]
name = "edit"
invocation = "ed"
execution = "/usr/bin/nvim {file}"

[[verbs]]
name = "view"
invocation = "view"
execution = "less {file}"

"#;