1pub mod project;
4
5use anyhow::Result;
6use parse_display::{Display, FromStr};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use validator::{Validate, ValidateRange};
10
11const DEFAULT_THEME_COLOR: f64 = 264.5;
12const DEFAULT_PROJECT_NAME_TEMPLATE: &str = "project-$nnn";
13
14#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
16#[ts(export)]
17#[serde(rename_all = "snake_case")]
18pub struct Configuration {
19 #[serde(default, skip_serializing_if = "is_default")]
21 #[validate(nested)]
22 pub settings: Settings,
23}
24
25impl Configuration {
26 pub fn backwards_compatible_toml_parse(toml_str: &str) -> Result<Self> {
28 let mut settings = toml::from_str::<Self>(toml_str)?;
29
30 if let Some(project_directory) = &settings.settings.app.project_directory {
31 if settings.settings.project.directory.to_string_lossy().is_empty() {
32 settings.settings.project.directory.clone_from(project_directory);
33 settings.settings.app.project_directory = None;
34 }
35 }
36
37 if let Some(theme) = &settings.settings.app.theme {
38 if settings.settings.app.appearance.theme == AppTheme::default() {
39 settings.settings.app.appearance.theme = *theme;
40 settings.settings.app.theme = None;
41 }
42 }
43
44 if let Some(theme_color) = &settings.settings.app.theme_color {
45 if settings.settings.app.appearance.color == AppColor::default() {
46 settings.settings.app.appearance.color = theme_color.clone().into();
47 settings.settings.app.theme_color = None;
48 }
49 }
50
51 if let Some(enable_ssao) = settings.settings.app.enable_ssao {
52 if settings.settings.modeling.enable_ssao.into() {
53 settings.settings.modeling.enable_ssao = enable_ssao.into();
54 settings.settings.app.enable_ssao = None;
55 }
56 }
57
58 settings.validate()?;
59
60 Ok(settings)
61 }
62}
63
64#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
66#[ts(export)]
67#[serde(rename_all = "snake_case")]
68pub struct Settings {
69 #[serde(default, skip_serializing_if = "is_default")]
71 #[validate(nested)]
72 pub app: AppSettings,
73 #[serde(default, skip_serializing_if = "is_default")]
75 #[validate(nested)]
76 pub modeling: ModelingSettings,
77 #[serde(default, alias = "textEditor", skip_serializing_if = "is_default")]
79 #[validate(nested)]
80 pub text_editor: TextEditorSettings,
81 #[serde(default, alias = "projects", skip_serializing_if = "is_default")]
83 #[validate(nested)]
84 pub project: ProjectSettings,
85 #[serde(default, alias = "commandBar", skip_serializing_if = "is_default")]
87 #[validate(nested)]
88 pub command_bar: CommandBarSettings,
89}
90
91#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
95#[ts(export)]
96#[serde(rename_all = "snake_case")]
97pub struct AppSettings {
98 #[serde(default, skip_serializing_if = "is_default")]
100 #[validate(nested)]
101 pub appearance: AppearanceSettings,
102 #[serde(default, alias = "onboardingStatus", skip_serializing_if = "is_default")]
104 pub onboarding_status: OnboardingStatus,
105 #[serde(default, alias = "projectDirectory", skip_serializing_if = "Option::is_none")]
107 pub project_directory: Option<std::path::PathBuf>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub theme: Option<AppTheme>,
111 #[serde(default, skip_serializing_if = "Option::is_none", alias = "themeColor")]
113 pub theme_color: Option<FloatOrInt>,
114 #[serde(default, alias = "enableSSAO", skip_serializing_if = "Option::is_none")]
116 pub enable_ssao: Option<bool>,
117 #[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
120 pub dismiss_web_banner: bool,
121 #[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
123 stream_idle_mode: bool,
124 #[serde(default, alias = "allowOrbitInSketchMode", skip_serializing_if = "is_default")]
126 allow_orbit_in_sketch_mode: bool,
127}
128
129#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
131#[ts(export)]
132#[serde(untagged)]
133pub enum FloatOrInt {
134 String(String),
135 Float(f64),
136 Int(i64),
137}
138
139impl From<FloatOrInt> for f64 {
140 fn from(float_or_int: FloatOrInt) -> Self {
141 match float_or_int {
142 FloatOrInt::String(s) => s.parse().unwrap(),
143 FloatOrInt::Float(f) => f,
144 FloatOrInt::Int(i) => i as f64,
145 }
146 }
147}
148
149impl From<FloatOrInt> for AppColor {
150 fn from(float_or_int: FloatOrInt) -> Self {
151 match float_or_int {
152 FloatOrInt::String(s) => s.parse::<f64>().unwrap().into(),
153 FloatOrInt::Float(f) => f.into(),
154 FloatOrInt::Int(i) => (i as f64).into(),
155 }
156 }
157}
158
159#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
161#[ts(export)]
162#[serde(rename_all = "snake_case")]
163pub struct AppearanceSettings {
164 #[serde(default, skip_serializing_if = "is_default")]
166 pub theme: AppTheme,
167 #[serde(default, skip_serializing_if = "is_default")]
169 #[validate(nested)]
170 pub color: AppColor,
171}
172
173#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
174#[ts(export)]
175#[serde(transparent)]
176pub struct AppColor(pub f64);
177
178impl Default for AppColor {
179 fn default() -> Self {
180 Self(DEFAULT_THEME_COLOR)
181 }
182}
183
184impl From<AppColor> for f64 {
185 fn from(color: AppColor) -> Self {
186 color.0
187 }
188}
189
190impl From<f64> for AppColor {
191 fn from(color: f64) -> Self {
192 Self(color)
193 }
194}
195
196impl Validate for AppColor {
197 fn validate(&self) -> Result<(), validator::ValidationErrors> {
198 if !self.0.validate_range(Some(0.0), None, None, Some(360.0)) {
199 let mut errors = validator::ValidationErrors::new();
200 let mut err = validator::ValidationError::new("color");
201 err.add_param(std::borrow::Cow::from("min"), &0.0);
202 err.add_param(std::borrow::Cow::from("exclusive_max"), &360.0);
203 errors.add("color", err);
204 return Err(errors);
205 }
206 Ok(())
207 }
208}
209
210#[derive(
212 Debug, Default, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq,
213)]
214#[ts(export)]
215#[serde(rename_all = "snake_case")]
216#[display(style = "snake_case")]
217pub enum AppTheme {
218 Light,
220 Dark,
222 #[default]
225 System,
226}
227
228impl From<AppTheme> for kittycad::types::Color {
229 fn from(theme: AppTheme) -> Self {
230 match theme {
231 AppTheme::Light => kittycad::types::Color {
232 r: 249.0 / 255.0,
233 g: 249.0 / 255.0,
234 b: 249.0 / 255.0,
235 a: 1.0,
236 },
237 AppTheme::Dark => kittycad::types::Color {
238 r: 28.0 / 255.0,
239 g: 28.0 / 255.0,
240 b: 28.0 / 255.0,
241 a: 1.0,
242 },
243 AppTheme::System => {
244 todo!()
246 }
247 }
248 }
249}
250
251#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
253#[serde(rename_all = "snake_case")]
254#[ts(export)]
255pub struct ModelingSettings {
256 #[serde(default, alias = "defaultUnit", skip_serializing_if = "is_default")]
258 pub base_unit: UnitLength,
259 #[serde(default, alias = "cameraProjection", skip_serializing_if = "is_default")]
261 pub camera_projection: CameraProjectionType,
262 #[serde(default, alias = "cameraOrbit", skip_serializing_if = "is_default")]
264 pub camera_orbit: CameraOrbitType,
265 #[serde(default, alias = "mouseControls", skip_serializing_if = "is_default")]
267 pub mouse_controls: MouseControlType,
268 #[serde(default, alias = "highlightEdges", skip_serializing_if = "is_default")]
270 pub highlight_edges: DefaultTrue,
271 #[serde(default, alias = "showDebugPanel", skip_serializing_if = "is_default")]
274 pub show_debug_panel: bool,
275 #[serde(default, skip_serializing_if = "is_default")]
277 pub enable_ssao: DefaultTrue,
278 #[serde(default, alias = "showScaleGrid", skip_serializing_if = "is_default")]
280 pub show_scale_grid: bool,
281}
282
283#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
284#[ts(export)]
285#[serde(transparent)]
286pub struct DefaultTrue(pub bool);
287
288impl Default for DefaultTrue {
289 fn default() -> Self {
290 Self(true)
291 }
292}
293
294impl From<DefaultTrue> for bool {
295 fn from(default_true: DefaultTrue) -> Self {
296 default_true.0
297 }
298}
299
300impl From<bool> for DefaultTrue {
301 fn from(b: bool) -> Self {
302 Self(b)
303 }
304}
305
306#[derive(
308 Debug, Default, Eq, PartialEq, Copy, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr,
309)]
310#[cfg_attr(feature = "pyo3", pyo3::pyclass(eq, eq_int))]
311#[ts(export)]
312#[serde(rename_all = "lowercase")]
313#[display(style = "lowercase")]
314pub enum UnitLength {
315 Cm,
317 Ft,
319 In,
321 M,
323 #[default]
325 Mm,
326 Yd,
328}
329
330impl From<kittycad::types::UnitLength> for UnitLength {
331 fn from(unit: kittycad::types::UnitLength) -> Self {
332 match unit {
333 kittycad::types::UnitLength::Cm => UnitLength::Cm,
334 kittycad::types::UnitLength::Ft => UnitLength::Ft,
335 kittycad::types::UnitLength::In => UnitLength::In,
336 kittycad::types::UnitLength::M => UnitLength::M,
337 kittycad::types::UnitLength::Mm => UnitLength::Mm,
338 kittycad::types::UnitLength::Yd => UnitLength::Yd,
339 }
340 }
341}
342
343impl From<UnitLength> for kittycad::types::UnitLength {
344 fn from(unit: UnitLength) -> Self {
345 match unit {
346 UnitLength::Cm => kittycad::types::UnitLength::Cm,
347 UnitLength::Ft => kittycad::types::UnitLength::Ft,
348 UnitLength::In => kittycad::types::UnitLength::In,
349 UnitLength::M => kittycad::types::UnitLength::M,
350 UnitLength::Mm => kittycad::types::UnitLength::Mm,
351 UnitLength::Yd => kittycad::types::UnitLength::Yd,
352 }
353 }
354}
355
356impl From<kittycad_modeling_cmds::units::UnitLength> for UnitLength {
357 fn from(unit: kittycad_modeling_cmds::units::UnitLength) -> Self {
358 match unit {
359 kittycad_modeling_cmds::units::UnitLength::Centimeters => UnitLength::Cm,
360 kittycad_modeling_cmds::units::UnitLength::Feet => UnitLength::Ft,
361 kittycad_modeling_cmds::units::UnitLength::Inches => UnitLength::In,
362 kittycad_modeling_cmds::units::UnitLength::Meters => UnitLength::M,
363 kittycad_modeling_cmds::units::UnitLength::Millimeters => UnitLength::Mm,
364 kittycad_modeling_cmds::units::UnitLength::Yards => UnitLength::Yd,
365 }
366 }
367}
368
369impl From<UnitLength> for kittycad_modeling_cmds::units::UnitLength {
370 fn from(unit: UnitLength) -> Self {
371 match unit {
372 UnitLength::Cm => kittycad_modeling_cmds::units::UnitLength::Centimeters,
373 UnitLength::Ft => kittycad_modeling_cmds::units::UnitLength::Feet,
374 UnitLength::In => kittycad_modeling_cmds::units::UnitLength::Inches,
375 UnitLength::M => kittycad_modeling_cmds::units::UnitLength::Meters,
376 UnitLength::Mm => kittycad_modeling_cmds::units::UnitLength::Millimeters,
377 UnitLength::Yd => kittycad_modeling_cmds::units::UnitLength::Yards,
378 }
379 }
380}
381
382#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
384#[ts(export)]
385#[serde(rename_all = "snake_case")]
386#[display(style = "snake_case")]
387pub enum MouseControlType {
388 #[default]
389 #[display("zoo")]
390 #[serde(rename = "zoo", alias = "Zoo", alias = "KittyCAD")]
391 Zoo,
392 #[display("onshape")]
393 #[serde(rename = "onshape", alias = "OnShape")]
394 OnShape,
395 #[serde(alias = "Trackpad Friendly")]
396 TrackpadFriendly,
397 #[serde(alias = "Solidworks")]
398 Solidworks,
399 #[serde(alias = "NX")]
400 Nx,
401 #[serde(alias = "Creo")]
402 Creo,
403 #[display("autocad")]
404 #[serde(rename = "autocad", alias = "AutoCAD")]
405 AutoCad,
406}
407
408#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
410#[ts(export)]
411#[serde(rename_all = "snake_case")]
412#[display(style = "snake_case")]
413pub enum CameraProjectionType {
414 Perspective,
416 #[default]
418 Orthographic,
419}
420
421#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
423#[ts(export)]
424#[serde(rename_all = "snake_case")]
425#[display(style = "snake_case")]
426pub enum CameraOrbitType {
427 #[default]
429 #[display("spherical")]
430 Spherical,
431 #[display("trackball")]
433 Trackball,
434}
435
436#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
438#[serde(rename_all = "snake_case")]
439#[ts(export)]
440pub struct TextEditorSettings {
441 #[serde(default, alias = "textWrapping", skip_serializing_if = "is_default")]
443 pub text_wrapping: DefaultTrue,
444 #[serde(default, alias = "blinkingCursor", skip_serializing_if = "is_default")]
446 pub blinking_cursor: DefaultTrue,
447}
448
449#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
451#[serde(rename_all = "snake_case")]
452#[ts(export)]
453pub struct ProjectSettings {
454 #[serde(default, skip_serializing_if = "is_default")]
456 pub directory: std::path::PathBuf,
457 #[serde(default, alias = "defaultProjectName", skip_serializing_if = "is_default")]
459 pub default_project_name: ProjectNameTemplate,
460}
461
462#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq)]
463#[ts(export)]
464#[serde(transparent)]
465pub struct ProjectNameTemplate(pub String);
466
467impl Default for ProjectNameTemplate {
468 fn default() -> Self {
469 Self(DEFAULT_PROJECT_NAME_TEMPLATE.to_string())
470 }
471}
472
473impl From<ProjectNameTemplate> for String {
474 fn from(project_name: ProjectNameTemplate) -> Self {
475 project_name.0
476 }
477}
478
479impl From<String> for ProjectNameTemplate {
480 fn from(s: String) -> Self {
481 Self(s)
482 }
483}
484
485#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Eq, Validate)]
487#[serde(rename_all = "snake_case")]
488#[ts(export)]
489pub struct CommandBarSettings {
490 #[serde(default, alias = "includeSettings", skip_serializing_if = "is_default")]
492 pub include_settings: DefaultTrue,
493}
494
495#[derive(Debug, Default, Eq, PartialEq, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, Display, FromStr)]
497#[ts(export)]
498#[serde(rename_all = "snake_case")]
499#[display(style = "snake_case")]
500pub enum OnboardingStatus {
501 #[serde(rename = "")]
503 #[display("")]
504 Unset,
505 Completed,
507 #[default]
509 Incomplete,
510 Dismissed,
512
513 #[serde(rename = "/")]
515 #[display("/")]
516 Index,
517 #[serde(rename = "/camera")]
518 #[display("/camera")]
519 Camera,
520 #[serde(rename = "/streaming")]
521 #[display("/streaming")]
522 Streaming,
523 #[serde(rename = "/editor")]
524 #[display("/editor")]
525 Editor,
526 #[serde(rename = "/parametric-modeling")]
527 #[display("/parametric-modeling")]
528 ParametricModeling,
529 #[serde(rename = "/interactive-numbers")]
530 #[display("/interactive-numbers")]
531 InteractiveNumbers,
532 #[serde(rename = "/command-k")]
533 #[display("/command-k")]
534 CommandK,
535 #[serde(rename = "/user-menu")]
536 #[display("/user-menu")]
537 UserMenu,
538 #[serde(rename = "/project-menu")]
539 #[display("/project-menu")]
540 ProjectMenu,
541 #[serde(rename = "/export")]
542 #[display("/export")]
543 Export,
544 #[serde(rename = "/move")]
545 #[display("/move")]
546 Move,
547 #[serde(rename = "/sketching")]
548 #[display("/sketching")]
549 Sketching,
550 #[serde(rename = "/future-work")]
551 #[display("/future-work")]
552 FutureWork,
553}
554
555fn is_default<T: Default + PartialEq>(t: &T) -> bool {
556 t == &T::default()
557}
558
559#[cfg(test)]
560mod tests {
561 use pretty_assertions::assert_eq;
562 use validator::Validate;
563
564 use super::{
565 AppColor, AppSettings, AppTheme, AppearanceSettings, CameraProjectionType, CommandBarSettings, Configuration,
566 ModelingSettings, OnboardingStatus, ProjectSettings, Settings, TextEditorSettings, UnitLength,
567 };
568 use crate::settings::types::CameraOrbitType;
569
570 #[test]
571 fn test_backwards_compatible_project_settings_file_pw() {
574 let old_project_file = r#"[settings.app]
575theme = "dark"
576onboardingStatus = "dismissed"
577projectDirectory = ""
578enableSSAO = false
579
580[settings.modeling]
581defaultUnit = "in"
582cameraProjection = "orthographic"
583mouseControls = "KittyCAD"
584showDebugPanel = true
585
586[settings.projects]
587defaultProjectName = "project-$nnn"
588
589[settings.textEditor]
590textWrapping = true
591#"#;
592
593 let parsed = Configuration::backwards_compatible_toml_parse(old_project_file).unwrap();
595 assert_eq!(
596 parsed,
597 Configuration {
598 settings: Settings {
599 app: AppSettings {
600 appearance: AppearanceSettings {
601 theme: AppTheme::Dark,
602 color: Default::default()
603 },
604 onboarding_status: OnboardingStatus::Dismissed,
605 project_directory: None,
606 theme: None,
607 theme_color: None,
608 dismiss_web_banner: false,
609 enable_ssao: None,
610 stream_idle_mode: false,
611 allow_orbit_in_sketch_mode: false,
612 },
613 modeling: ModelingSettings {
614 base_unit: UnitLength::In,
615 camera_projection: CameraProjectionType::Orthographic,
616 camera_orbit: Default::default(),
617 mouse_controls: Default::default(),
618 highlight_edges: Default::default(),
619 show_debug_panel: true,
620 enable_ssao: false.into(),
621 show_scale_grid: false,
622 },
623 text_editor: TextEditorSettings {
624 text_wrapping: true.into(),
625 blinking_cursor: true.into()
626 },
627 project: Default::default(),
628 command_bar: CommandBarSettings {
629 include_settings: true.into()
630 },
631 }
632 }
633 );
634 }
635
636 #[test]
637 fn test_backwards_compatible_project_settings_file() {
640 let old_project_file = r#"[settings.app]
641theme = "dark"
642themeColor = "138"
643
644[settings.modeling]
645defaultUnit = "yd"
646showDebugPanel = true
647
648[settings.textEditor]
649textWrapping = false
650blinkingCursor = false
651
652[settings.commandBar]
653includeSettings = false
654#"#;
655
656 let parsed = Configuration::backwards_compatible_toml_parse(old_project_file).unwrap();
658 assert_eq!(
659 parsed,
660 Configuration {
661 settings: Settings {
662 app: AppSettings {
663 appearance: AppearanceSettings {
664 theme: AppTheme::Dark,
665 color: 138.0.into()
666 },
667 onboarding_status: Default::default(),
668 project_directory: None,
669 theme: None,
670 theme_color: None,
671 dismiss_web_banner: false,
672 enable_ssao: None,
673 stream_idle_mode: false,
674 allow_orbit_in_sketch_mode: false,
675 },
676 modeling: ModelingSettings {
677 base_unit: UnitLength::Yd,
678 camera_projection: Default::default(),
679 camera_orbit: Default::default(),
680 mouse_controls: Default::default(),
681 highlight_edges: Default::default(),
682 show_debug_panel: true,
683 enable_ssao: true.into(),
684 show_scale_grid: false,
685 },
686 text_editor: TextEditorSettings {
687 text_wrapping: false.into(),
688 blinking_cursor: false.into()
689 },
690 project: Default::default(),
691 command_bar: CommandBarSettings {
692 include_settings: false.into()
693 },
694 }
695 }
696 );
697 }
698
699 #[test]
700 fn test_backwards_compatible_app_settings_file() {
703 let old_app_settings_file = r#"[settings.app]
704onboardingStatus = "dismissed"
705projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects"
706theme = "dark"
707themeColor = "138"
708
709[settings.modeling]
710defaultUnit = "yd"
711showDebugPanel = true
712
713[settings.textEditor]
714textWrapping = false
715blinkingCursor = false
716
717[settings.commandBar]
718includeSettings = false
719
720[settings.projects]
721defaultProjectName = "projects-$nnn"
722#"#;
723
724 let parsed = Configuration::backwards_compatible_toml_parse(old_app_settings_file).unwrap();
726 assert_eq!(
727 parsed,
728 Configuration {
729 settings: Settings {
730 app: AppSettings {
731 appearance: AppearanceSettings {
732 theme: AppTheme::Dark,
733 color: 138.0.into()
734 },
735 onboarding_status: OnboardingStatus::Dismissed,
736 project_directory: None,
737 theme: None,
738 theme_color: None,
739 dismiss_web_banner: false,
740 enable_ssao: None,
741 stream_idle_mode: false,
742 allow_orbit_in_sketch_mode: false,
743 },
744 modeling: ModelingSettings {
745 base_unit: UnitLength::Yd,
746 camera_projection: Default::default(),
747 camera_orbit: CameraOrbitType::Spherical,
748 mouse_controls: Default::default(),
749 highlight_edges: Default::default(),
750 show_debug_panel: true,
751 enable_ssao: true.into(),
752 show_scale_grid: false,
753 },
754 text_editor: TextEditorSettings {
755 text_wrapping: false.into(),
756 blinking_cursor: false.into()
757 },
758 project: ProjectSettings {
759 directory: "/Users/macinatormax/Documents/kittycad-modeling-projects".into(),
760 default_project_name: "projects-$nnn".to_string().into()
761 },
762 command_bar: CommandBarSettings {
763 include_settings: false.into()
764 },
765 }
766 }
767 );
768
769 let serialized = toml::to_string(&parsed).unwrap();
771 assert_eq!(
772 serialized,
773 r#"[settings.app]
774onboarding_status = "dismissed"
775
776[settings.app.appearance]
777theme = "dark"
778color = 138.0
779
780[settings.modeling]
781base_unit = "yd"
782show_debug_panel = true
783
784[settings.text_editor]
785text_wrapping = false
786blinking_cursor = false
787
788[settings.project]
789directory = "/Users/macinatormax/Documents/kittycad-modeling-projects"
790default_project_name = "projects-$nnn"
791
792[settings.command_bar]
793include_settings = false
794"#
795 );
796 }
797
798 #[test]
799 fn test_settings_backwards_compat_partial() {
800 let partial_settings_file = r#"[settings.app]
801onboardingStatus = "dismissed"
802projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
803
804 let parsed = Configuration::backwards_compatible_toml_parse(partial_settings_file).unwrap();
806 assert_eq!(
807 parsed,
808 Configuration {
809 settings: Settings {
810 app: AppSettings {
811 appearance: AppearanceSettings {
812 theme: AppTheme::System,
813 color: Default::default()
814 },
815 onboarding_status: OnboardingStatus::Dismissed,
816 project_directory: None,
817 theme: None,
818 theme_color: None,
819 dismiss_web_banner: false,
820 enable_ssao: None,
821 stream_idle_mode: false,
822 allow_orbit_in_sketch_mode: false,
823 },
824 modeling: ModelingSettings {
825 base_unit: UnitLength::Mm,
826 camera_projection: Default::default(),
827 camera_orbit: Default::default(),
828 mouse_controls: Default::default(),
829 highlight_edges: true.into(),
830 show_debug_panel: false,
831 enable_ssao: true.into(),
832 show_scale_grid: false,
833 },
834 text_editor: TextEditorSettings {
835 text_wrapping: true.into(),
836 blinking_cursor: true.into()
837 },
838 project: ProjectSettings {
839 directory: "/Users/macinatormax/Documents/kittycad-modeling-projects".into(),
840 default_project_name: "project-$nnn".to_string().into()
841 },
842 command_bar: CommandBarSettings {
843 include_settings: true.into()
844 },
845 }
846 }
847 );
848
849 let serialized = toml::to_string(&parsed).unwrap();
851 assert_eq!(
852 serialized,
853 r#"[settings.app]
854onboarding_status = "dismissed"
855
856[settings.project]
857directory = "/Users/macinatormax/Documents/kittycad-modeling-projects"
858"#
859 );
860 }
861
862 #[test]
863 fn test_settings_empty_file_parses() {
864 let empty_settings_file = r#""#;
865
866 let parsed = toml::from_str::<Configuration>(empty_settings_file).unwrap();
867 assert_eq!(parsed, Configuration::default());
868
869 let serialized = toml::to_string(&parsed).unwrap();
871 assert_eq!(serialized, r#""#);
872
873 let parsed = Configuration::backwards_compatible_toml_parse(empty_settings_file).unwrap();
874 assert_eq!(parsed, Configuration::default());
875 }
876
877 #[test]
878 fn test_color_validation() {
879 let color = AppColor(360.0);
880
881 let result = color.validate();
882 if let Ok(r) = result {
883 panic!("Expected an error, but got success: {:?}", r);
884 }
885 assert!(result.is_err());
886 assert!(result
887 .unwrap_err()
888 .to_string()
889 .contains("color: Validation error: color"));
890
891 let appearance = AppearanceSettings {
892 theme: AppTheme::System,
893 color: AppColor(361.5),
894 };
895 let result = appearance.validate();
896 if let Ok(r) = result {
897 panic!("Expected an error, but got success: {:?}", r);
898 }
899 assert!(result.is_err());
900 assert!(result
901 .unwrap_err()
902 .to_string()
903 .contains("color: Validation error: color"));
904 }
905
906 #[test]
907 fn test_settings_color_validation_error() {
908 let settings_file = r#"[settings.app.appearance]
909color = 1567.4"#;
910
911 let result = Configuration::backwards_compatible_toml_parse(settings_file);
912 if let Ok(r) = result {
913 panic!("Expected an error, but got success: {:?}", r);
914 }
915 assert!(result.is_err());
916
917 assert!(result
918 .unwrap_err()
919 .to_string()
920 .contains("color: Validation error: color"));
921 }
922}