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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
use crate::errors::Error;
use gettextrs::gettext;
use gio::{Settings, SettingsExt, SettingsSchemaSource};
use log::{debug, error, trace, warn};
use serde_derive::*;
use std::fs::OpenOptions;
use std::io::prelude::*;
use tempfile::tempdir;

/// Wrapper struct around `XiConfig`, it's annoying to pass around path otherwise
#[derive(Debug, Default)]
pub struct Config {
    pub path: String,
    pub config: XiConfig,
}

/// For stuff that goes into preferences.xiconfig
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct XiConfig {
    pub tab_size: u32,
    pub translate_tabs_to_spaces: bool,
    pub use_tab_stops: bool,
    pub plugin_search_path: Vec<String>,
    pub font_face: String,
    pub font_size: u32,
    pub auto_indent: bool,
    pub scroll_past_end: bool,
    pub wrap_width: u32,
    pub word_wrap: bool,
    pub autodetect_whitespace: bool,
    pub line_ending: String,
    pub surrounding_pairs: Vec<Vec<String>>,
    pub save_with_newline: bool,
}

impl Default for XiConfig {
    fn default() -> Self {
        #[cfg(windows)]
        const LINE_ENDING: &str = "\r\n";
        #[cfg(not(windows))]
        const LINE_ENDING: &str = "\n";

        let surrounding_pairs = vec![
            vec!["\"".to_string(), "\"".to_string()],
            vec!["'".to_string(), "'".to_string()],
            vec!["{".to_string(), "}".to_string()],
            vec!["[".to_string(), "]".to_string()],
        ];

        // Default valuess as dictated by https://github.com/xi-editor/xi-editor/blob/master/rust/core-lib/assets/client_example.toml
        Self {
            tab_size: 4,
            translate_tabs_to_spaces: false,
            use_tab_stops: true,
            plugin_search_path: vec![String::new()],
            font_face: get_default_monospace_font_schema(),
            font_size: 12,
            auto_indent: true,
            scroll_past_end: false,
            wrap_width: 0,
            word_wrap: false,
            autodetect_whitespace: true,
            line_ending: LINE_ENDING.to_string(),
            surrounding_pairs,
            save_with_newline: true,
        }
    }
}

impl Config {
    pub fn new() -> (String, Self) {
        if let Some(user_config_dir) = dirs::config_dir() {
            let config_dir = user_config_dir.join("gxi");
            std::fs::create_dir_all(&config_dir)
                .map_err(|e| {
                    error!(
                        "{}: {}",
                        gettext("Failed to create the config dir"),
                        e.to_string()
                    )
                })
                .unwrap();

            let mut xi_config = Self {
                config: XiConfig::default(),
                path: config_dir
                    .join("preferences.xiconfig")
                    .to_str()
                    .map(std::string::ToString::to_string)
                    .unwrap(),
            };

            xi_config = if let Ok(xi_config) = xi_config.open() {
                /*
                We have to immediately save the config file here to "upgrade" it (as in add missing
                entries which have been added by us during a version upgrade). This works because
                the above call to Config::new() sets defaults.
                */
                xi_config
                    .save()
                    .unwrap_or_else(|e| error!("{}", e.to_string()));

                Self {
                    path: xi_config.path.to_string(),
                    config: XiConfig {
                        tab_size: xi_config.config.tab_size,
                        translate_tabs_to_spaces: xi_config.config.translate_tabs_to_spaces,
                        use_tab_stops: xi_config.config.use_tab_stops,
                        plugin_search_path: xi_config.config.plugin_search_path.clone(),
                        font_face: xi_config.config.font_face.to_string(),
                        font_size: xi_config.config.font_size,
                        auto_indent: xi_config.config.auto_indent,
                        scroll_past_end: xi_config.config.scroll_past_end,
                        wrap_width: xi_config.config.wrap_width,
                        word_wrap: xi_config.config.word_wrap,
                        autodetect_whitespace: xi_config.config.autodetect_whitespace,
                        line_ending: xi_config.config.line_ending.to_string(),
                        surrounding_pairs: xi_config.config.surrounding_pairs.clone(),
                        save_with_newline: xi_config.config.save_with_newline,
                    },
                }
            } else {
                error!(
                    "{}",
                    gettext("Couldn't read config, falling back to the default XI-Editor config")
                );
                xi_config
                    .save()
                    .unwrap_or_else(|e| error!("{}", e.to_string()));
                xi_config
            };

            let config_dir = config_dir.into_os_string().into_string().unwrap();
            debug!(
                "{}: '{}'",
                gettext("Discovered config dir in home dir"),
                &config_dir
            );

            (config_dir, xi_config)
        } else {
            error!(
                "{}",
                gettext("Couldn't determine home dir! Settings will be temporary")
            );

            let config_dir = tempfile::Builder::new()
                .prefix("gxi-config")
                .tempdir()
                .map_err(|e| {
                    error!(
                        "{} {}",
                        gettext("Failed to create temporary config dir"),
                        e.to_string()
                    )
                })
                .unwrap()
                .into_path();

            let xi_config = Self {
                config: XiConfig::default(),
                path: config_dir
                    .join("preferences.xiconfig")
                    .to_str()
                    .map(std::string::ToString::to_string)
                    .unwrap(),
            };
            xi_config
                .save()
                .unwrap_or_else(|e| error!("{}", e.to_string()));

            let config_dir = config_dir.into_os_string().into_string().unwrap();

            debug!(
                "{}: '{}'",
                gettext("Discovered config dir in temporary dir"),
                &config_dir
            );

            (config_dir, xi_config)
        }
    }

    pub fn open(&mut self) -> Result<&mut Self, Error> {
        trace!("{}", gettext("Opening config file"));
        let mut config_file = OpenOptions::new().read(true).open(&self.path)?;
        let mut config_string = String::new();

        trace!("{}", gettext("Reading config file"));
        config_file.read_to_string(&mut config_string)?;

        let config_toml: XiConfig = toml::from_str(&config_string)?;
        debug!("{}: {:?}", gettext("Xi-Config"), config_toml);

        self.config = config_toml;

        Ok(self)
    }

    /// Atomically write the config. First writes the config to a tmp_file (non-atomic) and then
    /// copies that (atomically). This ensures that the config files stay valid
    pub fn save(&self) -> Result<(), Error> {
        trace!("{} '{}'", gettext("Saving config to"), &self.path);
        let tmp_dir = tempdir()?;
        let tmp_file_path = tmp_dir.path().join(".gxi-atomic");
        let mut tmp_file = OpenOptions::new()
            .write(true)
            .create(true)
            .open(&tmp_file_path)?;

        tmp_file.write_all(toml::to_string(&self.config).unwrap().as_bytes())?;
        std::fs::copy(&tmp_file_path, &self.path)?;
        OpenOptions::new().read(true).open(&self.path)?.sync_all()?;

        Ok(())
    }
}

pub trait GSchemaExt<RHS = Self> {
    fn get(schema_name: &str, field_name: &str) -> Option<RHS>;

    fn set(schema_name: &str, field_name: &str, val: RHS);
}

pub struct GSchema {}

impl GSchemaExt<String> for GSchema {
    fn get(schema_name: &str, field_name: &str) -> Option<String> {
        SettingsSchemaSource::get_default()
            .and_then(|settings_source| settings_source.lookup(schema_name, true))
            .and_then(|_| Settings::new(schema_name).get_string(field_name))
            .map(|s| s.to_string())
    }

    fn set(schema_name: &str, field_name: &str, val: String) {
        if SettingsSchemaSource::get_default()
            .and_then(|settings_source| settings_source.lookup(schema_name, true))
            .is_some()
        {
            Settings::new(schema_name).set_string(field_name, &val);
        };
    }
}

impl GSchemaExt<bool> for GSchema {
    fn get(schema_name: &str, field_name: &str) -> Option<bool> {
        SettingsSchemaSource::get_default()
            .and_then(|settings_source| settings_source.lookup(schema_name, true))
            .map(|_| Settings::new(schema_name).get_boolean(field_name))
    }

    fn set(schema_name: &str, field_name: &str, val: bool) {
        if SettingsSchemaSource::get_default()
            .and_then(|settings_source| settings_source.lookup(schema_name, true))
            .is_some()
        {
            Settings::new(schema_name).set_boolean(field_name, val);
        };
    }
}

impl GSchemaExt<u32> for GSchema {
    fn get(schema_name: &str, field_name: &str) -> Option<u32> {
        SettingsSchemaSource::get_default()
            .and_then(|settings_source| settings_source.lookup(schema_name, true))
            .map(|_| Settings::new(schema_name).get_uint(field_name))
    }

    fn set(schema_name: &str, field_name: &str, val: u32) {
        if SettingsSchemaSource::get_default()
            .and_then(|settings_source| settings_source.lookup(schema_name, true))
            .is_some()
        {
            Settings::new(schema_name).set_uint(field_name, val);
        };
    }
}

pub fn get_theme_schema() -> String {
    GSchema::get(app_id!(), "theme-name").unwrap_or_else(|| {
        warn!("Couldn't find GSchema! Defaulting to default theme.");
        "InspiredGitHub".to_string()
    })
}

pub fn set_theme_schema(theme_name: String) {
    GSchema::set(app_id!(), "theme-name", theme_name);
}

pub fn get_default_monospace_font_schema() -> String {
    GSchema::get("org.gnome.desktop.interface", "monospace-font-name").unwrap_or_else(|| {
        warn!("Couldn't find GSchema! Defaulting to default monospace font.");
        "Monospace".to_string()
    })
}

pub fn get_default_interface_font_schema() -> String {
    GSchema::get("org.gnome.desktop.interface", "font-name").unwrap_or_else(|| {
        warn!("Couldn't find GSchema! Defaulting to default interface font.");
        "Cantarell 11".to_string()
    })
}

pub fn get_draw_trailing_spaces_schema() -> bool {
    GSchema::get(app_id!(), "draw-trailing-spaces").unwrap_or_else(|| {
        warn!("Couldn't find GSchema! Defaulting to not drawing tabs!");
        false
    })
}

pub fn set_draw_trailing_spaces_schema(val: bool) {
    GSchema::set(app_id!(), "draw-trailing-spaces", val);
}

pub fn get_draw_right_margin() -> bool {
    GSchema::get(app_id!(), "draw-right-margin").unwrap_or_else(|| {
        warn!("Couldn't find GSchema! Defaulting to not drawing a right hand margin!");
        false
    })
}

pub fn set_draw_right_margin(val: bool) {
    GSchema::set(app_id!(), "draw-right-margin", val);
}

pub fn get_column_right_margin() -> u32 {
    GSchema::get(app_id!(), "column-right-margin").unwrap_or_else(|| {
        warn!("Couldn't find GSchema! Defaulting to drawing right hand marging at column 80");
        80
    })
}

pub fn set_column_right_margin(val: u32) {
    GSchema::set(app_id!(), "column-right-margin", val);
}

pub fn get_highlight_line() -> bool {
    GSchema::get(app_id!(), "highlight-line").unwrap_or_else(|| {
        warn!("Couldn't find GSchema! Defaulting to not highlighting the current line");
        false
    })
}

pub fn set_highlight_line(val: bool) {
    GSchema::set(app_id!(), "highlight-line", val);
}