Skip to main content

bible/
settings.rs

1//! User-configurable settings — typography, theme, reader width, parallel
2//! divider. TOML at `<config_dir>/settings.toml`. Schema-versioned; on
3//! mismatch the file is ignored and defaults are used (file is left intact).
4
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::PathBuf;
8
9use crate::error::{Error, Result};
10use crate::storage::config_dir;
11
12const SCHEMA_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Settings {
16    pub schema_version: u32,
17    #[serde(default)]
18    pub typography: Typography,
19    #[serde(default)]
20    pub theme: ThemeSettings,
21    #[serde(default)]
22    pub reader: ReaderSettings,
23    #[serde(default)]
24    pub parallel: ParallelSettings,
25}
26
27impl Default for Settings {
28    fn default() -> Self {
29        Self {
30            schema_version: SCHEMA_VERSION,
31            typography: Typography::default(),
32            theme: ThemeSettings::default(),
33            reader: ReaderSettings::default(),
34            parallel: ParallelSettings::default(),
35        }
36    }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct Typography {
41    #[serde(default)]
42    pub script_letter_padding: ScriptPadding,
43    #[serde(default)]
44    pub word_padding: u8,
45    #[serde(default = "default_verse_spacing")]
46    pub verse_spacing: u8,
47    #[serde(default)]
48    pub line_spacing: u8,
49    #[serde(default)]
50    pub verse_number_style: VerseNumberStyle,
51    #[serde(default = "default_true")]
52    pub justify: bool,
53}
54
55impl Default for Typography {
56    fn default() -> Self {
57        Self {
58            script_letter_padding: ScriptPadding::default(),
59            word_padding: 0,
60            verse_spacing: default_verse_spacing(),
61            line_spacing: 0,
62            verse_number_style: VerseNumberStyle::default(),
63            justify: default_true(),
64        }
65    }
66}
67
68fn default_verse_spacing() -> u8 {
69    1
70}
71
72fn default_true() -> bool {
73    true
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct ScriptPadding {
78    #[serde(default)]
79    pub default: u8,
80    #[serde(default)]
81    pub tamil: u8,
82    #[serde(default)]
83    pub devanagari: u8,
84    #[serde(default)]
85    pub arabic: u8,
86    #[serde(default)]
87    pub hebrew: u8,
88    #[serde(default)]
89    pub cjk: u8,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "kebab-case")]
94pub enum VerseNumberStyle {
95    InlineBold,
96    Superscript,
97    Hidden,
98}
99
100impl Default for VerseNumberStyle {
101    fn default() -> Self {
102        VerseNumberStyle::InlineBold
103    }
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
107pub struct ThemeSettings {
108    #[serde(default)]
109    pub preset: ThemePreset,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "kebab-case")]
114pub enum ThemePreset {
115    Default,
116    SolarizedDark,
117    HighContrast,
118}
119
120impl Default for ThemePreset {
121    fn default() -> Self {
122        ThemePreset::Default
123    }
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
127pub struct ReaderSettings {
128    /// Hard cap on chapter-pane width in cells. `0` = no cap.
129    #[serde(default)]
130    pub max_columns: u16,
131    /// Translation id loaded on startup when no saved position exists. Empty
132    /// string = "first installed alphabetically".
133    #[serde(default)]
134    pub default_translation: String,
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize)]
138pub struct ParallelSettings {
139    #[serde(default)]
140    pub divider: DividerStyle,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "lowercase")]
145pub enum DividerStyle {
146    Single,
147    Double,
148    None,
149}
150
151impl Default for DividerStyle {
152    fn default() -> Self {
153        DividerStyle::Single
154    }
155}
156
157fn settings_path() -> Result<PathBuf> {
158    Ok(config_dir()?.join("settings.toml"))
159}
160
161pub fn load() -> Settings {
162    let Ok(path) = settings_path() else {
163        return Settings::default();
164    };
165    let Ok(s) = fs::read_to_string(&path) else {
166        return Settings::default();
167    };
168    let Ok(parsed) = toml::from_str::<Settings>(&s) else {
169        return Settings::default();
170    };
171    if parsed.schema_version != SCHEMA_VERSION {
172        return Settings::default();
173    }
174    parsed
175}
176
177pub fn save(s: &Settings) -> Result<()> {
178    let path = settings_path()?;
179    let toml_str = toml::to_string_pretty(s).map_err(|e| Error::Toml(e.to_string()))?;
180    fs::write(&path, toml_str).map_err(Error::Io)?;
181    Ok(())
182}