1use 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 #[serde(default)]
130 pub max_columns: u16,
131 #[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}