summaryrefslogtreecommitdiff
path: root/src/config.rs
blob: 655b83502a77a63dac7af6e88278fa04098c3fc2 (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
//! Configuration file handling.

use crate::error::JournalError;
use crate::opt::Opt;

use directories_next::ProjectDirs;
use serde::Deserialize;
use std::default::Default;
use std::path::{Path, PathBuf};

const APP: &str = "jt2";

// The configuration file we read.
//
// Some of the fields are optional in the file. We will use default
// values for those, or get them command line options.
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct InputConfiguration {
    dirname: Option<PathBuf>,
    editor: Option<String>,
    entries: Option<PathBuf>,
}

impl InputConfiguration {
    fn read(filename: &Path) -> Result<Self, JournalError> {
        let text = std::fs::read(&filename)
            .map_err(|err| JournalError::ReadConfig(filename.to_path_buf(), err))?;
        let config = serde_yaml::from_slice(&text)
            .map_err(|err| JournalError::ConfigSyntax(filename.to_path_buf(), err))?;
        Ok(config)
    }
}

/// The run-time configuration.
///
/// This is the configuration as read from the configuration file, if
/// any, and with all command line options applied. Nothing here is
/// optional.
#[derive(Debug, Deserialize)]
pub struct Configuration {
    /// The directory where the journal is stored.
    pub dirname: PathBuf,

    /// The editor to open for editing journal entry drafts.
    pub editor: String,

    /// The directory where new entries are put.
    ///
    /// This is the full path name, not relative to `dirname`.
    pub entries: PathBuf,
}

impl Configuration {
    /// Read configuration file.
    ///
    /// The configuration is read from the file specified by the user
    /// on the command line, or from a default location following the
    /// XDG base directory specification. Note that only one of those
    /// is read.
    ///
    /// It's OK for the default configuration file to be missing, but
    /// if one is specified by the user explicitly, that one MUST
    /// exist.
    pub fn read(opt: &Opt) -> Result<Self, JournalError> {
        let proj_dirs =
            ProjectDirs::from("", "", APP).expect("could not figure out home directory");
        let filename = match &opt.global.config {
            Some(path) => {
                if !path.exists() {
                    return Err(JournalError::ConfigMissing(path.to_path_buf()));
                }
                path.to_path_buf()
            }
            None => proj_dirs.config_dir().to_path_buf().join("config.yaml"),
        };
        let input = if filename.exists() {
            InputConfiguration::read(&filename)?
        } else {
            InputConfiguration::default()
        };

        let dirname = if let Some(path) = &opt.global.dirname {
            path.to_path_buf()
        } else if let Some(path) = &input.dirname {
            expand_tilde(path)
        } else {
            proj_dirs.data_dir().to_path_buf()
        };

        Ok(Self {
            dirname: dirname.clone(),
            editor: if let Some(name) = &opt.global.editor {
                name.to_string()
            } else if let Some(name) = &input.editor {
                name.to_string()
            } else {
                "/usr/bin/editor".to_string()
            },
            entries: if let Some(entries) = &opt.global.entries {
                dirname.join(entries)
            } else if let Some(entries) = &input.entries {
                dirname.join(entries)
            } else {
                dirname.join("entries")
            },
        })
    }

    /// Write configuration to stdout.
    pub fn dump(&self) {
        println!("{:#?}", self);
    }
}

fn expand_tilde(path: &Path) -> PathBuf {
    if path.starts_with("~/") {
        if let Some(home) = std::env::var_os("HOME") {
            let mut expanded = PathBuf::from(home);
            for comp in path.components().skip(1) {
                expanded.push(comp);
            }
            expanded
        } else {
            path.to_path_buf()
        }
    } else {
        path.to_path_buf()
    }
}