1use std::fs;
4use std::io;
5
6use serde::Deserialize;
7use serde::Serialize;
8use thiserror::Error;
9use tytanic_utils::result::ResultEx;
10use tytanic_utils::result::io_not_found;
11
12pub const MANIFEST_TOOL_KEY: &str = crate::TOOL_NAME;
14
15pub const CONFIG_SUB_DIRECTORY: &str = crate::TOOL_NAME;
17
18#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
21#[serde(deny_unknown_fields)]
22#[serde(rename_all = "kebab-case")]
23pub struct SystemConfig {}
24
25impl SystemConfig {
26 pub fn collect_user() -> Result<Option<Self>, Error> {
30 let Some(config_dir) = dirs::config_dir() else {
31 tracing::warn!("couldn't retrieve user config home");
32 return Ok(None);
33 };
34
35 let config = config_dir.join(CONFIG_SUB_DIRECTORY).join("config.toml");
36 let Some(content) = fs::read_to_string(config).ignore(io_not_found)? else {
37 return Ok(None);
38 };
39
40 Ok(toml::from_str(&content)?)
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Deserialize)]
46#[serde(deny_unknown_fields)]
47#[serde(rename_all = "kebab-case")]
48pub struct ProjectConfig {
49 #[serde(rename = "tests", default = "default_unit_tests_root")]
53 pub unit_tests_root: String,
54
55 #[serde(rename = "default", default)]
57 pub defaults: ProjectDefaults,
58}
59
60impl Default for ProjectConfig {
61 fn default() -> Self {
62 Self {
63 unit_tests_root: default_unit_tests_root(),
64 defaults: ProjectDefaults::default(),
65 }
66 }
67}
68
69fn default_unit_tests_root() -> String {
70 String::from("tests")
71}
72
73#[derive(Debug, Clone, PartialEq, Deserialize)]
74#[serde(deny_unknown_fields)]
75#[serde(rename_all = "kebab-case")]
76pub struct ProjectDefaults {
77 #[serde(rename = "dir", default = "default_direction")]
79 pub direction: Direction,
80
81 #[serde(default = "default_ppi")]
85 pub ppi: f32,
86
87 #[serde(default = "default_max_delta")]
91 pub max_delta: u8,
92
93 #[serde(default = "default_max_deviations")]
97 pub max_deviations: usize,
98}
99
100impl Default for ProjectDefaults {
101 fn default() -> Self {
102 Self {
103 direction: default_direction(),
104 ppi: default_ppi(),
105 max_delta: default_max_delta(),
106 max_deviations: default_max_deviations(),
107 }
108 }
109}
110
111fn default_direction() -> Direction {
112 Direction::Ltr
113}
114
115fn default_ppi() -> f32 {
116 144.0
117}
118
119fn default_max_delta() -> u8 {
120 1
121}
122
123fn default_max_deviations() -> usize {
124 0
125}
126
127#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
129#[serde(rename_all = "kebab-case")]
130pub enum Direction {
131 #[default]
133 Ltr,
134
135 Rtl,
137}
138
139#[derive(Debug, Error)]
141pub enum Error {
142 #[error("a toml parsing error occurred")]
144 Toml(#[from] toml::de::Error),
145
146 #[error("an io error occurred")]
148 Io(#[from] io::Error),
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
157 fn config_defaults_section_is_optional() {
158 let config = r#"
159 [package]
160 name = "testpackage"
161 version = "0.1.0"
162 entrypoint = "lib.typ"
163
164 [tool.tytanic]
165 tests = "test_dir"
166 "#;
167
168 let manifest = toml::from_str::<typst::syntax::package::PackageManifest>(config).unwrap();
169 let project_config = ProjectConfig::deserialize(
170 manifest
171 .tool
172 .sections
173 .get(crate::TOOL_NAME)
174 .unwrap()
175 .to_owned(),
176 )
177 .unwrap();
178
179 assert_eq!(project_config.unit_tests_root, "test_dir");
180 assert_eq!(project_config.defaults.ppi, ProjectDefaults::default().ppi);
181 }
182}